From 1bd54d4cfb30b354a56188b076f00423294ce71b Mon Sep 17 00:00:00 2001 From: Ryan Goodfellow Date: Thu, 11 Dec 2025 19:11:16 +0000 Subject: [PATCH 01/20] ipv6 all the things --- Cargo.lock | 42 ++++++++--------- Cargo.toml | 12 ++--- illumos-utils/src/running_zone.rs | 46 +++++++++++-------- .../src/db/datastore/address_lot.rs | 35 +++++++------- nexus/external-api/src/lib.rs | 2 +- nexus/internal-api/src/lib.rs | 7 +++ nexus/src/app/background/tasks/nat_cleanup.rs | 2 +- .../background/tasks/sync_service_zone_nat.rs | 2 +- nexus/src/app/instance_network.rs | 6 +-- nexus/src/internal_api/http_entrypoints.rs | 3 +- nexus/tests/integration_tests/address_lots.rs | 35 +++++++++++++- package-manifest.toml | 24 +++++----- tools/dendrite_stub_checksums | 6 +-- tools/dendrite_version | 2 +- tools/maghemite_ddm_openapi_version | 2 +- tools/maghemite_mg_openapi_version | 2 +- tools/maghemite_mgd_checksums | 4 +- tools/opte_version_override | 2 +- 18 files changed, 140 insertions(+), 94 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e76aed1376c..2f033d89997 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1773,7 +1773,7 @@ dependencies = [ [[package]] name = "common" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/dendrite?rev=ab30fa91227fd478bfe0e023310ca83dec0bc22b#ab30fa91227fd478bfe0e023310ca83dec0bc22b" +source = "git+https://github.com/oxidecomputer/dendrite?rev=21fbf39f63b25567b25aeadf0d9a73a01c842512#21fbf39f63b25567b25aeadf0d9a73a01c842512" dependencies = [ "anyhow", "chrono", @@ -2481,7 +2481,7 @@ dependencies = [ [[package]] name = "ddm-admin-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=205b3ccf75b527ac7a565285fdcc0c78f4fcee95#205b3ccf75b527ac7a565285fdcc0c78f4fcee95" +source = "git+https://github.com/oxidecomputer/maghemite?rev=3877aa0467fe275806f07ff4f7e92efa43e6fa6d#3877aa0467fe275806f07ff4f7e92efa43e6fa6d" dependencies = [ "oxnet", "progenitor 0.11.2", @@ -3007,11 +3007,11 @@ dependencies = [ [[package]] name = "dpd-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/dendrite?rev=ab30fa91227fd478bfe0e023310ca83dec0bc22b#ab30fa91227fd478bfe0e023310ca83dec0bc22b" +source = "git+https://github.com/oxidecomputer/dendrite?rev=21fbf39f63b25567b25aeadf0d9a73a01c842512#21fbf39f63b25567b25aeadf0d9a73a01c842512" dependencies = [ "async-trait", "chrono", - "common 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=ab30fa91227fd478bfe0e023310ca83dec0bc22b)", + "common 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=21fbf39f63b25567b25aeadf0d9a73a01c842512)", "crc8", "futures", "http", @@ -5129,7 +5129,7 @@ dependencies = [ [[package]] name = "illumos-sys-hdrs" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=795a1e0aeefb7a2c6fe4139779fdf66930d09b80#795a1e0aeefb7a2c6fe4139779fdf66930d09b80" +source = "git+https://github.com/oxidecomputer/opte?rev=a1ed0960673b6ca2e6b68835537f53cc86110a77#a1ed0960673b6ca2e6b68835537f53cc86110a77" dependencies = [ "bitflags 2.9.4", ] @@ -5781,7 +5781,7 @@ dependencies = [ [[package]] name = "kstat-macro" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=795a1e0aeefb7a2c6fe4139779fdf66930d09b80#795a1e0aeefb7a2c6fe4139779fdf66930d09b80" +source = "git+https://github.com/oxidecomputer/opte?rev=a1ed0960673b6ca2e6b68835537f53cc86110a77#a1ed0960673b6ca2e6b68835537f53cc86110a77" dependencies = [ "quote", "syn 2.0.111", @@ -6363,7 +6363,7 @@ dependencies = [ [[package]] name = "mg-admin-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=205b3ccf75b527ac7a565285fdcc0c78f4fcee95#205b3ccf75b527ac7a565285fdcc0c78f4fcee95" +source = "git+https://github.com/oxidecomputer/maghemite?rev=3877aa0467fe275806f07ff4f7e92efa43e6fa6d#3877aa0467fe275806f07ff4f7e92efa43e6fa6d" dependencies = [ "chrono", "colored 3.0.0", @@ -7457,7 +7457,7 @@ dependencies = [ "crucible-agent-client", "dns-server", "dns-service-client", - "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=ab30fa91227fd478bfe0e023310ca83dec0bc22b)", + "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=21fbf39f63b25567b25aeadf0d9a73a01c842512)", "dropshot", "futures", "gateway-messages", @@ -8345,7 +8345,7 @@ dependencies = [ "display-error-chain", "dns-server", "dns-service-client", - "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=ab30fa91227fd478bfe0e023310ca83dec0bc22b)", + "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=21fbf39f63b25567b25aeadf0d9a73a01c842512)", "dropshot", "ereport-types", "expectorate", @@ -8828,7 +8828,7 @@ dependencies = [ "display-error-chain", "dns-server", "dns-service-client", - "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=ab30fa91227fd478bfe0e023310ca83dec0bc22b)", + "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=21fbf39f63b25567b25aeadf0d9a73a01c842512)", "dropshot", "expectorate", "flate2", @@ -9282,7 +9282,7 @@ dependencies = [ [[package]] name = "opte" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=795a1e0aeefb7a2c6fe4139779fdf66930d09b80#795a1e0aeefb7a2c6fe4139779fdf66930d09b80" +source = "git+https://github.com/oxidecomputer/opte?rev=a1ed0960673b6ca2e6b68835537f53cc86110a77#a1ed0960673b6ca2e6b68835537f53cc86110a77" dependencies = [ "bitflags 2.9.4", "dyn-clone", @@ -9301,7 +9301,7 @@ dependencies = [ [[package]] name = "opte-api" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=795a1e0aeefb7a2c6fe4139779fdf66930d09b80#795a1e0aeefb7a2c6fe4139779fdf66930d09b80" +source = "git+https://github.com/oxidecomputer/opte?rev=a1ed0960673b6ca2e6b68835537f53cc86110a77#a1ed0960673b6ca2e6b68835537f53cc86110a77" dependencies = [ "illumos-sys-hdrs", "ingot", @@ -9314,7 +9314,7 @@ dependencies = [ [[package]] name = "opte-ioctl" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=795a1e0aeefb7a2c6fe4139779fdf66930d09b80#795a1e0aeefb7a2c6fe4139779fdf66930d09b80" +source = "git+https://github.com/oxidecomputer/opte?rev=a1ed0960673b6ca2e6b68835537f53cc86110a77#a1ed0960673b6ca2e6b68835537f53cc86110a77" dependencies = [ "libc", "libnet", @@ -9416,7 +9416,7 @@ dependencies = [ [[package]] name = "oxide-vpc" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=795a1e0aeefb7a2c6fe4139779fdf66930d09b80#795a1e0aeefb7a2c6fe4139779fdf66930d09b80" +source = "git+https://github.com/oxidecomputer/opte?rev=a1ed0960673b6ca2e6b68835537f53cc86110a77#a1ed0960673b6ca2e6b68835537f53cc86110a77" dependencies = [ "cfg-if", "illumos-sys-hdrs", @@ -11441,7 +11441,7 @@ dependencies = [ [[package]] name = "rdb-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=205b3ccf75b527ac7a565285fdcc0c78f4fcee95#205b3ccf75b527ac7a565285fdcc0c78f4fcee95" +source = "git+https://github.com/oxidecomputer/maghemite?rev=3877aa0467fe275806f07ff4f7e92efa43e6fa6d#3877aa0467fe275806f07ff4f7e92efa43e6fa6d" dependencies = [ "oxnet", "schemars 0.8.22", @@ -11582,18 +11582,18 @@ dependencies = [ [[package]] name = "ref-cast" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", @@ -16261,7 +16261,7 @@ name = "wicket-common" version = "0.1.0" dependencies = [ "anyhow", - "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=ab30fa91227fd478bfe0e023310ca83dec0bc22b)", + "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=21fbf39f63b25567b25aeadf0d9a73a01c842512)", "dropshot", "gateway-client", "gateway-types", @@ -16321,7 +16321,7 @@ dependencies = [ "clap", "debug-ignore", "display-error-chain", - "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=ab30fa91227fd478bfe0e023310ca83dec0bc22b)", + "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=21fbf39f63b25567b25aeadf0d9a73a01c842512)", "dropshot", "either", "expectorate", diff --git a/Cargo.toml b/Cargo.toml index 72dc0a7c2ab..0e58e9dd14f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -462,7 +462,7 @@ digest = "0.10.7" dns-server = { path = "dns-server" } dns-server-api = { path = "dns-server-api" } dns-service-client = { path = "clients/dns-service-client" } -dpd-client = { git = "https://github.com/oxidecomputer/dendrite", rev = "ab30fa91227fd478bfe0e023310ca83dec0bc22b" } +dpd-client = { git = "https://github.com/oxidecomputer/dendrite", rev = "21fbf39f63b25567b25aeadf0d9a73a01c842512" } dropshot = { version = "0.16.6", features = [ "usdt-probes" ] } dropshot-api-manager = "0.2.4" dropshot-api-manager-types = "0.2.4" @@ -566,8 +566,8 @@ ntp-admin-api = { path = "ntp-admin/api" } ntp-admin-client = { path = "clients/ntp-admin-client" } ntp-admin-types = { path = "ntp-admin/types" } ntp-admin-types-versions = { path = "ntp-admin/types/versions" } -mg-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "205b3ccf75b527ac7a565285fdcc0c78f4fcee95" } -ddm-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "205b3ccf75b527ac7a565285fdcc0c78f4fcee95" } +mg-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "3877aa0467fe275806f07ff4f7e92efa43e6fa6d" } +ddm-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "3877aa0467fe275806f07ff4f7e92efa43e6fa6d" } multimap = "0.10.1" nexus-auth = { path = "nexus/auth" } nexus-background-task-interface = { path = "nexus/background-task-interface" } @@ -626,7 +626,7 @@ omicron-workspace-hack = "0.1.0" omicron-zone-package = "0.12.2" oxide-client = { path = "clients/oxide-client" } oxide-tokio-rt = "0.1.2" -oxide-vpc = { git = "https://github.com/oxidecomputer/opte", rev = "795a1e0aeefb7a2c6fe4139779fdf66930d09b80", features = [ "api", "std" ] } +oxide-vpc = { git = "https://github.com/oxidecomputer/opte", rev = "a1ed0960673b6ca2e6b68835537f53cc86110a77", features = [ "api", "std" ] } oxlog = { path = "dev-tools/oxlog" } oxnet = "0.1.3" once_cell = "1.21.3" @@ -635,7 +635,7 @@ openapiv3 = "2.2.0" # must match samael's crate! openssl = "0.10" openssl-sys = "0.9" -opte-ioctl = { git = "https://github.com/oxidecomputer/opte", rev = "795a1e0aeefb7a2c6fe4139779fdf66930d09b80" } +opte-ioctl = { git = "https://github.com/oxidecomputer/opte", rev = "a1ed0960673b6ca2e6b68835537f53cc86110a77" } oso = "0.27" owo-colors = "4.2.2" oximeter = { path = "oximeter/oximeter" } @@ -697,7 +697,7 @@ rats-corim = { git = "https://github.com/oxidecomputer/rats-corim.git", rev = "f raw-cpuid = { git = "https://github.com/oxidecomputer/rust-cpuid.git", rev = "a4cf01df76f35430ff5d39dc2fe470bcb953503b" } rayon = "1.10" rcgen = "0.12.1" -rdb-types = { git = "https://github.com/oxidecomputer/maghemite", rev = "205b3ccf75b527ac7a565285fdcc0c78f4fcee95" } +rdb-types = { git = "https://github.com/oxidecomputer/maghemite", rev = "3877aa0467fe275806f07ff4f7e92efa43e6fa6d" } reconfigurator-cli = { path = "dev-tools/reconfigurator-cli" } reedline = "0.40.0" ref-cast = "1.0" diff --git a/illumos-utils/src/running_zone.rs b/illumos-utils/src/running_zone.rs index 532086b9b80..63553f93d59 100644 --- a/illumos-utils/src/running_zone.rs +++ b/illumos-utils/src/running_zone.rs @@ -353,30 +353,30 @@ impl RunningZone { Ok(network) } - // TODO-completeness: Handle dual-stack OPTE ports here. This works for - // either IPv4 or IPv6 addresses, but not both. - // See https://github.com/oxidecomputer/omicron/issues/9247. pub async fn ensure_address_for_port( &self, name: &str, port_idx: usize, - ) -> Result { + ) -> Result<(), EnsureAddressError> { info!(self.inner.log, "Ensuring address for OPTE port"); + let port = self.opte_ports().nth(port_idx).ok_or_else(|| { EnsureAddressError::MissingOptePort { zone: self.inner.name.clone(), port_idx, } })?; - let addrobj = AddrObject::new(port.name(), name).map_err(|err| { - EnsureAddressError::AddrObject { - request: AddressRequest::Dhcp, - zone: self.inner.name.clone(), - err, - } - })?; let zone = Some(self.inner.name.as_ref()); if let Some(gateway) = port.gateway().ipv4_addr() { + let v4_name = format!("{}4", name); + let addrobj = + AddrObject::new(port.name(), &v4_name).map_err(|err| { + EnsureAddressError::AddrObject { + request: AddressRequest::Dhcp, + zone: self.inner.name.clone(), + err, + } + })?; let addr = Zones::ensure_address(zone, &addrobj, AddressRequest::Dhcp) .await?; @@ -403,8 +403,17 @@ impl RunningZone { "default", &gateway_ip, ])?; - Ok(addr) - } else { + } + if port.gateway().ipv6_addr().is_some() { + let v6_name = format!("{}6", name); + let addrobj = + AddrObject::new(port.name(), &v6_name).map_err(|err| { + EnsureAddressError::AddrObject { + request: AddressRequest::Dhcp, + zone: self.inner.name.clone(), + err, + } + })?; // If the port is using IPv6 addressing we still want it to use // DHCP(v6) which requires first creating a link-local address. Zones::ensure_has_link_local_v6_address(zone, &addrobj) @@ -430,15 +439,13 @@ impl RunningZone { ) })?; - // Ipv6Addr::is_unicast_link_local is sadly not stable - let is_ll = - |ip: Ipv6Addr| (ip.segments()[0] & 0xffc0) == 0xfe80; - // Look for a non link-local addr addrs .into_iter() .find(|addr| match addr { - IpNetwork::V6(ip) => !is_ll(ip.ip()), + IpNetwork::V6(ip) => { + !ip.ip().is_unicast_link_local() + } _ => false, }) .ok_or_else(|| { @@ -458,8 +465,9 @@ impl RunningZone { ); }, ) - .await + .await?; } + Ok(()) } pub fn add_default_route( diff --git a/nexus/db-queries/src/db/datastore/address_lot.rs b/nexus/db-queries/src/db/datastore/address_lot.rs index 8ac999eb53e..98d351c8701 100644 --- a/nexus/db-queries/src/db/datastore/address_lot.rs +++ b/nexus/db-queries/src/db/datastore/address_lot.rs @@ -95,22 +95,6 @@ impl DataStore { let found_blocks: Vec = block_dsl::address_lot_block .filter(block_dsl::address_lot_id.eq(db_lot.id())) - .filter( - block_dsl::first_address.eq_any( - desired_blocks - .iter() - .map(|b| b.first_address) - .collect::>(), - ), - ) - .filter( - block_dsl::last_address.eq_any( - desired_blocks - .iter() - .map(|b| b.last_address) - .collect::>(), - ), - ) .get_results_async(&conn) .await?; @@ -118,7 +102,7 @@ impl DataStore { // If the block is found in the database, use the found block. // If the block is not found in the database, insert it. - for desired_block in desired_blocks { + for desired_block in &desired_blocks { let block = match found_blocks.iter().find(|db_b| { db_b.first_address == desired_block.first_address && db_b.last_address == desired_block.last_address @@ -126,7 +110,7 @@ impl DataStore { Some(block) => block.clone(), None => { diesel::insert_into(block_dsl::address_lot_block) - .values(desired_block) + .values(desired_block.clone()) .returning(AddressLotBlock::as_returning()) .get_results_async(&conn) .await?[0] @@ -136,6 +120,21 @@ impl DataStore { blocks.push(block); } + // If the block is found in the database, but not desired, + // remove it. + for found_block in &found_blocks { + if !desired_blocks.iter().any(|x| { + x.first_address == found_block.first_address + && x.last_address == found_block.last_address + }) { + diesel::delete(block_dsl::address_lot_block) + .filter(block_dsl::address_lot_id.eq(db_lot.id())) + .filter(block_dsl::id.eq(found_block.id)) + .execute_async(&conn) + .await?; + } + } + Ok(AddressLotCreateResult { lot: db_lot, blocks }) }) .await diff --git a/nexus/external-api/src/lib.rs b/nexus/external-api/src/lib.rs index 03751e73a9a..bc2f3ed96c0 100644 --- a/nexus/external-api/src/lib.rs +++ b/nexus/external-api/src/lib.rs @@ -1696,7 +1696,7 @@ pub trait NexusExternalApi { /// Create instance #[endpoint { - operation_id = "disk_create", + operation_id = "instance_create", method = POST, path = "/v1/instances", tags = ["instances"], diff --git a/nexus/internal-api/src/lib.rs b/nexus/internal-api/src/lib.rs index b0d31947025..a211aa868d6 100644 --- a/nexus/internal-api/src/lib.rs +++ b/nexus/internal-api/src/lib.rs @@ -237,6 +237,13 @@ pub trait NexusInternalApi { /// Fetch NAT ChangeSet /// + /// NOTE: This is no longer just IPv4, it includes IPv6. However, this API + /// cannot have forward-incompatable changes (e.g. clients running against + /// new API definitions need to be able to talk to older server still) so + /// we cannot add a generic nat_changeset API endpoint which would only be + /// backwards compatiable. So for the time being we are stuck with this + /// misleading name. + /// /// Caller provides their generation as `from_gen`, along with a query /// parameter for the page size (`limit`). Endpoint will return changes /// that have occured since the caller's generation number up to the latest diff --git a/nexus/src/app/background/tasks/nat_cleanup.rs b/nexus/src/app/background/tasks/nat_cleanup.rs index 9efe1c7506b..5f6d3754c1d 100644 --- a/nexus/src/app/background/tasks/nat_cleanup.rs +++ b/nexus/src/app/background/tasks/nat_cleanup.rs @@ -92,7 +92,7 @@ impl BackgroundTask for Ipv4NatGarbageCollector { } for client in dpd_clients.values() { - let response = client.ipv4_nat_generation().await; + let response = client.nat_generation().await; match response { Ok(r#gen) => min_gen = std::cmp::min(min_gen, *r#gen), Err(error) => { diff --git a/nexus/src/app/background/tasks/sync_service_zone_nat.rs b/nexus/src/app/background/tasks/sync_service_zone_nat.rs index 3483d439c20..9c70fa4ce31 100644 --- a/nexus/src/app/background/tasks/sync_service_zone_nat.rs +++ b/nexus/src/app/background/tasks/sync_service_zone_nat.rs @@ -275,7 +275,7 @@ impl BackgroundTask for ServiceZoneNatTracker { }; for (_location, client) in dpd_clients { - if let Err(e) = client.ipv4_nat_trigger_update().await { + if let Err(e) = client.nat_trigger_update().await { error!( &log, "failed to trigger dpd rpw workflow"; diff --git a/nexus/src/app/instance_network.rs b/nexus/src/app/instance_network.rs index ab5f1d066d1..1a34605ab6e 100644 --- a/nexus/src/app/instance_network.rs +++ b/nexus/src/app/instance_network.rs @@ -510,7 +510,7 @@ pub(crate) async fn probe_ensure_dpd_config( // Notify dendrite that there are changes for it to reconcile. // In the event of a failure to notify dendrite, we'll log an error // and rely on dendrite's RPW timer to catch it up. - if let Err(e) = dpd_client.ipv4_nat_trigger_update().await { + if let Err(e) = dpd_client.nat_trigger_update().await { error!(log, "failed to notify dendrite of nat updates"; "error" => ?e); }; @@ -642,7 +642,7 @@ pub(crate) async fn probe_delete_dpd_config( // Notify dendrite that there are changes for it to reconcile. // In the event of a failure to notify dendrite, we'll log an error // and rely on dendrite's RPW timer to catch it up. - if let Err(e) = dpd_client.ipv4_nat_trigger_update().await { + if let Err(e) = dpd_client.nat_trigger_update().await { error!(log, "failed to notify dendrite of nat updates"; "error" => ?e); }; } @@ -762,7 +762,7 @@ async fn notify_dendrite_nat_state( // Notify dendrite that there are changes for it to reconcile. // In the event of a failure to notify dendrite, we'll log an error // and rely on dendrite's RPW timer to catch it up. - if let Err(e) = dpd_client.ipv4_nat_trigger_update().await { + if let Err(e) = dpd_client.nat_trigger_update().await { error!(log, "failed to notify dendrite of nat updates"; "error" => ?e); }; } diff --git a/nexus/src/internal_api/http_entrypoints.rs b/nexus/src/internal_api/http_entrypoints.rs index 774ff419def..ece90cf28f5 100644 --- a/nexus/src/internal_api/http_entrypoints.rs +++ b/nexus/src/internal_api/http_entrypoints.rs @@ -16,6 +16,7 @@ use dropshot::Query; use dropshot::RequestContext; use dropshot::ResultsPage; use dropshot::TypedBody; +use nexus_internal_api::ProbeInfo; use nexus_internal_api::*; use nexus_types::internal_api::params::SledAgentInfo; use nexus_types::internal_api::params::SwitchPutRequest; @@ -409,8 +410,6 @@ impl NexusInternalApi for NexusInternalApiImpl { .await } - // NAT RPW internal APIs - async fn ipv4_nat_changeset( rqctx: RequestContext, path_params: Path, diff --git a/nexus/tests/integration_tests/address_lots.rs b/nexus/tests/integration_tests/address_lots.rs index 5637e0ab48d..3a52825f3b4 100644 --- a/nexus/tests/integration_tests/address_lots.rs +++ b/nexus/tests/integration_tests/address_lots.rs @@ -39,7 +39,7 @@ async fn test_address_lot_basic_crud(ctx: &ControlPlaneTestContext) { assert_eq!(lots.len(), 1, "Expected one lot"); // Create a lot - let params = AddressLotCreate { + let mut params = AddressLotCreate { identity: IdentityMetadataCreateParams { name: "parkinglot".parse().unwrap(), description: "an address parking lot".into(), @@ -124,6 +124,39 @@ async fn test_address_lot_basic_crud(ctx: &ControlPlaneTestContext) { assert_eq!(blist.len(), 1, "Expected 1 address lot block"); assert_eq!(blist[0], blocks[0]); + + // add a block to a lot + params.blocks.push(AddressLotBlockCreate { + first_address: "fd00:203:113::10".parse().unwrap(), + last_address: "fd00:203:113::20".parse().unwrap(), + }); + let response: AddressLotCreateResponse = NexusRequest::objects_post( + client, + "/v1/system/networking/address-lot", + ¶ms, + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap() + .parsed_body() + .unwrap(); + assert_eq!(response.blocks.len(), params.blocks.len()); + + // remove block from lot + params.blocks.pop(); + let response: AddressLotCreateResponse = NexusRequest::objects_post( + client, + "/v1/system/networking/address-lot", + ¶ms, + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap() + .parsed_body() + .unwrap(); + assert_eq!(response.blocks.len(), params.blocks.len()); } #[nexus_test] diff --git a/package-manifest.toml b/package-manifest.toml index 81556853f23..3ed704cbd92 100644 --- a/package-manifest.toml +++ b/package-manifest.toml @@ -654,10 +654,10 @@ source.repo = "maghemite" # `tools/maghemite_openapi_version`. Failing to do so will cause a failure when # building `ddm-admin-client` (which will instruct you to update # `tools/maghemite_openapi_version`). -source.commit = "205b3ccf75b527ac7a565285fdcc0c78f4fcee95" +source.commit = "3877aa0467fe275806f07ff4f7e92efa43e6fa6d" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/maghemite/image//mg-ddm-gz.sha256.txt -source.sha256 = "fa122dfaf77b1c060f19ca0e044f57044342c8c2b444595ca156ac8885852ebe" +source.sha256 = "c317e4bd2958324a6de2b5eba3380cd3b4071617676684213eb6ddbd33087118" output.type = "tarball" [package.mg-ddm] @@ -670,10 +670,10 @@ source.repo = "maghemite" # `tools/maghemite_openapi_version`. Failing to do so will cause a failure when # building `ddm-admin-client` (which will instruct you to update # `tools/maghemite_openapi_version`). -source.commit = "205b3ccf75b527ac7a565285fdcc0c78f4fcee95" +source.commit = "3877aa0467fe275806f07ff4f7e92efa43e6fa6d" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/maghemite/image//mg-ddm.sha256.txt -source.sha256 = "14c8f3f77149c5aa217f56b57585eb1bcffc507d04910e84fc87c033cbe0ef39" +source.sha256 = "e74b8bd7bc8384dec558a5afafc624c67e9704a2a976f5cb75664f62d969bd97" output.type = "zone" output.intermediate_only = true @@ -685,10 +685,10 @@ source.repo = "maghemite" # `tools/maghemite_openapi_version`. Failing to do so will cause a failure when # building `ddm-admin-client` (which will instruct you to update # `tools/maghemite_openapi_version`). -source.commit = "205b3ccf75b527ac7a565285fdcc0c78f4fcee95" +source.commit = "3877aa0467fe275806f07ff4f7e92efa43e6fa6d" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/maghemite/image//mgd.sha256.txt -source.sha256 = "53576d28cae4304db61367d687580e9b96da23e647898b9034fdfa1603376b0c" +source.sha256 = "c32c66f81c2a6b9149d97c83153327e7d6897c53e047ae425291bd622d8559ec" output.type = "zone" output.intermediate_only = true @@ -736,8 +736,8 @@ only_for_targets.image = "standard" # the other `source.*` keys. source.type = "prebuilt" source.repo = "dendrite" -source.commit = "ab30fa91227fd478bfe0e023310ca83dec0bc22b" -source.sha256 = "a5361a24136fbf4d61ce73063c4e69dca08c4a017cf1aa469c41b62a2e86f280" +source.commit = "21fbf39f63b25567b25aeadf0d9a73a01c842512" +source.sha256 = "5fdf1716837b30cad74815d853339694233bdd57ed660890d6dc3dec5b9faa5d" output.type = "zone" output.intermediate_only = true @@ -763,8 +763,8 @@ only_for_targets.image = "standard" # the other `source.*` keys. source.type = "prebuilt" source.repo = "dendrite" -source.commit = "ab30fa91227fd478bfe0e023310ca83dec0bc22b" -source.sha256 = "cca55b7a4995034e4217bddcf97230e18538089162ef413db5e30466b9e2ef8e" +source.commit = "21fbf39f63b25567b25aeadf0d9a73a01c842512" +source.sha256 = "4c2169695baa344cf031b7445246a32debaa53fc8a0bb8ad0e5ca51d2a6d1acb" output.type = "zone" output.intermediate_only = true @@ -783,8 +783,8 @@ only_for_targets.image = "standard" # the other `source.*` keys. source.type = "prebuilt" source.repo = "dendrite" -source.commit = "ab30fa91227fd478bfe0e023310ca83dec0bc22b" -source.sha256 = "272c5b9e927f6c271afb9b9e54a21d14ed0d7d9d319a17c22187fa86a17197ea" +source.commit = "21fbf39f63b25567b25aeadf0d9a73a01c842512" +source.sha256 = "2ba8711482b4f96ada44ee5b1ac5543a693d5d7b62ae502357d019d95677e55b" output.type = "zone" output.intermediate_only = true diff --git a/tools/dendrite_stub_checksums b/tools/dendrite_stub_checksums index be676840875..d49c44a5ce3 100644 --- a/tools/dendrite_stub_checksums +++ b/tools/dendrite_stub_checksums @@ -1,3 +1,3 @@ -CIDL_SHA256_ILLUMOS="a5361a24136fbf4d61ce73063c4e69dca08c4a017cf1aa469c41b62a2e86f280" -CIDL_SHA256_LINUX_DPD="6c914499455477d0e4f9b30c2c2522363b2b8ee8bc11f9ca0b316a0f1b6bc0f2" -CIDL_SHA256_LINUX_SWADM="a178107fe8dfca66eed4190563d9ae563763d62dfcc1f411f4eca1d4330d9c64" +CIDL_SHA256_ILLUMOS="5fdf1716837b30cad74815d853339694233bdd57ed660890d6dc3dec5b9faa5d" +CIDL_SHA256_LINUX_DPD="b04cd77fb06b88be84d7dd7200af25876cd17227f4eadce92c4d50694d7e78f5" +CIDL_SHA256_LINUX_SWADM="173e694c14d812bb7200f63496dc4f4189ce571a41286707cb106ea33144568c" diff --git a/tools/dendrite_version b/tools/dendrite_version index a7526dabf5f..d5ee005e7ca 100644 --- a/tools/dendrite_version +++ b/tools/dendrite_version @@ -1 +1 @@ -COMMIT="ab30fa91227fd478bfe0e023310ca83dec0bc22b" +COMMIT="21fbf39f63b25567b25aeadf0d9a73a01c842512" diff --git a/tools/maghemite_ddm_openapi_version b/tools/maghemite_ddm_openapi_version index 65569a7a3ab..9e65032fcf7 100644 --- a/tools/maghemite_ddm_openapi_version +++ b/tools/maghemite_ddm_openapi_version @@ -1 +1 @@ -COMMIT="205b3ccf75b527ac7a565285fdcc0c78f4fcee95" +COMMIT="3877aa0467fe275806f07ff4f7e92efa43e6fa6d" diff --git a/tools/maghemite_mg_openapi_version b/tools/maghemite_mg_openapi_version index 65569a7a3ab..9e65032fcf7 100644 --- a/tools/maghemite_mg_openapi_version +++ b/tools/maghemite_mg_openapi_version @@ -1 +1 @@ -COMMIT="205b3ccf75b527ac7a565285fdcc0c78f4fcee95" +COMMIT="3877aa0467fe275806f07ff4f7e92efa43e6fa6d" diff --git a/tools/maghemite_mgd_checksums b/tools/maghemite_mgd_checksums index 12228122372..04be9e0172e 100644 --- a/tools/maghemite_mgd_checksums +++ b/tools/maghemite_mgd_checksums @@ -1,2 +1,2 @@ -CIDL_SHA256="53576d28cae4304db61367d687580e9b96da23e647898b9034fdfa1603376b0c" -MGD_LINUX_SHA256="118a3bac35064cd231dfd123a3d6c7aa308fdf099b4ba045dcdfac6fbb4aae92" \ No newline at end of file +CIDL_SHA256="c32c66f81c2a6b9149d97c83153327e7d6897c53e047ae425291bd622d8559ec" +MGD_LINUX_SHA256="c158e06e62ad58dd23c2ab6009b9b6002185c758ed1d972c7243e915cce447e1" diff --git a/tools/opte_version_override b/tools/opte_version_override index 8d57f7ae9f4..cf3b7e676eb 100644 --- a/tools/opte_version_override +++ b/tools/opte_version_override @@ -2,4 +2,4 @@ # only set this if you want to override the version of opte/xde installed by the # install_opte.sh script -OPTE_COMMIT="" +OPTE_COMMIT="a1ed0960673b6ca2e6b68835537f53cc86110a77" From 409e7b41be43f58a666b9f1eadddc81b46ea0a5b Mon Sep 17 00:00:00 2001 From: Ryan Goodfellow Date: Sat, 17 Jan 2026 18:29:27 +0000 Subject: [PATCH 02/20] pull in bgp work --- Cargo.lock | 30 ++-- Cargo.toml | 8 +- .../tasks/sync_switch_configuration.rs | 94 ++++++++--- nexus/src/app/bgp.rs | 9 +- package-manifest.toml | 12 +- sled-agent/src/bootstrap/early_networking.rs | 149 ++++++++++++------ tools/maghemite_ddm_openapi_version | 2 +- tools/maghemite_mg_openapi_version | 2 +- tools/maghemite_mgd_checksums | 4 +- 9 files changed, 210 insertions(+), 100 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2f033d89997..b4d0557203c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -759,7 +759,7 @@ dependencies = [ "bitflags 2.9.4", "cexpr", "clang-sys", - "itertools 0.10.5", + "itertools 0.12.1", "lazy_static", "lazycell", "log", @@ -782,7 +782,7 @@ dependencies = [ "bitflags 2.9.4", "cexpr", "clang-sys", - "itertools 0.10.5", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -1745,7 +1745,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -2481,7 +2481,7 @@ dependencies = [ [[package]] name = "ddm-admin-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=3877aa0467fe275806f07ff4f7e92efa43e6fa6d#3877aa0467fe275806f07ff4f7e92efa43e6fa6d" +source = "git+https://github.com/oxidecomputer/maghemite?rev=7f78e2b9ab37981e9edcf2e076a3257a032bbb06#7f78e2b9ab37981e9edcf2e076a3257a032bbb06" dependencies = [ "oxnet", "progenitor 0.11.2", @@ -6363,7 +6363,7 @@ dependencies = [ [[package]] name = "mg-admin-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=3877aa0467fe275806f07ff4f7e92efa43e6fa6d#3877aa0467fe275806f07ff4f7e92efa43e6fa6d" +source = "git+https://github.com/oxidecomputer/maghemite?rev=7f78e2b9ab37981e9edcf2e076a3257a032bbb06#7f78e2b9ab37981e9edcf2e076a3257a032bbb06" dependencies = [ "chrono", "colored 3.0.0", @@ -9840,9 +9840,9 @@ dependencies = [ [[package]] name = "oxnet" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8200429754152e6379fbb1dd06eea90156c3b67591f6e31d08e787d010ef0168" +checksum = "5dc6fb07ecd6d2a17ff1431bc5b3ce11036c0b6dd93a3c4904db5b910817b162" dependencies = [ "ipnetwork", "schemars 0.8.22", @@ -11441,7 +11441,7 @@ dependencies = [ [[package]] name = "rdb-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=3877aa0467fe275806f07ff4f7e92efa43e6fa6d#3877aa0467fe275806f07ff4f7e92efa43e6fa6d" +source = "git+https://github.com/oxidecomputer/maghemite?rev=7f78e2b9ab37981e9edcf2e076a3257a032bbb06#7f78e2b9ab37981e9edcf2e076a3257a032bbb06" dependencies = [ "oxnet", "schemars 0.8.22", @@ -12651,15 +12651,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -13538,7 +13538,7 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" dependencies = [ - "heck 0.4.1", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.111", @@ -17182,6 +17182,12 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" +[[package]] +name = "zmij" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" + [[package]] name = "zone" version = "0.1.8" diff --git a/Cargo.toml b/Cargo.toml index 0e58e9dd14f..e57c53cf862 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -566,8 +566,8 @@ ntp-admin-api = { path = "ntp-admin/api" } ntp-admin-client = { path = "clients/ntp-admin-client" } ntp-admin-types = { path = "ntp-admin/types" } ntp-admin-types-versions = { path = "ntp-admin/types/versions" } -mg-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "3877aa0467fe275806f07ff4f7e92efa43e6fa6d" } -ddm-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "3877aa0467fe275806f07ff4f7e92efa43e6fa6d" } +mg-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "7f78e2b9ab37981e9edcf2e076a3257a032bbb06" } +ddm-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "7f78e2b9ab37981e9edcf2e076a3257a032bbb06" } multimap = "0.10.1" nexus-auth = { path = "nexus/auth" } nexus-background-task-interface = { path = "nexus/background-task-interface" } @@ -628,7 +628,7 @@ oxide-client = { path = "clients/oxide-client" } oxide-tokio-rt = "0.1.2" oxide-vpc = { git = "https://github.com/oxidecomputer/opte", rev = "a1ed0960673b6ca2e6b68835537f53cc86110a77", features = [ "api", "std" ] } oxlog = { path = "dev-tools/oxlog" } -oxnet = "0.1.3" +oxnet = "0.1.4" once_cell = "1.21.3" openapi-lint = { git = "https://github.com/oxidecomputer/openapi-lint", branch = "main" } openapiv3 = "2.2.0" @@ -697,7 +697,7 @@ rats-corim = { git = "https://github.com/oxidecomputer/rats-corim.git", rev = "f raw-cpuid = { git = "https://github.com/oxidecomputer/rust-cpuid.git", rev = "a4cf01df76f35430ff5d39dc2fe470bcb953503b" } rayon = "1.10" rcgen = "0.12.1" -rdb-types = { git = "https://github.com/oxidecomputer/maghemite", rev = "3877aa0467fe275806f07ff4f7e92efa43e6fa6d" } +rdb-types = { git = "https://github.com/oxidecomputer/maghemite", rev = "7f78e2b9ab37981e9edcf2e076a3257a032bbb06" } reconfigurator-cli = { path = "dev-tools/reconfigurator-cli" } reedline = "0.40.0" ref-cast = "1.0" diff --git a/nexus/src/app/background/tasks/sync_switch_configuration.rs b/nexus/src/app/background/tasks/sync_switch_configuration.rs index 9a91dd28a7c..03873f86349 100644 --- a/nexus/src/app/background/tasks/sync_switch_configuration.rs +++ b/nexus/src/app/background/tasks/sync_switch_configuration.rs @@ -30,9 +30,10 @@ use futures::future::BoxFuture; use mg_admin_client::types::{ AddStaticRoute4Request, AddStaticRoute6Request, ApplyRequest, BgpPeerConfig, CheckerSource, DeleteStaticRoute4Request, - DeleteStaticRoute6Request, ImportExportPolicy as MgImportExportPolicy, - ShaperSource, StaticRoute4, StaticRoute4List, StaticRoute6, - StaticRoute6List, + DeleteStaticRoute6Request, ImportExportPolicy4 as MgImportExportPolicy4, + ImportExportPolicy6 as MgImportExportPolicy6, Ipv4UnicastConfig, + Ipv6UnicastConfig, JitterRange, ShaperSource, StaticRoute4, + StaticRoute4List, StaticRoute6, StaticRoute6List, }; use nexus_db_queries::{ context::OpContext, @@ -48,7 +49,7 @@ use omicron_common::{ internal::shared::ParseSwitchLocationError, }, }; -use rdb_types::{Prefix as MgPrefix, Prefix4, Prefix6}; +use rdb_types::{Prefix4, Prefix6}; use serde_json::json; use sled_agent_client::types::{ BgpConfig as SledBgpConfig, BgpPeerConfig as SledBgpPeerConfig, @@ -703,30 +704,48 @@ impl BackgroundTask for SwitchPortSettingsManager { } }; - let import_policy = match allow_import { + let import_policy4 = match &allow_import { Some(list) => { - MgImportExportPolicy::Allow(list + MgImportExportPolicy4::Allow(list + .clone() .into_iter() - .map(|x| + .filter_map(|x| match x.prefix { - IpNetwork::V4(p) => MgPrefix::V4( + IpNetwork::V4(p) => Some( Prefix4{ length: p.prefix(), value: p.ip(), } ), - IpNetwork::V6(p) => MgPrefix::V6( + IpNetwork::V6(_) => None, + } + ) + .collect() + ) + } + None => MgImportExportPolicy4::NoFiltering, + }; + + let import_policy6 = match &allow_import { + Some(list) => { + MgImportExportPolicy6::Allow(list + .clone() + .into_iter() + .filter_map(|x| + match x.prefix { + IpNetwork::V6(p) => Some( Prefix6{ length: p.prefix(), value: p.ip(), } - ) + ), + IpNetwork::V4(_) => None, } ) .collect() ) } - None => MgImportExportPolicy::NoFiltering, + None => MgImportExportPolicy6::NoFiltering, }; let allow_export = match self.datastore.allow_export_for_peer( @@ -753,30 +772,48 @@ impl BackgroundTask for SwitchPortSettingsManager { } }; - let export_policy = match allow_export { + let export_policy4 = match &allow_export { Some(list) => { - MgImportExportPolicy::Allow(list + MgImportExportPolicy4::Allow(list + .clone() .into_iter() - .map(|x| + .filter_map(|x| match x.prefix { - IpNetwork::V4(p) => MgPrefix::V4( + IpNetwork::V4(p) => Some( Prefix4{ length: p.prefix(), value: p.ip(), } ), - IpNetwork::V6(p) => MgPrefix::V6( + IpNetwork::V6(_) => None, + } + ) + .collect() + ) + } + None => MgImportExportPolicy4::NoFiltering, + }; + + let export_policy6 = match &allow_export { + Some(list) => { + MgImportExportPolicy6::Allow(list + .clone() + .into_iter() + .filter_map(|x| + match x.prefix { + IpNetwork::V6(p) => Some( Prefix6{ length: p.prefix(), value: p.ip(), } - ) + ), + IpNetwork::V4(_) => None, } ) .collect() ) } - None => MgImportExportPolicy::NoFiltering, + None => MgImportExportPolicy6::NoFiltering, }; // now that the peer passes the above validations, add it to the list for configuration @@ -797,9 +834,24 @@ impl BackgroundTask for SwitchPortSettingsManager { local_pref: peer.local_pref.as_ref().map(|x| x.0), enforce_first_as: peer.enforce_first_as, communities: communities.into_iter().map(|c| c.community.0).collect(), - allow_export: export_policy, - allow_import: import_policy, + ipv4_unicast: Some(Ipv4UnicastConfig{ + nexthop: None, + import_policy: import_policy4, + export_policy: export_policy4, + }), + ipv6_unicast: Some(Ipv6UnicastConfig{ + nexthop: None, + import_policy: import_policy6, + export_policy: export_policy6, + }), vlan_id: peer.vlan_id.map(|x| x.0), + //TODO plumb these out to the external API + connect_retry_jitter: Some(JitterRange { + max: 1.0, + min: 0.75, + }), + deterministic_collision_resolution: false, + idle_hold_jitter: None, }; // update the stored vec if it exists, create a new on if it doesn't exist @@ -874,7 +926,7 @@ impl BackgroundTask for SwitchPortSettingsManager { "switch_location" => ?location, "config" => ?config, ); - if let Err(e) = client.bgp_apply(config).await { + if let Err(e) = client.bgp_apply_v2(config).await { error!(log, "error while applying bgp configuration"; "error" => ?e); } } diff --git a/nexus/src/app/bgp.rs b/nexus/src/app/bgp.rs index 60f23d958d4..0287ba18cf8 100644 --- a/nexus/src/app/bgp.rs +++ b/nexus/src/app/bgp.rs @@ -123,7 +123,7 @@ impl super::Nexus { for r in &router_info { let asn = r.asn; - let peers = match client.get_neighbors_v2(asn).await { + let peers = match client.get_neighbors_v3(asn).await { Ok(result) => result.into_inner(), Err(e) => { error!( @@ -146,8 +146,11 @@ impl super::Nexus { addr: host, local_asn: r.asn, remote_asn: info.asn.unwrap_or(0), - state: info.state.into(), - state_duration_millis: info.duration_millis, + state: info.fsm_state.into(), + state_duration_millis: u64::try_from( + info.fsm_state_duration.as_millis(), + ) + .unwrap_or(u64::MAX), }); } } diff --git a/package-manifest.toml b/package-manifest.toml index 3ed704cbd92..e236e111407 100644 --- a/package-manifest.toml +++ b/package-manifest.toml @@ -654,10 +654,10 @@ source.repo = "maghemite" # `tools/maghemite_openapi_version`. Failing to do so will cause a failure when # building `ddm-admin-client` (which will instruct you to update # `tools/maghemite_openapi_version`). -source.commit = "3877aa0467fe275806f07ff4f7e92efa43e6fa6d" +source.commit = "7f78e2b9ab37981e9edcf2e076a3257a032bbb06" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/maghemite/image//mg-ddm-gz.sha256.txt -source.sha256 = "c317e4bd2958324a6de2b5eba3380cd3b4071617676684213eb6ddbd33087118" +source.sha256 = "c962841f5d3a4c9e2d2dcdc9ec002d1978e70463009d31dfd46d7def80b3ecbb" output.type = "tarball" [package.mg-ddm] @@ -670,10 +670,10 @@ source.repo = "maghemite" # `tools/maghemite_openapi_version`. Failing to do so will cause a failure when # building `ddm-admin-client` (which will instruct you to update # `tools/maghemite_openapi_version`). -source.commit = "3877aa0467fe275806f07ff4f7e92efa43e6fa6d" +source.commit = "7f78e2b9ab37981e9edcf2e076a3257a032bbb06" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/maghemite/image//mg-ddm.sha256.txt -source.sha256 = "e74b8bd7bc8384dec558a5afafc624c67e9704a2a976f5cb75664f62d969bd97" +source.sha256 = "c97b4e329b59d467a5c5af39e5d45298b6ce8dafb93831f37d7aa827a897472d" output.type = "zone" output.intermediate_only = true @@ -685,10 +685,10 @@ source.repo = "maghemite" # `tools/maghemite_openapi_version`. Failing to do so will cause a failure when # building `ddm-admin-client` (which will instruct you to update # `tools/maghemite_openapi_version`). -source.commit = "3877aa0467fe275806f07ff4f7e92efa43e6fa6d" +source.commit = "7f78e2b9ab37981e9edcf2e076a3257a032bbb06" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/maghemite/image//mgd.sha256.txt -source.sha256 = "c32c66f81c2a6b9149d97c83153327e7d6897c53e047ae425291bd622d8559ec" +source.sha256 = "2b0813a1e857ec2e995bc07c6d1de13c8321a20ea768cbab3f01f8e494240d61" output.type = "zone" output.intermediate_only = true diff --git a/sled-agent/src/bootstrap/early_networking.rs b/sled-agent/src/bootstrap/early_networking.rs index 0fbea9555ca..286ccde4fc2 100644 --- a/sled-agent/src/bootstrap/early_networking.rs +++ b/sled-agent/src/bootstrap/early_networking.rs @@ -15,13 +15,18 @@ use http::StatusCode; use internal_dns_resolver::{ResolveError, Resolver as DnsResolver}; use internal_dns_types::names::ServiceName; use mg_admin_client::Client as MgdClient; -use mg_admin_client::types::BfdPeerConfig as MgBfdPeerConfig; -use mg_admin_client::types::BgpPeerConfig as MgBgpPeerConfig; -use mg_admin_client::types::ImportExportPolicy as MgImportExportPolicy; use mg_admin_client::types::{ - AddStaticRoute4Request, ApplyRequest, CheckerSource, ShaperSource, + AddStaticRoute4Request, ApplyRequest, CheckerSource, + ImportExportPolicy4 as MgImportExportPolicy4, + ImportExportPolicy6 as MgImportExportPolicy6, JitterRange, ShaperSource, StaticRoute4, StaticRoute4List, }; +use mg_admin_client::types::{ + BfdPeerConfig as MgBfdPeerConfig, Ipv4UnicastConfig, +}; +use mg_admin_client::types::{ + BgpPeerConfig as MgBgpPeerConfig, Ipv6UnicastConfig, +}; use omicron_common::OMICRON_DPD_TAG; use omicron_common::address::DENDRITE_PORT; use omicron_common::address::{MGD_PORT, MGS_PORT}; @@ -35,7 +40,7 @@ use omicron_common::backoff::{ }; use omicron_ddm_admin_client::DdmError; use oxnet::IpNet; -use rdb_types::{Prefix, Prefix4, Prefix6}; +use rdb_types::{Prefix4, Prefix6}; use slog::Logger; use slog_error_chain::InlineErrorChain; use std::collections::{HashMap, HashSet}; @@ -542,51 +547,95 @@ impl<'a> EarlyNetworkSetup<'a> { communities: peer.communities.clone(), local_pref: peer.local_pref, enforce_first_as: peer.enforce_first_as, - allow_export: match &peer.allowed_export { - ImportExportPolicy::NoFiltering => { - MgImportExportPolicy::NoFiltering - } - ImportExportPolicy::Allow(list) => { - MgImportExportPolicy::Allow( - list.clone() - .iter() - .map(|x| match x { - IpNet::V4(p) => Prefix::V4(Prefix4 { - length: p.width(), - value: p.addr(), - }), - IpNet::V6(p) => Prefix::V6(Prefix6 { - length: p.width(), - value: p.addr(), - }), - }) - .collect(), - ) - } - }, - allow_import: match &peer.allowed_import { - ImportExportPolicy::NoFiltering => { - MgImportExportPolicy::NoFiltering - } - ImportExportPolicy::Allow(list) => { - MgImportExportPolicy::Allow( - list.clone() - .iter() - .map(|x| match x { - IpNet::V4(p) => Prefix::V4(Prefix4 { - length: p.width(), - value: p.addr(), - }), - IpNet::V6(p) => Prefix::V6(Prefix6 { - length: p.width(), - value: p.addr(), - }), - }) - .collect(), - ) - } - }, + ipv4_unicast: Some(Ipv4UnicastConfig { + nexthop: None, + import_policy: match &peer.allowed_import { + ImportExportPolicy::NoFiltering => { + MgImportExportPolicy4::NoFiltering + } + ImportExportPolicy::Allow(list) => { + MgImportExportPolicy4::Allow( + list.clone() + .iter() + .filter_map(|x| match x { + IpNet::V4(p) => Some(Prefix4 { + length: p.width(), + value: p.addr(), + }), + IpNet::V6(_) => None, + }) + .collect(), + ) + } + }, + export_policy: match &peer.allowed_export { + ImportExportPolicy::NoFiltering => { + MgImportExportPolicy4::NoFiltering + } + ImportExportPolicy::Allow(list) => { + MgImportExportPolicy4::Allow( + list.clone() + .iter() + .filter_map(|x| match x { + IpNet::V4(p) => Some(Prefix4 { + length: p.width(), + value: p.addr(), + }), + IpNet::V6(_) => None, + }) + .collect(), + ) + } + }, + }), + ipv6_unicast: Some(Ipv6UnicastConfig { + nexthop: None, + import_policy: match &peer.allowed_import { + ImportExportPolicy::NoFiltering => { + MgImportExportPolicy6::NoFiltering + } + ImportExportPolicy::Allow(list) => { + MgImportExportPolicy6::Allow( + list.clone() + .iter() + .filter_map(|x| match x { + IpNet::V6(p) => Some(Prefix6 { + length: p.width(), + value: p.addr(), + }), + IpNet::V4(_) => None, + }) + .collect(), + ) + } + }, + export_policy: match &peer.allowed_export { + ImportExportPolicy::NoFiltering => { + MgImportExportPolicy6::NoFiltering + } + ImportExportPolicy::Allow(list) => { + MgImportExportPolicy6::Allow( + list.clone() + .iter() + .filter_map(|x| match x { + IpNet::V6(p) => Some(Prefix6 { + length: p.width(), + value: p.addr(), + }), + IpNet::V4(_) => None, + }) + .collect(), + ) + } + }, + }), vlan_id: peer.vlan_id, + connect_retry_jitter: Some(JitterRange { + max: 1.0, + min: 0.75, + }), + deterministic_collision_resolution: false, + idle_hold_jitter: None, }; match bgp_peer_configs.get_mut(&port.port) { Some(peers) => { @@ -619,7 +668,7 @@ impl<'a> EarlyNetworkSetup<'a> { .collect(), }; - if let Err(e) = mgd.bgp_apply(&request).await { + if let Err(e) = mgd.bgp_apply_v2(&request).await { error!( self.log, "BGP peer configuration failed"; diff --git a/tools/maghemite_ddm_openapi_version b/tools/maghemite_ddm_openapi_version index 9e65032fcf7..c66212c3baf 100644 --- a/tools/maghemite_ddm_openapi_version +++ b/tools/maghemite_ddm_openapi_version @@ -1 +1 @@ -COMMIT="3877aa0467fe275806f07ff4f7e92efa43e6fa6d" +COMMIT="7f78e2b9ab37981e9edcf2e076a3257a032bbb06" diff --git a/tools/maghemite_mg_openapi_version b/tools/maghemite_mg_openapi_version index 9e65032fcf7..c66212c3baf 100644 --- a/tools/maghemite_mg_openapi_version +++ b/tools/maghemite_mg_openapi_version @@ -1 +1 @@ -COMMIT="3877aa0467fe275806f07ff4f7e92efa43e6fa6d" +COMMIT="7f78e2b9ab37981e9edcf2e076a3257a032bbb06" diff --git a/tools/maghemite_mgd_checksums b/tools/maghemite_mgd_checksums index 04be9e0172e..431f5300f81 100644 --- a/tools/maghemite_mgd_checksums +++ b/tools/maghemite_mgd_checksums @@ -1,2 +1,2 @@ -CIDL_SHA256="c32c66f81c2a6b9149d97c83153327e7d6897c53e047ae425291bd622d8559ec" -MGD_LINUX_SHA256="c158e06e62ad58dd23c2ab6009b9b6002185c758ed1d972c7243e915cce447e1" +CIDL_SHA256="2b0813a1e857ec2e995bc07c6d1de13c8321a20ea768cbab3f01f8e494240d61" +MGD_LINUX_SHA256="813a2e09247ed0e51c9f53caabb53fbe07301979caf8f84bae47310167657d1a" \ No newline at end of file From a4fb72c18e23c1f4ac43f739842bf0015a77a2e8 Mon Sep 17 00:00:00 2001 From: Ryan Goodfellow Date: Sun, 18 Jan 2026 07:16:26 +0000 Subject: [PATCH 03/20] bgp unnumbered plumbing --- .github/buildomat/jobs/deploy.sh | 2 +- Cargo.lock | 6 +- Cargo.toml | 6 +- common/src/api/external/mod.rs | 6 +- common/src/api/internal/shared/mod.rs | 4 +- dev-tools/ls-apis/tests/api_dependencies.out | 1 + nexus/db-model/src/bgp.rs | 2 +- nexus/db-model/src/schema_versions.rs | 3 +- nexus/db-model/src/switch_port.rs | 6 +- nexus/db-queries/src/db/datastore/bgp.rs | 139 +- .../src/db/datastore/switch_port.rs | 78 +- nexus/db-schema/src/schema.rs | 7 +- nexus/external-api/src/lib.rs | 51 + nexus/external-api/src/v2026010300.rs | 287 + .../tasks/sync_switch_configuration.rs | 478 +- nexus/src/app/rack.rs | 6 +- nexus/src/app/switch_port.rs | 10 +- nexus/tests/integration_tests/switch_port.rs | 153 +- openapi/nexus-lockstep.json | 2 +- .../nexus/nexus-2026011700.0.0-491bb1.json | 29780 ++++++++++++++++ openapi/nexus/nexus-latest.json | 2 +- package-manifest.toml | 12 +- schema/crdb/bgp-unnumbered-peers/up01.sql | 5 + schema/crdb/bgp-unnumbered-peers/up02.sql | 6 + schema/crdb/bgp-unnumbered-peers/up03.sql | 9 + schema/crdb/bgp-unnumbered-peers/up04.sql | 5 + schema/crdb/bgp-unnumbered-peers/up05.sql | 5 + schema/crdb/bgp-unnumbered-peers/up06.sql | 5 + schema/crdb/dbinit.sql | 22 +- sled-agent/src/bootstrap/early_networking.rs | 286 +- tools/maghemite_ddm_openapi_version | 2 +- tools/maghemite_mg_openapi_version | 2 +- tools/maghemite_mgd_checksums | 4 +- 33 files changed, 30952 insertions(+), 440 deletions(-) create mode 100644 nexus/external-api/src/v2026010300.rs create mode 100644 openapi/nexus/nexus-2026011700.0.0-491bb1.json create mode 100644 schema/crdb/bgp-unnumbered-peers/up01.sql create mode 100644 schema/crdb/bgp-unnumbered-peers/up02.sql create mode 100644 schema/crdb/bgp-unnumbered-peers/up03.sql create mode 100644 schema/crdb/bgp-unnumbered-peers/up04.sql create mode 100644 schema/crdb/bgp-unnumbered-peers/up05.sql create mode 100644 schema/crdb/bgp-unnumbered-peers/up06.sql diff --git a/.github/buildomat/jobs/deploy.sh b/.github/buildomat/jobs/deploy.sh index e7f5a0a81e8..77fc9cbce4c 100755 --- a/.github/buildomat/jobs/deploy.sh +++ b/.github/buildomat/jobs/deploy.sh @@ -2,7 +2,7 @@ #: #: name = "helios / deploy" #: variety = "basic" -#: target = "lab-2.0-opte-0.37" +#: target = "lab-2.0-opte-0.38" #: output_rules = [ #: "%/var/svc/log/oxide-*.log*", #: "%/zone/oxz_*/root/var/svc/log/oxide-*.log*", diff --git a/Cargo.lock b/Cargo.lock index b4d0557203c..12a0b0a515a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2481,7 +2481,7 @@ dependencies = [ [[package]] name = "ddm-admin-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=7f78e2b9ab37981e9edcf2e076a3257a032bbb06#7f78e2b9ab37981e9edcf2e076a3257a032bbb06" +source = "git+https://github.com/oxidecomputer/maghemite?rev=f1ea4650fc95a95bbcd1a64d19a45c40e98286f7#f1ea4650fc95a95bbcd1a64d19a45c40e98286f7" dependencies = [ "oxnet", "progenitor 0.11.2", @@ -6363,7 +6363,7 @@ dependencies = [ [[package]] name = "mg-admin-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=7f78e2b9ab37981e9edcf2e076a3257a032bbb06#7f78e2b9ab37981e9edcf2e076a3257a032bbb06" +source = "git+https://github.com/oxidecomputer/maghemite?rev=f1ea4650fc95a95bbcd1a64d19a45c40e98286f7#f1ea4650fc95a95bbcd1a64d19a45c40e98286f7" dependencies = [ "chrono", "colored 3.0.0", @@ -11441,7 +11441,7 @@ dependencies = [ [[package]] name = "rdb-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=7f78e2b9ab37981e9edcf2e076a3257a032bbb06#7f78e2b9ab37981e9edcf2e076a3257a032bbb06" +source = "git+https://github.com/oxidecomputer/maghemite?rev=f1ea4650fc95a95bbcd1a64d19a45c40e98286f7#f1ea4650fc95a95bbcd1a64d19a45c40e98286f7" dependencies = [ "oxnet", "schemars 0.8.22", diff --git a/Cargo.toml b/Cargo.toml index e57c53cf862..cf431a1b9a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -566,8 +566,8 @@ ntp-admin-api = { path = "ntp-admin/api" } ntp-admin-client = { path = "clients/ntp-admin-client" } ntp-admin-types = { path = "ntp-admin/types" } ntp-admin-types-versions = { path = "ntp-admin/types/versions" } -mg-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "7f78e2b9ab37981e9edcf2e076a3257a032bbb06" } -ddm-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "7f78e2b9ab37981e9edcf2e076a3257a032bbb06" } +mg-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "f1ea4650fc95a95bbcd1a64d19a45c40e98286f7" } +ddm-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "f1ea4650fc95a95bbcd1a64d19a45c40e98286f7" } multimap = "0.10.1" nexus-auth = { path = "nexus/auth" } nexus-background-task-interface = { path = "nexus/background-task-interface" } @@ -697,7 +697,7 @@ rats-corim = { git = "https://github.com/oxidecomputer/rats-corim.git", rev = "f raw-cpuid = { git = "https://github.com/oxidecomputer/rust-cpuid.git", rev = "a4cf01df76f35430ff5d39dc2fe470bcb953503b" } rayon = "1.10" rcgen = "0.12.1" -rdb-types = { git = "https://github.com/oxidecomputer/maghemite", rev = "7f78e2b9ab37981e9edcf2e076a3257a032bbb06" } +rdb-types = { git = "https://github.com/oxidecomputer/maghemite", rev = "f1ea4650fc95a95bbcd1a64d19a45c40e98286f7" } reconfigurator-cli = { path = "dev-tools/reconfigurator-cli" } reedline = "0.40.0" ref-cast = "1.0" diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index 85bf5d45e0e..160a7c7e4c3 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -3244,8 +3244,10 @@ pub struct BgpPeer { /// could be vlan47 to refer to a VLAN interface. pub interface_name: Name, - /// The address of the host to peer with. - pub addr: IpAddr, + /// The address of the host to peer with. If not provided, this is an + /// unnumbered BGP session that will be established over the interface + /// specified by `interface_name`. + pub addr: Option, /// How long to hold peer connections between keepalives (seconds). pub hold_time: u32, diff --git a/common/src/api/internal/shared/mod.rs b/common/src/api/internal/shared/mod.rs index 3113f41c0d6..25c5edd48f7 100644 --- a/common/src/api/internal/shared/mod.rs +++ b/common/src/api/internal/shared/mod.rs @@ -91,7 +91,9 @@ pub struct BgpPeerConfig { pub asn: u32, /// Switch port the peer is reachable on. pub port: String, - /// Address of the peer. + /// Address of the peer. Use `Ipv4Addr::UNSPECIFIED` to indicate an + /// unnumbered BGP session established over the interface specified by + /// `port`. pub addr: Ipv4Addr, /// How long to keep a session alive without a keepalive in seconds. /// Defaults to 6. diff --git a/dev-tools/ls-apis/tests/api_dependencies.out b/dev-tools/ls-apis/tests/api_dependencies.out index 94197641c23..d48fe9fcfcf 100644 --- a/dev-tools/ls-apis/tests/api_dependencies.out +++ b/dev-tools/ls-apis/tests/api_dependencies.out @@ -46,6 +46,7 @@ Downstairs Controller (debugging only) (client: dsc-client) Management Gateway Service (client: gateway-client) consumed by: dpd (dendrite/dpd) via 1 path + consumed by: mgd (maghemite/mgd) via 1 path consumed by: omicron-nexus (omicron/nexus) via 4 paths consumed by: omicron-sled-agent (omicron/sled-agent) via 1 path consumed by: wicketd (omicron/wicketd) via 3 paths diff --git a/nexus/db-model/src/bgp.rs b/nexus/db-model/src/bgp.rs index f73745bbcb9..28a79c952ec 100644 --- a/nexus/db-model/src/bgp.rs +++ b/nexus/db-model/src/bgp.rs @@ -128,7 +128,7 @@ impl Into for BgpAnnouncement { pub struct BgpPeerView { pub switch_location: String, pub port_name: String, - pub addr: IpNetwork, + pub addr: Option, pub asn: SqlU32, pub connect_retry: SqlU32, pub delay_open: SqlU32, diff --git a/nexus/db-model/src/schema_versions.rs b/nexus/db-model/src/schema_versions.rs index 293b6086c6b..d0ee47210d1 100644 --- a/nexus/db-model/src/schema_versions.rs +++ b/nexus/db-model/src/schema_versions.rs @@ -16,7 +16,7 @@ use std::{collections::BTreeMap, sync::LazyLock}; /// /// This must be updated when you change the database schema. Refer to /// schema/crdb/README.adoc in the root of this repository for details. -pub const SCHEMA_VERSION: Version = Version::new(217, 0, 0); +pub const SCHEMA_VERSION: Version = Version::new(218, 0, 0); /// List of all past database schema versions, in *reverse* order /// @@ -28,6 +28,7 @@ static KNOWN_VERSIONS: LazyLock> = LazyLock::new(|| { // | leaving the first copy as an example for the next person. // v // KnownVersion::new(next_int, "unique-dirname-with-the-sql-files"), + KnownVersion::new(218, "bgp-unnumbered-peers"), KnownVersion::new(217, "multiple-default-ip-pools-per-silo"), KnownVersion::new(216, "add-trust-quorum"), KnownVersion::new(215, "support-up-to-12-disks"), diff --git a/nexus/db-model/src/switch_port.rs b/nexus/db-model/src/switch_port.rs index 67b77df9376..dbe2983b71d 100644 --- a/nexus/db-model/src/switch_port.rs +++ b/nexus/db-model/src/switch_port.rs @@ -650,10 +650,11 @@ impl Into for SwitchPortRouteConfig { )] #[diesel(table_name = switch_port_settings_bgp_peer_config)] pub struct SwitchPortBgpPeerConfig { + pub id: Uuid, pub port_settings_id: Uuid, pub bgp_config_id: Uuid, pub interface_name: Name, - pub addr: IpNetwork, + pub addr: Option, pub hold_time: SqlU32, pub idle_hold_time: SqlU32, pub delay_open: SqlU32, @@ -740,10 +741,11 @@ impl SwitchPortBgpPeerConfig { p: &BgpPeer, ) -> Self { Self { + id: Uuid::new_v4(), port_settings_id, bgp_config_id, interface_name, - addr: p.addr.into(), + addr: p.addr.map(|a| a.into()), hold_time: p.hold_time.into(), idle_hold_time: p.idle_hold_time.into(), delay_open: p.delay_open.into(), diff --git a/nexus/db-queries/src/db/datastore/bgp.rs b/nexus/db-queries/src/db/datastore/bgp.rs index 19fe1dc1ef4..923322429bc 100644 --- a/nexus/db-queries/src/db/datastore/bgp.rs +++ b/nexus/db-queries/src/db/datastore/bgp.rs @@ -828,19 +828,28 @@ impl DataStore { Ok(results) } + /// Look up communities for a BGP peer. + /// + /// For numbered peers, pass `Some(addr)`. For unnumbered peers, pass `None` + /// (the function will query using the sentinel value 0.0.0.0/32). pub async fn communities_for_peer( &self, opctx: &OpContext, port_settings_id: Uuid, interface_name: &str, - addr: IpNetwork, + addr: Option, ) -> ListResultVec { use nexus_db_schema::schema::switch_port_settings_bgp_peer_config_communities::dsl; + // For unnumbered peers (addr is None), use UNSPECIFIED as sentinel + let db_addr: IpNetwork = addr.unwrap_or_else(|| { + std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED).into() + }); + let results = dsl::switch_port_settings_bgp_peer_config_communities .filter(dsl::port_settings_id.eq(port_settings_id)) .filter(dsl::interface_name.eq(interface_name.to_owned())) - .filter(dsl::addr.eq(addr)) + .filter(dsl::addr.eq(db_addr)) .load_async(&*self.pool_connection_authorized(opctx).await?) .await .map_err(|e| { @@ -852,43 +861,66 @@ impl DataStore { Ok(results) } + /// Look up allowed exports for a BGP peer. + /// + /// For numbered peers, pass `Some(addr)`. For unnumbered peers, pass `None`. pub async fn allow_export_for_peer( &self, opctx: &OpContext, port_settings_id: Uuid, interface_name: &str, - addr: IpNetwork, + addr: Option, ) -> LookupResult>> { use nexus_db_schema::schema::switch_port_settings_bgp_peer_config as db_peer; use nexus_db_schema::schema::switch_port_settings_bgp_peer_config::dsl as peer_dsl; use nexus_db_schema::schema::switch_port_settings_bgp_peer_config_allow_export as db_allow; use nexus_db_schema::schema::switch_port_settings_bgp_peer_config_allow_export::dsl; + // For unnumbered peers (addr is None), use UNSPECIFIED as sentinel + // for the allow_export table (which has non-nullable addr) + let db_addr: IpNetwork = addr.unwrap_or_else(|| { + std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED).into() + }); + let conn = self.pool_connection_authorized(opctx).await?; let err = OptionalError::new(); self.transaction_retry_wrapper("bgp_allow_export_for_peer") .transaction(&conn, |conn| { let err = err.clone(); async move { - let active = peer_dsl::switch_port_settings_bgp_peer_config - .filter(db_peer::port_settings_id.eq(port_settings_id)) - .filter(db_peer::addr.eq(addr)) - .select(db_peer::allow_export_list_active) - .limit(1) - .first_async::(&conn) - .await - .map_err(|e| { - let msg = "failed to lookup export settings for peer"; - error!(opctx.log, "{msg}"; "error" => ?e); + // Query the main peer config table. For unnumbered peers, + // addr is NULL; for numbered peers, addr matches. + let active = if addr.is_some() { + peer_dsl::switch_port_settings_bgp_peer_config + .filter(db_peer::port_settings_id.eq(port_settings_id)) + .filter(db_peer::addr.eq(addr)) + .select(db_peer::allow_export_list_active) + .limit(1) + .first_async::(&conn) + .await + } else { + peer_dsl::switch_port_settings_bgp_peer_config + .filter(db_peer::port_settings_id.eq(port_settings_id)) + .filter(db_peer::addr.is_null()) + .filter(db_peer::interface_name.eq(interface_name.to_owned())) + .select(db_peer::allow_export_list_active) + .limit(1) + .first_async::(&conn) + .await + }; - match e { - diesel::result::Error::NotFound => { - let not_found_msg = format!("peer with {addr} not found for port settings {port_settings_id}"); - err.bail(Error::non_resourcetype_not_found(not_found_msg)) - }, - _ => err.bail(Error::internal_error(msg)), - } - })?; + let active = active.map_err(|e| { + let msg = "failed to lookup export settings for peer"; + error!(opctx.log, "{msg}"; "error" => ?e); + + match e { + diesel::result::Error::NotFound => { + let not_found_msg = format!("peer with {:?} not found for port settings {port_settings_id}", addr); + err.bail(Error::non_resourcetype_not_found(not_found_msg)) + }, + _ => err.bail(Error::internal_error(msg)), + } + })?; if !active { return Ok(None); @@ -903,7 +935,7 @@ impl DataStore { db_allow::interface_name .eq(interface_name.to_owned()), ) - .filter(db_allow::addr.eq(addr)) + .filter(db_allow::addr.eq(db_addr)) .load_async(&conn) .await?; @@ -923,18 +955,27 @@ impl DataStore { }) } + /// Look up allowed imports for a BGP peer. + /// + /// For numbered peers, pass `Some(addr)`. For unnumbered peers, pass `None`. pub async fn allow_import_for_peer( &self, opctx: &OpContext, port_settings_id: Uuid, interface_name: &str, - addr: IpNetwork, + addr: Option, ) -> LookupResult>> { use nexus_db_schema::schema::switch_port_settings_bgp_peer_config as db_peer; use nexus_db_schema::schema::switch_port_settings_bgp_peer_config::dsl as peer_dsl; use nexus_db_schema::schema::switch_port_settings_bgp_peer_config_allow_import as db_allow; use nexus_db_schema::schema::switch_port_settings_bgp_peer_config_allow_import::dsl; + // For unnumbered peers (addr is None), use UNSPECIFIED as sentinel + // for the allow_import table (which has non-nullable addr) + let db_addr: IpNetwork = addr.unwrap_or_else(|| { + std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED).into() + }); + let err = OptionalError::new(); let conn = self.pool_connection_authorized(opctx).await?; self @@ -942,25 +983,39 @@ impl DataStore { .transaction(&conn, |conn| { let err = err.clone(); async move { - let active = peer_dsl::switch_port_settings_bgp_peer_config - .filter(db_peer::port_settings_id.eq(port_settings_id)) - .filter(db_peer::addr.eq(addr)) - .select(db_peer::allow_import_list_active) - .limit(1) - .first_async::(&conn) - .await - .map_err(|e| { - let msg = "failed to lookup import settings for peer"; - error!(opctx.log, "{msg}"; "error" => ?e); + // Query the main peer config table. For unnumbered peers, + // addr is NULL; for numbered peers, addr matches. + let active = if addr.is_some() { + peer_dsl::switch_port_settings_bgp_peer_config + .filter(db_peer::port_settings_id.eq(port_settings_id)) + .filter(db_peer::addr.eq(addr)) + .select(db_peer::allow_import_list_active) + .limit(1) + .first_async::(&conn) + .await + } else { + peer_dsl::switch_port_settings_bgp_peer_config + .filter(db_peer::port_settings_id.eq(port_settings_id)) + .filter(db_peer::addr.is_null()) + .filter(db_peer::interface_name.eq(interface_name.to_owned())) + .select(db_peer::allow_import_list_active) + .limit(1) + .first_async::(&conn) + .await + }; - match e { - diesel::result::Error::NotFound => { - let not_found_msg = format!("peer with {addr} not found for port settings {port_settings_id}"); - err.bail(Error::non_resourcetype_not_found(not_found_msg)) - }, - _ => err.bail(Error::internal_error(msg)), - } - })?; + let active = active.map_err(|e| { + let msg = "failed to lookup import settings for peer"; + error!(opctx.log, "{msg}"; "error" => ?e); + + match e { + diesel::result::Error::NotFound => { + let not_found_msg = format!("peer with {:?} not found for port settings {port_settings_id}", addr); + err.bail(Error::non_resourcetype_not_found(not_found_msg)) + }, + _ => err.bail(Error::internal_error(msg)), + } + })?; if !active { return Ok(None); @@ -975,7 +1030,7 @@ impl DataStore { db_allow::interface_name .eq(interface_name.to_owned()), ) - .filter(db_allow::addr.eq(addr)) + .filter(db_allow::addr.eq(db_addr)) .load_async(&conn) .await?; diff --git a/nexus/db-queries/src/db/datastore/switch_port.rs b/nexus/db-queries/src/db/datastore/switch_port.rs index f1080f87497..e43fa8c4646 100644 --- a/nexus/db-queries/src/db/datastore/switch_port.rs +++ b/nexus/db-queries/src/db/datastore/switch_port.rs @@ -3,7 +3,7 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use std::collections::BTreeMap; -use std::net::IpAddr; +use std::net::{IpAddr, Ipv4Addr}; use super::DataStore; use crate::context::OpContext; @@ -51,7 +51,7 @@ pub struct BgpPeerConfig { pub port_settings_id: Uuid, pub bgp_config_id: Uuid, pub interface_name: Name, - pub addr: IpNetwork, + pub addr: Option, pub hold_time: SqlU32, pub idle_hold_time: SqlU32, pub delay_open: SqlU32, @@ -74,7 +74,7 @@ impl Into for BgpPeerConfig { external::BgpPeer { bgp_config: self.bgp_config_id.into(), interface_name: self.interface_name.into(), - addr: self.addr.ip(), + addr: self.addr.map(|a| a.ip()), hold_time: self.hold_time.into(), idle_hold_time: self.idle_hold_time.into(), delay_open: self.delay_open.into(), @@ -606,12 +606,18 @@ impl DataStore { .await?; for p in peers.iter() { + // For unnumbered peers (addr is None), use the sentinel value + // (UNSPECIFIED address) for lookups since that's how they're stored. + let lookup_addr: IpNetwork = p.addr.unwrap_or_else(|| { + IpNetwork::from(IpAddr::V4(Ipv4Addr::UNSPECIFIED)) + }); + let allowed_import: ImportExportPolicy = if p.allow_import_list_active { let db_list: Vec = allow_import_dsl::switch_port_settings_bgp_peer_config_allow_import .filter(allow_import_dsl::port_settings_id.eq(id)) .filter(allow_import_dsl::interface_name.eq(p.interface_name.clone())) - .filter(allow_import_dsl::addr.eq(p.addr)) + .filter(allow_import_dsl::addr.eq(lookup_addr)) .select(SwitchPortBgpPeerConfigAllowImport::as_select()) .load_async::(&conn) .await?; @@ -630,7 +636,7 @@ impl DataStore { allow_export_dsl::switch_port_settings_bgp_peer_config_allow_export .filter(allow_export_dsl::port_settings_id.eq(id)) .filter(allow_export_dsl::interface_name.eq(p.interface_name.clone())) - .filter(allow_export_dsl::addr.eq(p.addr)) + .filter(allow_export_dsl::addr.eq(lookup_addr)) .select(SwitchPortBgpPeerConfigAllowExport::as_select()) .load_async::(&conn) .await?; @@ -648,7 +654,7 @@ impl DataStore { bgp_communities_dsl::switch_port_settings_bgp_peer_config_communities .filter(bgp_communities_dsl::port_settings_id.eq(id)) .filter(bgp_communities_dsl::interface_name.eq(p.interface_name.clone())) - .filter(bgp_communities_dsl::addr.eq(p.addr)) + .filter(bgp_communities_dsl::addr.eq(lookup_addr)) .select(SwitchPortBgpPeerConfigCommunity::as_select()) .load_async::(&conn) .await?; @@ -1395,7 +1401,11 @@ async fn do_switch_port_settings_create( let mut bgp_peer_config = Vec::new(); for peer_config in ¶ms.bgp_peers { for p in &peer_config.peers { - peer_by_addr.insert(p.addr, &p); + // Track peers for policy lookup. For unnumbered peers (addr is None), + // use UNSPECIFIED as the sentinel value. + let lookup_addr = + p.addr.unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED)); + peer_by_addr.insert(lookup_addr, &p); use nexus_db_schema::schema::bgp_config; let bgp_config_id = match &p.bgp_config { NameOrId::Id(id) => bgp_config::table @@ -1429,6 +1439,12 @@ async fn do_switch_port_settings_create( } }; + // For unnumbered peers (addr is None), use UNSPECIFIED as a sentinel + // value in the communities/import/export tables since addr is part + // of the primary key. + let db_addr: IpNetwork = + p.addr.unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED)).into(); + if let ImportExportPolicy::Allow(list) = &p.allowed_import { let id = port_settings.identity.id; let to_insert: Vec = list @@ -1437,7 +1453,7 @@ async fn do_switch_port_settings_create( .map(|x| SwitchPortBgpPeerConfigAllowImport { port_settings_id: id, interface_name: peer_config.link_name.clone().into(), - addr: p.addr.into(), + addr: db_addr, prefix: x.into(), }) .collect(); @@ -1456,7 +1472,7 @@ async fn do_switch_port_settings_create( .map(|x| SwitchPortBgpPeerConfigAllowExport { port_settings_id: id, interface_name: peer_config.link_name.clone().into(), - addr: p.addr.into(), + addr: db_addr, prefix: x.into(), }) .collect(); @@ -1476,7 +1492,7 @@ async fn do_switch_port_settings_create( .map(|x| SwitchPortBgpPeerConfigCommunity { port_settings_id: id, interface_name: peer_config.link_name.clone().into(), - addr: p.addr.into(), + addr: db_addr, community: x.into(), }) .collect(); @@ -1504,6 +1520,24 @@ async fn do_switch_port_settings_create( .await?; for p in db_bgp_peers.into_iter() { + // Lookup policies and communities for peers. For unnumbered peers (addr + // is None), use UNSPECIFIED as the lookup key. + let lookup_addr = + p.addr.map(|a| a.ip()).unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED)); + let (allowed_import, allowed_export, communities) = ( + peer_by_addr + .get(&lookup_addr) + .map(|x| x.allowed_import.clone()) + .unwrap_or(ImportExportPolicy::NoFiltering), + peer_by_addr + .get(&lookup_addr) + .map(|x| x.allowed_export.clone()) + .unwrap_or(ImportExportPolicy::NoFiltering), + peer_by_addr + .get(&lookup_addr) + .map(|x| x.communities.clone()) + .unwrap_or(Vec::new()), + ); let view = BgpPeerConfig { port_settings_id: p.port_settings_id, bgp_config_id: p.bgp_config_id, @@ -1521,21 +1555,9 @@ async fn do_switch_port_settings_create( local_pref: p.local_pref, enforce_first_as: p.enforce_first_as, vlan_id: p.vlan_id, - allowed_import: peer_by_addr - .get(&p.addr.ip()) - .map(|x| x.allowed_import.clone()) - .unwrap_or(ImportExportPolicy::NoFiltering) - .clone(), - allowed_export: peer_by_addr - .get(&p.addr.ip()) - .map(|x| x.allowed_export.clone()) - .unwrap_or(ImportExportPolicy::NoFiltering) - .clone(), - communities: peer_by_addr - .get(&p.addr.ip()) - .map(|x| x.communities.clone()) - .unwrap_or(Vec::new()) - .clone(), + allowed_import, + allowed_export, + communities, }; result.bgp_peers.push(view); } @@ -1916,7 +1938,9 @@ mod test { "test-bgp-config".parse().unwrap(), ), interface_name: "qsfp0".parse().unwrap(), - addr: "192.168.1.1".parse().unwrap(), + addr: Some( + "192.168.1.1".parse::().unwrap(), + ), hold_time: 0, idle_hold_time: 0, delay_open: 0, @@ -2150,7 +2174,7 @@ mod test { } } - assert_eq!(db_peer.addr.ip(), peer.addr); + assert_eq!(db_peer.addr.map(|a| a.ip()), peer.addr); assert_eq!(*db_peer.hold_time, peer.hold_time); assert_eq!(*db_peer.idle_hold_time, peer.idle_hold_time); assert_eq!(*db_peer.delay_open, peer.delay_open); diff --git a/nexus/db-schema/src/schema.rs b/nexus/db-schema/src/schema.rs index de626e5c64f..9dbe977ea13 100644 --- a/nexus/db-schema/src/schema.rs +++ b/nexus/db-schema/src/schema.rs @@ -226,11 +226,12 @@ table! { } table! { - switch_port_settings_bgp_peer_config (port_settings_id, interface_name, addr) { + switch_port_settings_bgp_peer_config (id) { + id -> Uuid, port_settings_id -> Uuid, bgp_config_id -> Uuid, interface_name -> Text, - addr -> Inet, + addr -> Nullable, hold_time -> Int8, idle_hold_time -> Int8, delay_open -> Int8, @@ -295,7 +296,7 @@ table! { bgp_peer_view (switch_location, port_name) { switch_location -> Text, port_name -> Text, - addr -> Inet, + addr -> Nullable, asn -> Int8, connect_retry -> Int8, delay_open -> Int8, diff --git a/nexus/external-api/src/lib.rs b/nexus/external-api/src/lib.rs index bc2f3ed96c0..6b2df609eef 100644 --- a/nexus/external-api/src/lib.rs +++ b/nexus/external-api/src/lib.rs @@ -39,6 +39,7 @@ mod v2025120300; mod v2025121200; mod v2025122300; mod v2026010100; +mod v2026010300; api_versions!([ // API versions are in the format YYYYMMDDNN.0.0, defined below as @@ -68,6 +69,7 @@ api_versions!([ // | date-based version should be at the top of the list. // v // (next_yyyymmddnn, IDENT), + (2026011700, BGP_UNNUMBERED_PEERS), (2026010300, DUAL_STACK_NICS), (2026010100, SILO_PROJECT_IP_VERSION_AND_POOL_TYPE), (2025122300, IP_VERSION_AND_MULTIPLE_DEFAULT_POOLS), @@ -2417,11 +2419,37 @@ pub trait NexusExternalApi { query_params: Query, ) -> Result>, HttpError>; + /// Create switch port settings (old version with required BgpPeer.addr) + #[endpoint { + method = POST, + path = "/v1/system/networking/switch-port-settings", + tags = ["system/networking"], + versions = ..VERSION_BGP_UNNUMBERED_PEERS, + }] + async fn v2026010300_networking_switch_port_settings_create( + rqctx: RequestContext, + new_settings: TypedBody, + ) -> Result, HttpError> + { + match Self::networking_switch_port_settings_create( + rqctx, + new_settings.map(Into::into), + ) + .await + { + Ok(HttpResponseCreated(result)) => { + Ok(HttpResponseCreated(result.try_into()?)) + } + Err(e) => Err(e), + } + } + /// Create switch port settings #[endpoint { method = POST, path = "/v1/system/networking/switch-port-settings", tags = ["system/networking"], + versions = VERSION_BGP_UNNUMBERED_PEERS.., }] async fn networking_switch_port_settings_create( rqctx: RequestContext, @@ -2455,11 +2483,34 @@ pub trait NexusExternalApi { HttpError, >; + /// Get information about switch port (old version with required BgpPeer.addr) + #[endpoint { + method = GET, + path = "/v1/system/networking/switch-port-settings/{port}", + tags = ["system/networking"], + versions = ..VERSION_BGP_UNNUMBERED_PEERS, + }] + async fn v2026010300_networking_switch_port_settings_view( + rqctx: RequestContext, + path_params: Path, + ) -> Result, HttpError> + { + match Self::networking_switch_port_settings_view(rqctx, path_params) + .await + { + Ok(HttpResponseOk(result)) => { + Ok(HttpResponseOk(result.try_into()?)) + } + Err(e) => Err(e), + } + } + /// Get information about switch port #[endpoint { method = GET, path = "/v1/system/networking/switch-port-settings/{port}", tags = ["system/networking"], + versions = VERSION_BGP_UNNUMBERED_PEERS.., }] async fn networking_switch_port_settings_view( rqctx: RequestContext, diff --git a/nexus/external-api/src/v2026010300.rs b/nexus/external-api/src/v2026010300.rs new file mode 100644 index 00000000000..df14474c5e0 --- /dev/null +++ b/nexus/external-api/src/v2026010300.rs @@ -0,0 +1,287 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Nexus external types that changed from 2026010300 to 2026011700. +//! +//! Version 2026010300 types (before BGP unnumbered peers support was added). +//! +//! Key differences from newer API versions: +//! - [`BgpPeer`] has a required `addr` field. Newer versions make `addr` +//! optional to support BGP unnumbered sessions where the peer address +//! is discovered dynamically via the interface rather than being specified. +//! +//! [`BgpPeer`]: self::BgpPeer + +use omicron_common::api::external::{self, ImportExportPolicy, Name, NameOrId}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::net::IpAddr; + +/// A BGP peer configuration for an interface. Includes the set of announcements +/// that will be advertised to the peer identified by `addr`. The `bgp_config` +/// parameter is a reference to global BGP parameters. The `interface_name` +/// indicates what interface the peer should be contacted on. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +pub struct BgpPeer { + /// The global BGP configuration used for establishing a session with this + /// peer. + pub bgp_config: NameOrId, + + /// The name of interface to peer on. This is relative to the port + /// configuration this BGP peer configuration is a part of. For example this + /// value could be phy0 to refer to a primary physical interface. Or it + /// could be vlan47 to refer to a VLAN interface. + pub interface_name: Name, + + /// The address of the host to peer with. + pub addr: IpAddr, + + /// How long to hold peer connections between keepalives (seconds). + pub hold_time: u32, + + /// How long to hold a peer in idle before attempting a new session + /// (seconds). + pub idle_hold_time: u32, + + /// How long to delay sending an open request after establishing a TCP + /// session (seconds). + pub delay_open: u32, + + /// How long to to wait between TCP connection retries (seconds). + pub connect_retry: u32, + + /// How often to send keepalive requests (seconds). + pub keepalive: u32, + + /// Require that a peer has a specified ASN. + pub remote_asn: Option, + + /// Require messages from a peer have a minimum IP time to live field. + pub min_ttl: Option, + + /// Use the given key for TCP-MD5 authentication with the peer. + pub md5_auth_key: Option, + + /// Apply the provided multi-exit discriminator (MED) updates sent to the peer. + pub multi_exit_discriminator: Option, + + /// Include the provided communities in updates sent to the peer. + pub communities: Vec, + + /// Apply a local preference to routes received from this peer. + pub local_pref: Option, + + /// Enforce that the first AS in paths received from this peer is the peer's AS. + pub enforce_first_as: bool, + + /// Define import policy for a peer. + pub allowed_import: ImportExportPolicy, + + /// Define export policy for a peer. + pub allowed_export: ImportExportPolicy, + + /// Associate a VLAN ID with a peer. + pub vlan_id: Option, +} + +impl From for external::BgpPeer { + fn from(old: BgpPeer) -> external::BgpPeer { + external::BgpPeer { + bgp_config: old.bgp_config, + interface_name: old.interface_name, + addr: Some(old.addr), + hold_time: old.hold_time, + idle_hold_time: old.idle_hold_time, + delay_open: old.delay_open, + connect_retry: old.connect_retry, + keepalive: old.keepalive, + remote_asn: old.remote_asn, + min_ttl: old.min_ttl, + md5_auth_key: old.md5_auth_key, + multi_exit_discriminator: old.multi_exit_discriminator, + communities: old.communities, + local_pref: old.local_pref, + enforce_first_as: old.enforce_first_as, + allowed_import: old.allowed_import, + allowed_export: old.allowed_export, + vlan_id: old.vlan_id, + } + } +} + +impl TryFrom for BgpPeer { + type Error = external::Error; + + fn try_from(new: external::BgpPeer) -> Result { + let addr = new.addr.ok_or_else(|| { + external::Error::invalid_request( + "BGP peer has no address configured, but the API version \ + in use requires an address. Update your client to use \ + BGP unnumbered peers.", + ) + })?; + Ok(BgpPeer { + bgp_config: new.bgp_config, + interface_name: new.interface_name, + addr, + hold_time: new.hold_time, + idle_hold_time: new.idle_hold_time, + delay_open: new.delay_open, + connect_retry: new.connect_retry, + keepalive: new.keepalive, + remote_asn: new.remote_asn, + min_ttl: new.min_ttl, + md5_auth_key: new.md5_auth_key, + multi_exit_discriminator: new.multi_exit_discriminator, + communities: new.communities, + local_pref: new.local_pref, + enforce_first_as: new.enforce_first_as, + allowed_import: new.allowed_import, + allowed_export: new.allowed_export, + vlan_id: new.vlan_id, + }) + } +} + +// --- Params types that contain BgpPeer --- + +use nexus_types::external_api::params; + +/// BGP peer configuration for a link (old version with required addr). +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct BgpPeerConfig { + /// Link that the peer is reachable on. + /// On ports that are not broken out, this is always phy0. + /// On a 2x breakout the options are phy0 and phy1, on 4x + /// phy0-phy3, etc. + pub link_name: Name, + + pub peers: Vec, +} + +impl From for params::BgpPeerConfig { + fn from(old: BgpPeerConfig) -> params::BgpPeerConfig { + params::BgpPeerConfig { + link_name: old.link_name, + peers: old.peers.into_iter().map(Into::into).collect(), + } + } +} + +impl TryFrom for BgpPeerConfig { + type Error = external::Error; + + fn try_from(new: params::BgpPeerConfig) -> Result { + Ok(BgpPeerConfig { + link_name: new.link_name, + peers: new + .peers + .into_iter() + .map(BgpPeer::try_from) + .collect::, _>>()?, + }) + } +} + +/// Parameters for creating switch port settings (old version with required BgpPeer.addr). +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct SwitchPortSettingsCreate { + #[serde(flatten)] + pub identity: external::IdentityMetadataCreateParams, + + pub port_config: params::SwitchPortConfigCreate, + + #[serde(default)] + pub groups: Vec, + + /// Link configurations. + pub links: Vec, + + /// Interface configurations. + #[serde(default)] + pub interfaces: Vec, + + /// Route configurations. + #[serde(default)] + pub routes: Vec, + + /// BGP peer configurations. + #[serde(default)] + pub bgp_peers: Vec, + + /// Address configurations. + pub addresses: Vec, +} + +impl From for params::SwitchPortSettingsCreate { + fn from(old: SwitchPortSettingsCreate) -> params::SwitchPortSettingsCreate { + params::SwitchPortSettingsCreate { + identity: old.identity, + port_config: old.port_config, + groups: old.groups, + links: old.links, + interfaces: old.interfaces, + routes: old.routes, + bgp_peers: old.bgp_peers.into_iter().map(Into::into).collect(), + addresses: old.addresses, + } + } +} + +// --- Response types that contain BgpPeer --- + +/// Switch port settings (old version with required BgpPeer.addr). +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct SwitchPortSettings { + #[serde(flatten)] + pub identity: external::IdentityMetadata, + + /// Switch port settings included from other switch port settings groups. + pub groups: Vec, + + /// Layer 1 physical port settings. + pub port: external::SwitchPortConfig, + + /// Layer 2 link settings. + pub links: Vec, + + /// Layer 3 interface settings. + pub interfaces: Vec, + + /// Vlan interface settings. + pub vlan_interfaces: Vec, + + /// IP route settings. + pub routes: Vec, + + /// BGP peer settings. + pub bgp_peers: Vec, + + /// Layer 3 IP address settings. + pub addresses: Vec, +} + +impl TryFrom for SwitchPortSettings { + type Error = external::Error; + + fn try_from( + new: external::SwitchPortSettings, + ) -> Result { + Ok(SwitchPortSettings { + identity: new.identity, + groups: new.groups, + port: new.port, + links: new.links, + interfaces: new.interfaces, + vlan_interfaces: new.vlan_interfaces, + routes: new.routes, + bgp_peers: new + .bgp_peers + .into_iter() + .map(BgpPeer::try_from) + .collect::, _>>()?, + addresses: new.addresses, + }) + } +} diff --git a/nexus/src/app/background/tasks/sync_switch_configuration.rs b/nexus/src/app/background/tasks/sync_switch_configuration.rs index 03873f86349..e2d76291537 100644 --- a/nexus/src/app/background/tasks/sync_switch_configuration.rs +++ b/nexus/src/app/background/tasks/sync_switch_configuration.rs @@ -33,7 +33,7 @@ use mg_admin_client::types::{ DeleteStaticRoute6Request, ImportExportPolicy4 as MgImportExportPolicy4, ImportExportPolicy6 as MgImportExportPolicy6, Ipv4UnicastConfig, Ipv6UnicastConfig, JitterRange, ShaperSource, StaticRoute4, - StaticRoute4List, StaticRoute6, StaticRoute6List, + StaticRoute4List, StaticRoute6, StaticRoute6List, UnnumberedBgpPeerConfig, }; use nexus_db_queries::{ context::OpContext, @@ -558,6 +558,7 @@ impl BackgroundTask for SwitchPortSettingsManager { // desired peer configurations for a given switch port let mut peers: HashMap> = HashMap::new(); + let mut unnumbered_peers: HashMap> = HashMap::new(); for peer in &settings.bgp_peers { let bgp_config_id = peer.bgp_config_id; @@ -655,213 +656,272 @@ impl BackgroundTask for SwitchPortSettingsManager { let ttl = peer.min_ttl.map(|x| x.0); - //TODO consider awaiting in parallel and joining - let communities = match self.datastore.communities_for_peer( - opctx, - peer.port_settings_id, - &peer.interface_name.to_string(), - peer.addr, - ).await { - Ok(cs) => cs, - Err(e) => { - error!(log, - "failed to get communities for peer"; - "peer" => ?peer, - "error" => %DisplayErrorChain::new(&e) - ); - return json!({ - "error": - format!( - "failed to get port settings for peer {:?}: {}", - peer, - DisplayErrorChain::new(&e) - ) - }); - } - }; + // Determine if this is a numbered or unnumbered peer + // (None or unspecified address = unnumbered) + let is_numbered = peer.addr + .map(|a| !a.ip().is_unspecified()) + .unwrap_or(false); + if is_numbered { + let addr = peer.addr.unwrap(); + // Numbered peer - identified by address + //TODO consider awaiting in parallel and joining + let communities = match self.datastore.communities_for_peer( + opctx, + peer.port_settings_id, + &peer.interface_name.to_string(), + Some(addr), + ).await { + Ok(cs) => cs, + Err(e) => { + error!(log, + "failed to get communities for peer"; + "peer" => ?peer, + "error" => %DisplayErrorChain::new(&e) + ); + return json!({ + "error": + format!( + "failed to get port settings for peer {:?}: {}", + peer, + DisplayErrorChain::new(&e) + ) + }); + } + }; - let allow_import = match self.datastore.allow_import_for_peer( - opctx, - peer.port_settings_id, - &peer.interface_name.to_string(), - peer.addr, - ).await { - Ok(cs) => cs, - Err(e) => { - error!(log, - "failed to get peer allowed imports"; - "peer" => ?peer, - "error" => %DisplayErrorChain::new(&e) - ); - return json!({ - "error": - format!( - "failed to get allowed imports peer {:?}: {}", - peer, - DisplayErrorChain::new(&e) - ) - }); - } - }; + let allow_import = match self.datastore.allow_import_for_peer( + opctx, + peer.port_settings_id, + &peer.interface_name.to_string(), + Some(addr), + ).await { + Ok(cs) => cs, + Err(e) => { + error!(log, + "failed to get peer allowed imports"; + "peer" => ?peer, + "error" => %DisplayErrorChain::new(&e) + ); + return json!({ + "error": + format!( + "failed to get allowed imports peer {:?}: {}", + peer, + DisplayErrorChain::new(&e) + ) + }); + } + }; - let import_policy4 = match &allow_import { - Some(list) => { - MgImportExportPolicy4::Allow(list - .clone() - .into_iter() - .filter_map(|x| - match x.prefix { - IpNetwork::V4(p) => Some( - Prefix4{ - length: p.prefix(), - value: p.ip(), - } - ), - IpNetwork::V6(_) => None, - } + let import_policy4 = match &allow_import { + Some(list) => { + MgImportExportPolicy4::Allow(list + .clone() + .into_iter() + .filter_map(|x| + match x.prefix { + IpNetwork::V4(p) => Some( + Prefix4{ + length: p.prefix(), + value: p.ip(), + } + ), + IpNetwork::V6(_) => None, + } + ) + .collect() ) - .collect() - ) - } - None => MgImportExportPolicy4::NoFiltering, - }; + } + None => MgImportExportPolicy4::NoFiltering, + }; - let import_policy6 = match &allow_import { - Some(list) => { - MgImportExportPolicy6::Allow(list - .clone() - .into_iter() - .filter_map(|x| - match x.prefix { - IpNetwork::V6(p) => Some( - Prefix6{ - length: p.prefix(), - value: p.ip(), - } - ), - IpNetwork::V4(_) => None, - } + let import_policy6 = match &allow_import { + Some(list) => { + MgImportExportPolicy6::Allow(list + .clone() + .into_iter() + .filter_map(|x| + match x.prefix { + IpNetwork::V6(p) => Some( + Prefix6{ + length: p.prefix(), + value: p.ip(), + } + ), + IpNetwork::V4(_) => None, + } + ) + .collect() ) - .collect() - ) - } - None => MgImportExportPolicy6::NoFiltering, - }; + } + None => MgImportExportPolicy6::NoFiltering, + }; - let allow_export = match self.datastore.allow_export_for_peer( - opctx, - peer.port_settings_id, - &peer.interface_name.to_string(), - peer.addr, - ).await { - Ok(cs) => cs, - Err(e) => { - error!(log, - "failed to get peer allowed exportss"; - "peer" => ?peer, - "error" => %DisplayErrorChain::new(&e), - ); - return json!({ - "error": - format!( - "failed to get allowed exports peer {:?}: {}", - peer, - DisplayErrorChain::new(&e) - ) - }); - } - }; + let allow_export = match self.datastore.allow_export_for_peer( + opctx, + peer.port_settings_id, + &peer.interface_name.to_string(), + Some(addr), + ).await { + Ok(cs) => cs, + Err(e) => { + error!(log, + "failed to get peer allowed exportss"; + "peer" => ?peer, + "error" => %DisplayErrorChain::new(&e), + ); + return json!({ + "error": + format!( + "failed to get allowed exports peer {:?}: {}", + peer, + DisplayErrorChain::new(&e) + ) + }); + } + }; - let export_policy4 = match &allow_export { - Some(list) => { - MgImportExportPolicy4::Allow(list - .clone() - .into_iter() - .filter_map(|x| - match x.prefix { - IpNetwork::V4(p) => Some( - Prefix4{ - length: p.prefix(), - value: p.ip(), - } - ), - IpNetwork::V6(_) => None, - } + let export_policy4 = match &allow_export { + Some(list) => { + MgImportExportPolicy4::Allow(list + .clone() + .into_iter() + .filter_map(|x| + match x.prefix { + IpNetwork::V4(p) => Some( + Prefix4{ + length: p.prefix(), + value: p.ip(), + } + ), + IpNetwork::V6(_) => None, + } + ) + .collect() ) - .collect() - ) - } - None => MgImportExportPolicy4::NoFiltering, - }; + } + None => MgImportExportPolicy4::NoFiltering, + }; - let export_policy6 = match &allow_export { - Some(list) => { - MgImportExportPolicy6::Allow(list - .clone() - .into_iter() - .filter_map(|x| - match x.prefix { - IpNetwork::V6(p) => Some( - Prefix6{ - length: p.prefix(), - value: p.ip(), - } - ), - IpNetwork::V4(_) => None, - } + let export_policy6 = match &allow_export { + Some(list) => { + MgImportExportPolicy6::Allow(list + .clone() + .into_iter() + .filter_map(|x| + match x.prefix { + IpNetwork::V6(p) => Some( + Prefix6{ + length: p.prefix(), + value: p.ip(), + } + ), + IpNetwork::V4(_) => None, + } + ) + .collect() ) - .collect() - ) - } - None => MgImportExportPolicy6::NoFiltering, - }; + } + None => MgImportExportPolicy6::NoFiltering, + }; - // now that the peer passes the above validations, add it to the list for configuration - let peer_config = BgpPeerConfig { - name: format!("{}", peer.addr.ip()), - host: format!("{}:179", peer.addr.ip()), - hold_time: peer.hold_time.0.into(), - idle_hold_time: peer.idle_hold_time.0.into(), - delay_open: peer.delay_open.0.into(), - connect_retry: peer.connect_retry.0.into(), - keepalive: peer.keepalive.0.into(), - resolution: BGP_SESSION_RESOLUTION, - passive: false, - remote_asn: peer.remote_asn.as_ref().map(|x| x.0), - min_ttl: ttl, - md5_auth_key: peer.md5_auth_key.clone(), - multi_exit_discriminator: peer.multi_exit_discriminator.as_ref().map(|x| x.0), - local_pref: peer.local_pref.as_ref().map(|x| x.0), - enforce_first_as: peer.enforce_first_as, - communities: communities.into_iter().map(|c| c.community.0).collect(), - ipv4_unicast: Some(Ipv4UnicastConfig{ - nexthop: None, - import_policy: import_policy4, - export_policy: export_policy4, - }), - ipv6_unicast: Some(Ipv6UnicastConfig{ - nexthop: None, - import_policy: import_policy6, - export_policy: export_policy6, - }), - vlan_id: peer.vlan_id.map(|x| x.0), - //TODO plumb these out to the external API - connect_retry_jitter: Some(JitterRange { - max: 1.0, - min: 0.75, - }), - deterministic_collision_resolution: false, - idle_hold_jitter: None, - }; + // now that the peer passes the above validations, add it to the list for configuration + let peer_config = BgpPeerConfig { + name: format!("{}", addr.ip()), + host: format!("{}:179", addr.ip()), + hold_time: peer.hold_time.0.into(), + idle_hold_time: peer.idle_hold_time.0.into(), + delay_open: peer.delay_open.0.into(), + connect_retry: peer.connect_retry.0.into(), + keepalive: peer.keepalive.0.into(), + resolution: BGP_SESSION_RESOLUTION, + passive: false, + remote_asn: peer.remote_asn.as_ref().map(|x| x.0), + min_ttl: ttl, + md5_auth_key: peer.md5_auth_key.clone(), + multi_exit_discriminator: peer.multi_exit_discriminator.as_ref().map(|x| x.0), + local_pref: peer.local_pref.as_ref().map(|x| x.0), + enforce_first_as: peer.enforce_first_as, + communities: communities.into_iter().map(|c| c.community.0).collect(), + ipv4_unicast: Some(Ipv4UnicastConfig{ + nexthop: None, + import_policy: import_policy4, + export_policy: export_policy4, + }), + ipv6_unicast: Some(Ipv6UnicastConfig{ + nexthop: None, + import_policy: import_policy6, + export_policy: export_policy6, + }), + vlan_id: peer.vlan_id.map(|x| x.0), + //TODO plumb these out to the external API + connect_retry_jitter: Some(JitterRange { + max: 1.0, + min: 0.75, + }), + deterministic_collision_resolution: false, + idle_hold_jitter: None, + }; - // update the stored vec if it exists, create a new on if it doesn't exist - match peers.entry(port.port_name.clone().to_string()) { - Entry::Occupied(mut occupied_entry) => { - occupied_entry.get_mut().push(peer_config); - }, - Entry::Vacant(vacant_entry) => { - vacant_entry.insert(vec![peer_config]); - }, + // update the stored vec if it exists, create a new on if it doesn't exist + match peers.entry(port.port_name.clone().to_string()) { + Entry::Occupied(mut occupied_entry) => { + occupied_entry.get_mut().push(peer_config); + }, + Entry::Vacant(vacant_entry) => { + vacant_entry.insert(vec![peer_config]); + }, + } + } else { + // Unnumbered peer - identified by interface + // For unnumbered peers, we use NoFiltering policies as the + // communities/import/export tables are keyed by address + let peer_config = UnnumberedBgpPeerConfig { + name: format!("unnumbered-{}", peer.interface_name), + interface: peer.interface_name.to_string(), + hold_time: peer.hold_time.0.into(), + idle_hold_time: peer.idle_hold_time.0.into(), + delay_open: peer.delay_open.0.into(), + connect_retry: peer.connect_retry.0.into(), + keepalive: peer.keepalive.0.into(), + resolution: BGP_SESSION_RESOLUTION, + passive: false, + remote_asn: peer.remote_asn.as_ref().map(|x| x.0), + min_ttl: ttl, + md5_auth_key: peer.md5_auth_key.clone(), + multi_exit_discriminator: peer.multi_exit_discriminator.as_ref().map(|x| x.0), + local_pref: peer.local_pref.as_ref().map(|x| x.0), + enforce_first_as: peer.enforce_first_as, + communities: Vec::new(), + ipv4_unicast: Some(Ipv4UnicastConfig{ + nexthop: None, + import_policy: MgImportExportPolicy4::NoFiltering, + export_policy: MgImportExportPolicy4::NoFiltering, + }), + ipv6_unicast: Some(Ipv6UnicastConfig{ + nexthop: None, + import_policy: MgImportExportPolicy6::NoFiltering, + export_policy: MgImportExportPolicy6::NoFiltering, + }), + vlan_id: peer.vlan_id.map(|x| x.0), + connect_retry_jitter: Some(JitterRange { + max: 1.0, + min: 0.75, + }), + deterministic_collision_resolution: false, + idle_hold_jitter: None, + router_lifetime: 1800, // Default to 30 minutes + }; + + // update the stored vec if it exists, create a new on if it doesn't exist + match unnumbered_peers.entry(port.port_name.clone().to_string()) { + Entry::Occupied(mut occupied_entry) => { + occupied_entry.get_mut().push(peer_config); + }, + Entry::Vacant(vacant_entry) => { + vacant_entry.insert(vec![peer_config]); + }, + } } } @@ -892,12 +952,14 @@ impl BackgroundTask for SwitchPortSettingsManager { let config = occupied_entry.get_mut(); // peers are the only per-port part of the config. config.peers.extend(peers); + config.unnumbered_peers.extend(unnumbered_peers); } Entry::Vacant(vacant_entry) => { vacant_entry.insert( ApplyRequest { asn: *request_bgp_config.asn, peers, + unnumbered_peers, originate: request_prefixes.clone(), checker: request_bgp_config.checker.as_ref().map(|code| CheckerSource{ asn: *request_bgp_config.asn, @@ -1057,9 +1119,14 @@ impl BackgroundTask for SwitchPortSettingsManager { .unwrap_or(false), bgp_peers: peer_configs.into_iter() // filter maps are cool - .filter_map(|c| match c.addr.ip() { - IpAddr::V4(addr) => Some((c, addr)), - IpAddr::V6(_) => None, + // For unnumbered peers (addr is None), use UNSPECIFIED + // For IPv6 peers, skip (not supported in this context) + .filter_map(|c| match c.addr { + None => Some((c, Ipv4Addr::UNSPECIFIED)), + Some(addr) => match addr.ip() { + IpAddr::V4(v4) => Some((c, v4)), + IpAddr::V6(_) => None, + }, }) .map(|(c, addr)| { SledBgpPeerConfig { @@ -1126,13 +1193,20 @@ impl BackgroundTask for SwitchPortSettingsManager { ; for peer in port_config.bgp_peers.iter_mut() { + // For unnumbered peers (addr is UNSPECIFIED), pass None + let peer_addr_for_lookup = if peer.addr.is_unspecified() { + None + } else { + Some(IpNetwork::from(IpAddr::from(peer.addr))) + }; + peer.communities = match self .datastore .communities_for_peer( opctx, port.port_settings_id.unwrap(), PHY0, //TODO https://github.com/oxidecomputer/omicron/issues/3062 - IpNetwork::from(IpAddr::from(peer.addr)) + peer_addr_for_lookup, ).await { Ok(cs) => cs.iter().map(|c| c.community.0).collect(), Err(e) => { @@ -1150,7 +1224,7 @@ impl BackgroundTask for SwitchPortSettingsManager { opctx, port.port_settings_id.unwrap(), PHY0, //TODO https://github.com/oxidecomputer/omicron/issues/3062 - IpNetwork::from(IpAddr::from(peer.addr)), + peer_addr_for_lookup, ).await { Ok(cs) => cs, Err(e) => { @@ -1174,7 +1248,7 @@ impl BackgroundTask for SwitchPortSettingsManager { opctx, port.port_settings_id.unwrap(), PHY0, //TODO https://github.com/oxidecomputer/omicron/issues/3062 - IpNetwork::from(IpAddr::from(peer.addr)), + peer_addr_for_lookup, ).await { Ok(cs) => cs, Err(e) => { diff --git a/nexus/src/app/rack.rs b/nexus/src/app/rack.rs index 5d4f877392a..4ec340973ea 100644 --- a/nexus/src/app/rack.rs +++ b/nexus/src/app/rack.rs @@ -615,7 +615,11 @@ impl super::Nexus { format!("as{}", r.asn).parse().unwrap(), ), interface_name: link_name.clone(), - addr: r.addr.into(), + addr: if r.addr.is_unspecified() { + None + } else { + Some(std::net::IpAddr::V4(r.addr)) + }, hold_time: r.hold_time() as u32, idle_hold_time: r.idle_hold_time() as u32, delay_open: r.delay_open() as u32, diff --git a/nexus/src/app/switch_port.rs b/nexus/src/app/switch_port.rs index 1d456d19f40..331af66019d 100644 --- a/nexus/src/app/switch_port.rs +++ b/nexus/src/app/switch_port.rs @@ -62,12 +62,18 @@ impl super::Nexus { for x in ¶ms.bgp_peers { for p in x.peers.iter() { if let Some(ref key) = p.md5_auth_key { + let peer_id = match p.addr { + Some(addr) => format!("peer {}", addr), + None => { + format!("unnumbered peer on {}", p.interface_name) + } + }; if key.len() > 80 { return Err(Error::invalid_value( "md5_auth_key", format!( "md5 auth key for {} is longer than 80 characters", - p.addr + peer_id ), )); } @@ -77,7 +83,7 @@ impl super::Nexus { "md5_auth_key", format!( "md5 auth key for {} must be printable ascii", - p.addr + peer_id ), )); } diff --git a/nexus/tests/integration_tests/switch_port.rs b/nexus/tests/integration_tests/switch_port.rs index a27be7099d5..d64be027fc6 100644 --- a/nexus/tests/integration_tests/switch_port.rs +++ b/nexus/tests/integration_tests/switch_port.rs @@ -305,30 +305,59 @@ async fn test_port_settings_basic_crud(ctx: &ControlPlaneTestContext) { .unwrap(); // Update port settings. Should not see conflict. + // Add a numbered BGP peer (with explicit address) settings.bgp_peers.push(BgpPeerConfig { link_name: link0_name.clone(), - peers: vec![BgpPeer { - bgp_config: NameOrId::Name("as47".parse().unwrap()), - interface_name: "phy0".parse().unwrap(), - addr: "1.2.3.4".parse().unwrap(), - hold_time: 6, - idle_hold_time: 6, - delay_open: 0, - connect_retry: 3, - keepalive: 2, - remote_asn: None, - min_ttl: None, - md5_auth_key: None, - multi_exit_discriminator: None, - communities: Vec::new(), - local_pref: None, - enforce_first_as: false, - allowed_export: ImportExportPolicy::NoFiltering, - allowed_import: ImportExportPolicy::NoFiltering, - vlan_id: None, - }], + peers: vec![ + // Numbered peer - identified by address + BgpPeer { + bgp_config: NameOrId::Name("as47".parse().unwrap()), + interface_name: "phy0".parse().unwrap(), + addr: Some("1.2.3.4".parse().unwrap()), + hold_time: 6, + idle_hold_time: 6, + delay_open: 0, + connect_retry: 3, + keepalive: 2, + remote_asn: None, + min_ttl: None, + md5_auth_key: None, + multi_exit_discriminator: None, + communities: Vec::new(), + local_pref: None, + enforce_first_as: false, + allowed_export: ImportExportPolicy::NoFiltering, + allowed_import: ImportExportPolicy::NoFiltering, + vlan_id: None, + }, + // Unnumbered peer - identified by interface only (addr is None) + BgpPeer { + bgp_config: NameOrId::Name("as47".parse().unwrap()), + interface_name: "phy0".parse().unwrap(), + addr: None, + hold_time: 6, + idle_hold_time: 6, + delay_open: 0, + connect_retry: 3, + keepalive: 2, + remote_asn: Some(65000), + min_ttl: None, + md5_auth_key: None, + multi_exit_discriminator: None, + communities: vec![65000], + local_pref: Some(100), + enforce_first_as: false, + allowed_export: ImportExportPolicy::Allow(vec![ + "10.0.0.0/8".parse().unwrap(), + ]), + allowed_import: ImportExportPolicy::Allow(vec![ + "192.168.0.0/16".parse().unwrap(), + ]), + vlan_id: None, + }, + ], }); - let _created: SwitchPortSettings = NexusRequest::objects_post( + let created: SwitchPortSettings = NexusRequest::objects_post( client, "/v1/system/networking/switch-port-settings", &settings, @@ -340,6 +369,88 @@ async fn test_port_settings_basic_crud(ctx: &ControlPlaneTestContext) { .parsed_body() .unwrap(); + // Verify BGP peers were correctly stored + assert_eq!(created.bgp_peers.len(), 2, "Expected 2 BGP peers"); + + // Find the numbered peer (has an address) + let numbered_peer = created + .bgp_peers + .iter() + .find(|p| p.addr.is_some()) + .expect("Should have a numbered peer"); + assert_eq!( + numbered_peer.addr, + Some("1.2.3.4".parse().unwrap()), + "Numbered peer should have addr 1.2.3.4" + ); + + // Find the unnumbered peer (no address) + let unnumbered_peer = created + .bgp_peers + .iter() + .find(|p| p.addr.is_none()) + .expect("Should have an unnumbered peer"); + assert_eq!( + unnumbered_peer.addr, None, + "Unnumbered peer should have no addr" + ); + assert_eq!( + unnumbered_peer.remote_asn, + Some(65000), + "Unnumbered peer should have remote_asn 65000" + ); + assert_eq!( + unnumbered_peer.local_pref, + Some(100), + "Unnumbered peer should have local_pref 100" + ); + assert_eq!( + unnumbered_peer.communities, + vec![65000], + "Unnumbered peer should have community 65000" + ); + assert!( + matches!( + &unnumbered_peer.allowed_export, + ImportExportPolicy::Allow(prefixes) if prefixes.len() == 1 + ), + "Unnumbered peer should have 1 allowed export prefix" + ); + assert!( + matches!( + &unnumbered_peer.allowed_import, + ImportExportPolicy::Allow(prefixes) if prefixes.len() == 1 + ), + "Unnumbered peer should have 1 allowed import prefix" + ); + + // Verify round-trip by fetching the settings again + let roundtrip: SwitchPortSettings = NexusRequest::object_get( + client, + "/v1/system/networking/switch-port-settings/portofino", + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap() + .parsed_body() + .unwrap(); + + assert_eq!( + roundtrip.bgp_peers.len(), + 2, + "Roundtrip should have 2 BGP peers" + ); + + // Verify the unnumbered peer survived the round-trip + let roundtrip_unnumbered = roundtrip + .bgp_peers + .iter() + .find(|p| p.addr.is_none()) + .expect("Roundtrip should have an unnumbered peer"); + assert_eq!(roundtrip_unnumbered.remote_asn, Some(65000)); + assert_eq!(roundtrip_unnumbered.communities, vec![65000]); + // There should be one switch port to begin with, see // Server::start_and_populate in nexus/src/lib.rs diff --git a/openapi/nexus-lockstep.json b/openapi/nexus-lockstep.json index f2a4304865f..87aecb12208 100644 --- a/openapi/nexus-lockstep.json +++ b/openapi/nexus-lockstep.json @@ -1657,7 +1657,7 @@ "type": "object", "properties": { "addr": { - "description": "Address of the peer.", + "description": "Address of the peer. Use `Ipv4Addr::UNSPECIFIED` to indicate an unnumbered BGP session established over the interface specified by `port`.", "type": "string", "format": "ipv4" }, diff --git a/openapi/nexus/nexus-2026011700.0.0-491bb1.json b/openapi/nexus/nexus-2026011700.0.0-491bb1.json new file mode 100644 index 00000000000..cf7a81744c3 --- /dev/null +++ b/openapi/nexus/nexus-2026011700.0.0-491bb1.json @@ -0,0 +1,29780 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Oxide Region API", + "description": "API for interacting with the Oxide control plane", + "contact": { + "url": "https://oxide.computer", + "email": "api@oxide.computer" + }, + "version": "2026011700.0.0" + }, + "paths": { + "/device/auth": { + "post": { + "tags": [ + "console-auth" + ], + "summary": "Start an OAuth 2.0 Device Authorization Grant", + "description": "This endpoint is designed to be accessed from an *unauthenticated* API client. It generates and records a `device_code` and `user_code` which must be verified and confirmed prior to a token being granted.", + "operationId": "device_auth_request", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DeviceAuthRequest" + } + } + }, + "required": true + }, + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + } + }, + "/device/confirm": { + "post": { + "tags": [ + "console-auth" + ], + "summary": "Confirm an OAuth 2.0 Device Authorization Grant", + "description": "This endpoint is designed to be accessed by the user agent (browser), not the client requesting the token. So we do not actually return the token here; it will be returned in response to the poll on `/device/token`.\n\nSome special logic applies when authenticating this request with an existing device token instead of a console session: the requested TTL must not produce an expiration time later than the authenticating token's expiration. If no TTL was specified in the initial grant request, the expiration will be the lesser of the silo max and the authenticating token's expiration time. To get the longest allowed lifetime, omit the TTL and authenticate with a web console session.", + "operationId": "device_auth_confirm", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeviceAuthVerify" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/device/token": { + "post": { + "tags": [ + "console-auth" + ], + "summary": "Request a device access token", + "description": "This endpoint should be polled by the client until the user code is verified and the grant is confirmed.", + "operationId": "device_access_token", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DeviceAccessTokenRequest" + } + } + }, + "required": true + }, + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + } + }, + "/experimental/v1/probes": { + "get": { + "tags": [ + "experimental" + ], + "summary": "List instrumentation probes", + "operationId": "probe_list", + "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" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProbeInfoResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "project" + ] + } + }, + "post": { + "tags": [ + "experimental" + ], + "summary": "Create instrumentation probe", + "operationId": "probe_create", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProbeCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Probe" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/experimental/v1/probes/{probe}": { + "get": { + "tags": [ + "experimental" + ], + "summary": "View instrumentation probe", + "operationId": "probe_view", + "parameters": [ + { + "in": "path", + "name": "probe", + "description": "Name or ID of the probe", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProbeInfo" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "experimental" + ], + "summary": "Delete instrumentation probe", + "operationId": "probe_delete", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "probe", + "description": "Name or ID of the probe", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/experimental/v1/system/support-bundles": { + "get": { + "tags": [ + "experimental" + ], + "summary": "List all support bundles", + "operationId": "support_bundle_list", + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/TimeAndIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupportBundleInfoResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "experimental" + ], + "summary": "Create a new support bundle", + "operationId": "support_bundle_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupportBundleCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupportBundleInfo" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/experimental/v1/system/support-bundles/{bundle_id}": { + "get": { + "tags": [ + "experimental" + ], + "summary": "View a support bundle", + "operationId": "support_bundle_view", + "parameters": [ + { + "in": "path", + "name": "bundle_id", + "description": "ID of the support bundle", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupportBundleInfo" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "experimental" + ], + "summary": "Update a support bundle", + "operationId": "support_bundle_update", + "parameters": [ + { + "in": "path", + "name": "bundle_id", + "description": "ID of the support bundle", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupportBundleUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupportBundleInfo" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "experimental" + ], + "summary": "Delete an existing support bundle", + "description": "May also be used to cancel a support bundle which is currently being collected, or to remove metadata for a support bundle that has failed.", + "operationId": "support_bundle_delete", + "parameters": [ + { + "in": "path", + "name": "bundle_id", + "description": "ID of the support bundle", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/experimental/v1/system/support-bundles/{bundle_id}/download": { + "get": { + "tags": [ + "experimental" + ], + "summary": "Download the contents of a support bundle", + "operationId": "support_bundle_download", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "bundle_id", + "description": "ID of the support bundle", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + }, + "head": { + "tags": [ + "experimental" + ], + "summary": "Download the metadata of a support bundle", + "operationId": "support_bundle_head", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "bundle_id", + "description": "ID of the support bundle", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + } + }, + "/experimental/v1/system/support-bundles/{bundle_id}/download/{file}": { + "get": { + "tags": [ + "experimental" + ], + "summary": "Download a file within a support bundle", + "operationId": "support_bundle_download_file", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "bundle_id", + "description": "ID of the support bundle", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "path", + "name": "file", + "description": "The file within the bundle to download", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + }, + "head": { + "tags": [ + "experimental" + ], + "summary": "Download the metadata of a file within the support bundle", + "operationId": "support_bundle_head_file", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "bundle_id", + "description": "ID of the support bundle", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "path", + "name": "file", + "description": "The file within the bundle to download", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + } + }, + "/experimental/v1/system/support-bundles/{bundle_id}/index": { + "get": { + "tags": [ + "experimental" + ], + "summary": "Download the index of a support bundle", + "operationId": "support_bundle_index", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "bundle_id", + "description": "ID of the support bundle", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + } + }, + "/login/{silo_name}/saml/{provider_name}": { + "post": { + "tags": [ + "login" + ], + "summary": "Authenticate a user via SAML", + "operationId": "login_saml", + "parameters": [ + { + "in": "path", + "name": "provider_name", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + }, + { + "in": "path", + "name": "silo_name", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + }, + "required": true + }, + "responses": { + "303": { + "description": "redirect (see other)", + "headers": { + "location": { + "description": "HTTP \"Location\" header", + "style": "simple", + "required": true, + "schema": { + "type": "string" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/affinity-groups": { + "get": { + "tags": [ + "experimental" + ], + "summary": "List affinity groups", + "operationId": "affinity_group_list", + "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" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AffinityGroupResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "project" + ] + } + }, + "post": { + "tags": [ + "experimental" + ], + "summary": "Create affinity group", + "operationId": "affinity_group_create", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AffinityGroupCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AffinityGroup" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/affinity-groups/{affinity_group}": { + "get": { + "tags": [ + "experimental" + ], + "summary": "Fetch affinity group", + "operationId": "affinity_group_view", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "affinity_group", + "description": "Name or ID of the affinity group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AffinityGroup" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "experimental" + ], + "summary": "Update affinity group", + "operationId": "affinity_group_update", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "affinity_group", + "description": "Name or ID of the affinity group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AffinityGroupUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AffinityGroup" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "experimental" + ], + "summary": "Delete affinity group", + "operationId": "affinity_group_delete", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "affinity_group", + "description": "Name or ID of the affinity group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/affinity-groups/{affinity_group}/members": { + "get": { + "tags": [ + "experimental" + ], + "summary": "List affinity group members", + "operationId": "affinity_group_member_list", + "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" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "path", + "name": "affinity_group", + "description": "Name or ID of the affinity group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AffinityGroupMemberResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/affinity-groups/{affinity_group}/members/instance/{instance}": { + "get": { + "tags": [ + "experimental" + ], + "summary": "Fetch affinity group member", + "operationId": "affinity_group_member_instance_view", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "affinity_group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AffinityGroupMember" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "tags": [ + "experimental" + ], + "summary": "Add member to affinity group", + "operationId": "affinity_group_member_instance_add", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "affinity_group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AffinityGroupMember" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "experimental" + ], + "summary": "Remove member from affinity group", + "operationId": "affinity_group_member_instance_delete", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "affinity_group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/alert-classes": { + "get": { + "tags": [ + "system/alerts" + ], + "summary": "List alert classes", + "operationId": "alert_class_list", + "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" + } + }, + { + "in": "query", + "name": "filter", + "description": "An optional glob pattern for filtering alert class names.\n\nIf provided, only alert classes which match this glob pattern will be included in the response.", + "schema": { + "$ref": "#/components/schemas/AlertSubscription" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AlertClassResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/alert-receivers": { + "get": { + "tags": [ + "system/alerts" + ], + "summary": "List alert receivers", + "operationId": "alert_receiver_list", + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AlertReceiverResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/alert-receivers/{receiver}": { + "get": { + "tags": [ + "system/alerts" + ], + "summary": "Fetch alert receiver", + "operationId": "alert_receiver_view", + "parameters": [ + { + "in": "path", + "name": "receiver", + "description": "The name or ID of the webhook receiver.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AlertReceiver" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "system/alerts" + ], + "summary": "Delete alert receiver", + "operationId": "alert_receiver_delete", + "parameters": [ + { + "in": "path", + "name": "receiver", + "description": "The name or ID of the webhook receiver.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/alert-receivers/{receiver}/deliveries": { + "get": { + "tags": [ + "system/alerts" + ], + "summary": "List delivery attempts to alert receiver", + "description": "Optional query parameters to this endpoint may be used to filter deliveries by state. If none of the `failed`, `pending` or `delivered` query parameters are present, all deliveries are returned. If one or more of these parameters are provided, only those which are set to \"true\" are included in the response.", + "operationId": "alert_delivery_list", + "parameters": [ + { + "in": "path", + "name": "receiver", + "description": "The name or ID of the webhook receiver.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "delivered", + "description": "If true, include deliveries which have succeeded.\n\nIf any of the \"pending\", \"failed\", or \"delivered\" query parameters are set to true, only deliveries matching those state(s) will be included in the response. If NO state filter parameters are set, then all deliveries are included.", + "schema": { + "nullable": true, + "type": "boolean" + } + }, + { + "in": "query", + "name": "failed", + "description": "If true, include deliveries which have failed permanently.\n\nIf any of the \"pending\", \"failed\", or \"delivered\" query parameters are set to true, only deliveries matching those state(s) will be included in the response. If NO state filter parameters are set, then all deliveries are included.\n\nA delivery fails permanently when the retry limit of three total attempts is reached without a successful delivery.", + "schema": { + "nullable": true, + "type": "boolean" + } + }, + { + "in": "query", + "name": "pending", + "description": "If true, include deliveries which are currently in progress.\n\nIf any of the \"pending\", \"failed\", or \"delivered\" query parameters are set to true, only deliveries matching those state(s) will be included in the response. If NO state filter parameters are set, then all deliveries are included.\n\nA delivery is considered \"pending\" if it has not yet been sent at all, or if a delivery attempt has failed but the delivery has retries remaining.", + "schema": { + "nullable": true, + "type": "boolean" + } + }, + { + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/TimeAndIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AlertDeliveryResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/alert-receivers/{receiver}/probe": { + "post": { + "tags": [ + "system/alerts" + ], + "summary": "Send liveness probe to alert receiver", + "description": "This endpoint synchronously sends a liveness probe to the selected alert receiver. The response message describes the outcome of the probe: either the successful response (as appropriate), or indication of why the probe failed.\n\nThe result of the probe is represented as an `AlertDelivery` model. Details relating to the status of the probe depend on the alert delivery mechanism, and are included in the `AlertDeliveryAttempts` model. For example, webhook receiver liveness probes include the HTTP status code returned by the receiver endpoint.\n\nNote that the response status is `200 OK` as long as a probe request was able to be sent to the receiver endpoint. If an HTTP-based receiver, such as a webhook, responds to the another status code, including an error, this will be indicated by the response body, *not* the status of the response.\n\nThe `resend` query parameter can be used to request re-delivery of failed events if the liveness probe succeeds. If it is set to true and the liveness probe succeeds, any alerts for which delivery to this receiver has failed will be queued for re-delivery.", + "operationId": "alert_receiver_probe", + "parameters": [ + { + "in": "path", + "name": "receiver", + "description": "The name or ID of the webhook receiver.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "resend", + "description": "If true, resend all events that have not been delivered successfully if the probe request succeeds.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AlertProbeResult" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/alert-receivers/{receiver}/subscriptions": { + "post": { + "tags": [ + "system/alerts" + ], + "summary": "Add alert receiver subscription", + "operationId": "alert_receiver_subscription_add", + "parameters": [ + { + "in": "path", + "name": "receiver", + "description": "The name or ID of the webhook receiver.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AlertSubscriptionCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AlertSubscriptionCreated" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/alert-receivers/{receiver}/subscriptions/{subscription}": { + "delete": { + "tags": [ + "system/alerts" + ], + "summary": "Remove alert receiver subscription", + "operationId": "alert_receiver_subscription_remove", + "parameters": [ + { + "in": "path", + "name": "receiver", + "description": "The name or ID of the webhook receiver.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "subscription", + "description": "The event class subscription itself.", + "required": true, + "schema": { + "$ref": "#/components/schemas/AlertSubscription" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/alerts/{alert_id}/resend": { + "post": { + "tags": [ + "system/alerts" + ], + "summary": "Request re-delivery of alert", + "operationId": "alert_delivery_resend", + "parameters": [ + { + "in": "path", + "name": "alert_id", + "description": "UUID of the alert", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "receiver", + "description": "The name or ID of the webhook receiver.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AlertDeliveryId" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/anti-affinity-groups": { + "get": { + "tags": [ + "affinity" + ], + "summary": "List anti-affinity groups", + "operationId": "anti_affinity_group_list", + "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" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AntiAffinityGroupResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "project" + ] + } + }, + "post": { + "tags": [ + "affinity" + ], + "summary": "Create anti-affinity group", + "operationId": "anti_affinity_group_create", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AntiAffinityGroupCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AntiAffinityGroup" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/anti-affinity-groups/{anti_affinity_group}": { + "get": { + "tags": [ + "affinity" + ], + "summary": "Fetch anti-affinity group", + "operationId": "anti_affinity_group_view", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "anti_affinity_group", + "description": "Name or ID of the anti affinity group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AntiAffinityGroup" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "affinity" + ], + "summary": "Update anti-affinity group", + "operationId": "anti_affinity_group_update", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "anti_affinity_group", + "description": "Name or ID of the anti affinity group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AntiAffinityGroupUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AntiAffinityGroup" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "affinity" + ], + "summary": "Delete anti-affinity group", + "operationId": "anti_affinity_group_delete", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "anti_affinity_group", + "description": "Name or ID of the anti affinity group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/anti-affinity-groups/{anti_affinity_group}/members": { + "get": { + "tags": [ + "affinity" + ], + "summary": "List anti-affinity group members", + "operationId": "anti_affinity_group_member_list", + "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" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "path", + "name": "anti_affinity_group", + "description": "Name or ID of the anti affinity group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AntiAffinityGroupMemberResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/anti-affinity-groups/{anti_affinity_group}/members/instance/{instance}": { + "get": { + "tags": [ + "affinity" + ], + "summary": "Fetch anti-affinity group member", + "operationId": "anti_affinity_group_member_instance_view", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "anti_affinity_group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AntiAffinityGroupMember" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "tags": [ + "affinity" + ], + "summary": "Add member to anti-affinity group", + "operationId": "anti_affinity_group_member_instance_add", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "anti_affinity_group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AntiAffinityGroupMember" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "affinity" + ], + "summary": "Remove member from anti-affinity group", + "operationId": "anti_affinity_group_member_instance_delete", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "anti_affinity_group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/auth-settings": { + "get": { + "tags": [ + "silos" + ], + "summary": "Fetch current silo's auth settings", + "operationId": "auth_settings_view", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloAuthSettings" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "silos" + ], + "summary": "Update current silo's auth settings", + "operationId": "auth_settings_update", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloAuthSettingsUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloAuthSettings" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/certificates": { + "get": { + "tags": [ + "silos" + ], + "summary": "List certificates for external endpoints", + "description": "Returns a list of TLS certificates used for the external API (for the current Silo). These are sorted by creation date, with the most recent certificates appearing first.", + "operationId": "certificate_list", + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CertificateResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "silos" + ], + "summary": "Create new system-wide x.509 certificate", + "description": "This certificate is automatically used by the Oxide Control plane to serve external connections.", + "operationId": "certificate_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CertificateCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Certificate" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/certificates/{certificate}": { + "get": { + "tags": [ + "silos" + ], + "summary": "Fetch certificate", + "description": "Returns the details of a specific certificate", + "operationId": "certificate_view", + "parameters": [ + { + "in": "path", + "name": "certificate", + "description": "Name or ID of the certificate", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Certificate" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "silos" + ], + "summary": "Delete certificate", + "description": "Permanently delete a certificate. This operation cannot be undone.", + "operationId": "certificate_delete", + "parameters": [ + { + "in": "path", + "name": "certificate", + "description": "Name or ID of the certificate", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/disks": { + "get": { + "tags": [ + "disks" + ], + "summary": "List disks", + "operationId": "disk_list", + "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" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DiskResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "project" + ] + } + }, + "post": { + "tags": [ + "disks" + ], + "summary": "Create a disk", + "operationId": "disk_create", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DiskCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Disk" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/disks/{disk}": { + "get": { + "tags": [ + "disks" + ], + "summary": "Fetch disk", + "operationId": "disk_view", + "parameters": [ + { + "in": "path", + "name": "disk", + "description": "Name or ID of the disk", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Disk" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "disks" + ], + "summary": "Delete disk", + "operationId": "disk_delete", + "parameters": [ + { + "in": "path", + "name": "disk", + "description": "Name or ID of the disk", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/disks/{disk}/bulk-write": { + "post": { + "tags": [ + "disks" + ], + "summary": "Import blocks into disk", + "operationId": "disk_bulk_write_import", + "parameters": [ + { + "in": "path", + "name": "disk", + "description": "Name or ID of the disk", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportBlocksBulkWrite" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/disks/{disk}/bulk-write-start": { + "post": { + "tags": [ + "disks" + ], + "summary": "Start importing blocks into disk", + "description": "Start the process of importing blocks into a disk", + "operationId": "disk_bulk_write_import_start", + "parameters": [ + { + "in": "path", + "name": "disk", + "description": "Name or ID of the disk", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/disks/{disk}/bulk-write-stop": { + "post": { + "tags": [ + "disks" + ], + "summary": "Stop importing blocks into disk", + "description": "Stop the process of importing blocks into a disk", + "operationId": "disk_bulk_write_import_stop", + "parameters": [ + { + "in": "path", + "name": "disk", + "description": "Name or ID of the disk", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/disks/{disk}/finalize": { + "post": { + "tags": [ + "disks" + ], + "summary": "Confirm disk block import completion", + "operationId": "disk_finalize_import", + "parameters": [ + { + "in": "path", + "name": "disk", + "description": "Name or ID of the disk", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FinalizeDisk" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/floating-ips": { + "get": { + "tags": [ + "floating-ips" + ], + "summary": "List floating IPs", + "operationId": "floating_ip_list", + "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" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FloatingIpResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "project" + ] + } + }, + "post": { + "tags": [ + "floating-ips" + ], + "summary": "Create floating IP", + "operationId": "floating_ip_create", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FloatingIpCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FloatingIp" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/floating-ips/{floating_ip}": { + "get": { + "tags": [ + "floating-ips" + ], + "summary": "Fetch floating IP", + "operationId": "floating_ip_view", + "parameters": [ + { + "in": "path", + "name": "floating_ip", + "description": "Name or ID of the floating IP", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FloatingIp" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "floating-ips" + ], + "summary": "Update floating IP", + "operationId": "floating_ip_update", + "parameters": [ + { + "in": "path", + "name": "floating_ip", + "description": "Name or ID of the floating IP", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FloatingIpUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FloatingIp" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "floating-ips" + ], + "summary": "Delete floating IP", + "operationId": "floating_ip_delete", + "parameters": [ + { + "in": "path", + "name": "floating_ip", + "description": "Name or ID of the floating IP", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/floating-ips/{floating_ip}/attach": { + "post": { + "tags": [ + "floating-ips" + ], + "summary": "Attach floating IP", + "description": "Attach floating IP to an instance or other resource.", + "operationId": "floating_ip_attach", + "parameters": [ + { + "in": "path", + "name": "floating_ip", + "description": "Name or ID of the floating IP", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FloatingIpAttach" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FloatingIp" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/floating-ips/{floating_ip}/detach": { + "post": { + "tags": [ + "floating-ips" + ], + "summary": "Detach floating IP", + "operationId": "floating_ip_detach", + "parameters": [ + { + "in": "path", + "name": "floating_ip", + "description": "Name or ID of the floating IP", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FloatingIp" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/groups": { + "get": { + "tags": [ + "silos" + ], + "summary": "List groups", + "operationId": "group_list", + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/groups/{group_id}": { + "get": { + "tags": [ + "silos" + ], + "summary": "Fetch group", + "operationId": "group_view", + "parameters": [ + { + "in": "path", + "name": "group_id", + "description": "ID of the group", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Group" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/images": { + "get": { + "tags": [ + "images" + ], + "summary": "List images", + "description": "List images which are global or scoped to the specified project. The images are returned sorted by creation date, with the most recent images appearing first.", + "operationId": "image_list", + "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" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "images" + ], + "summary": "Create image", + "description": "Create a new image in a project.", + "operationId": "image_create", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Image" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/images/{image}": { + "get": { + "tags": [ + "images" + ], + "summary": "Fetch image", + "description": "Fetch the details for a specific image in a project.", + "operationId": "image_view", + "parameters": [ + { + "in": "path", + "name": "image", + "description": "Name or ID of the image", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Image" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "images" + ], + "summary": "Delete image", + "description": "Permanently delete an image from a project. This operation cannot be undone. Any instances in the project using the image will continue to run, however new instances can not be created with this image.", + "operationId": "image_delete", + "parameters": [ + { + "in": "path", + "name": "image", + "description": "Name or ID of the image", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/images/{image}/demote": { + "post": { + "tags": [ + "images" + ], + "summary": "Demote silo image", + "description": "Demote silo image to be visible only to a specified project", + "operationId": "image_demote", + "parameters": [ + { + "in": "path", + "name": "image", + "description": "Name or ID of the image", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Image" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/images/{image}/promote": { + "post": { + "tags": [ + "images" + ], + "summary": "Promote project image", + "description": "Promote project image to be visible to all projects in the silo", + "operationId": "image_promote", + "parameters": [ + { + "in": "path", + "name": "image", + "description": "Name or ID of the image", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Image" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/instances": { + "get": { + "tags": [ + "instances" + ], + "summary": "List instances", + "operationId": "instance_list", + "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" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "project" + ] + } + }, + "post": { + "tags": [ + "instances" + ], + "summary": "Create instance", + "operationId": "instance_create", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Instance" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/instances/{instance}": { + "get": { + "tags": [ + "instances" + ], + "summary": "Fetch instance", + "operationId": "instance_view", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Instance" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "instances" + ], + "summary": "Update instance", + "operationId": "instance_update", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Instance" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "instances" + ], + "summary": "Delete instance", + "operationId": "instance_delete", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/instances/{instance}/affinity-groups": { + "get": { + "tags": [ + "experimental" + ], + "summary": "List affinity groups containing instance", + "operationId": "instance_affinity_group_list", + "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" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AffinityGroupResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/instances/{instance}/anti-affinity-groups": { + "get": { + "tags": [ + "instances" + ], + "summary": "List anti-affinity groups containing instance", + "operationId": "instance_anti_affinity_group_list", + "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" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AntiAffinityGroupResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/instances/{instance}/disks": { + "get": { + "tags": [ + "instances" + ], + "summary": "List disks for instance", + "operationId": "instance_disk_list", + "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" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DiskResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/instances/{instance}/disks/attach": { + "post": { + "tags": [ + "instances" + ], + "summary": "Attach disk to instance", + "operationId": "instance_disk_attach", + "parameters": [ + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DiskPath" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Disk" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/instances/{instance}/disks/detach": { + "post": { + "tags": [ + "instances" + ], + "summary": "Detach disk from instance", + "operationId": "instance_disk_detach", + "parameters": [ + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DiskPath" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Disk" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/instances/{instance}/external-ips": { + "get": { + "tags": [ + "instances" + ], + "summary": "List external IP addresses", + "operationId": "instance_external_ip_list", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalIpResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/instances/{instance}/external-ips/ephemeral": { + "post": { + "tags": [ + "instances" + ], + "summary": "Allocate and attach ephemeral IP to instance", + "operationId": "instance_ephemeral_ip_attach", + "parameters": [ + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EphemeralIpCreate" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalIp" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "instances" + ], + "summary": "Detach and deallocate ephemeral IP from instance", + "operationId": "instance_ephemeral_ip_detach", + "parameters": [ + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/instances/{instance}/multicast-groups": { + "get": { + "tags": [ + "experimental" + ], + "summary": "List multicast groups for instance", + "operationId": "instance_multicast_group_list", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupMemberResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/instances/{instance}/multicast-groups/{multicast_group}": { + "put": { + "tags": [ + "experimental" + ], + "summary": "Join multicast group.", + "description": "This is functionally equivalent to adding the instance via the group's member management endpoint or updating the instance's `multicast_groups` field. All approaches modify the same membership and trigger reconciliation.", + "operationId": "instance_multicast_group_join", + "parameters": [ + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "multicast_group", + "description": "Name or ID of the multicast group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupMember" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "experimental" + ], + "summary": "Leave multicast group.", + "description": "This is functionally equivalent to removing the instance via the group's member management endpoint or updating the instance's `multicast_groups` field. All approaches modify the same membership and trigger reconciliation.", + "operationId": "instance_multicast_group_leave", + "parameters": [ + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "multicast_group", + "description": "Name or ID of the multicast group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/instances/{instance}/reboot": { + "post": { + "tags": [ + "instances" + ], + "summary": "Reboot an instance", + "operationId": "instance_reboot", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Instance" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/instances/{instance}/serial-console": { + "get": { + "tags": [ + "instances" + ], + "summary": "Fetch instance serial console", + "operationId": "instance_serial_console", + "parameters": [ + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "from_start", + "description": "Character index in the serial buffer from which to read, counting the bytes output since instance start. If this is not provided, `most_recent` must be provided, and if this *is* provided, `most_recent` must *not* be provided.", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + { + "in": "query", + "name": "max_bytes", + "description": "Maximum number of bytes of buffered serial console contents to return. If the requested range runs to the end of the available buffer, the data returned will be shorter than `max_bytes`.", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + { + "in": "query", + "name": "most_recent", + "description": "Character index in the serial buffer from which to read, counting *backward* from the most recently buffered data retrieved from the instance. (See note on `from_start` about mutual exclusivity)", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `instance` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceSerialConsoleData" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/instances/{instance}/serial-console/stream": { + "get": { + "tags": [ + "instances" + ], + "summary": "Stream instance serial console", + "operationId": "instance_serial_console_stream", + "parameters": [ + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "most_recent", + "description": "Character index in the serial buffer from which to read, counting *backward* from the most recently buffered data retrieved from the instance.", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `instance` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + }, + "x-dropshot-websocket": {} + } + }, + "/v1/instances/{instance}/ssh-public-keys": { + "get": { + "tags": [ + "instances" + ], + "summary": "List SSH public keys for instance", + "description": "List SSH public keys injected via cloud-init during instance creation. Note that this list is a snapshot in time and will not reflect updates made after the instance is created.", + "operationId": "instance_ssh_public_key_list", + "parameters": [ + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "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" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SshKeyResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/instances/{instance}/start": { + "post": { + "tags": [ + "instances" + ], + "summary": "Boot instance", + "operationId": "instance_start", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Instance" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/instances/{instance}/stop": { + "post": { + "tags": [ + "instances" + ], + "summary": "Stop instance", + "operationId": "instance_stop", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Instance" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/internet-gateway-ip-addresses": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "List IP addresses attached to internet gateway", + "operationId": "internet_gateway_ip_address_list", + "parameters": [ + { + "in": "query", + "name": "gateway", + "description": "Name or ID of the internet gateway", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "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" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC, only required if `gateway` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternetGatewayIpAddressResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "gateway" + ] + } + }, + "post": { + "tags": [ + "vpcs" + ], + "summary": "Attach IP address to internet gateway", + "operationId": "internet_gateway_ip_address_create", + "parameters": [ + { + "in": "query", + "name": "gateway", + "description": "Name or ID of the internet gateway", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC, only required if `gateway` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternetGatewayIpAddressCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternetGatewayIpAddress" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/internet-gateway-ip-addresses/{address}": { + "delete": { + "tags": [ + "vpcs" + ], + "summary": "Detach IP address from internet gateway", + "operationId": "internet_gateway_ip_address_delete", + "parameters": [ + { + "in": "path", + "name": "address", + "description": "Name or ID of the IP address", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "cascade", + "description": "Also delete routes targeting this gateway element.", + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "gateway", + "description": "Name or ID of the internet gateway", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC, only required if `gateway` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/internet-gateway-ip-pools": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "List IP pools attached to internet gateway", + "operationId": "internet_gateway_ip_pool_list", + "parameters": [ + { + "in": "query", + "name": "gateway", + "description": "Name or ID of the internet gateway", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "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" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC, only required if `gateway` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternetGatewayIpPoolResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "gateway" + ] + } + }, + "post": { + "tags": [ + "vpcs" + ], + "summary": "Attach IP pool to internet gateway", + "operationId": "internet_gateway_ip_pool_create", + "parameters": [ + { + "in": "query", + "name": "gateway", + "description": "Name or ID of the internet gateway", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC, only required if `gateway` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternetGatewayIpPoolCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternetGatewayIpPool" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/internet-gateway-ip-pools/{pool}": { + "delete": { + "tags": [ + "vpcs" + ], + "summary": "Detach IP pool from internet gateway", + "operationId": "internet_gateway_ip_pool_delete", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the IP pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "cascade", + "description": "Also delete routes targeting this gateway element.", + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "gateway", + "description": "Name or ID of the internet gateway", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC, only required if `gateway` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/internet-gateways": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "List internet gateways", + "operationId": "internet_gateway_list", + "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" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternetGatewayResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "vpc" + ] + } + }, + "post": { + "tags": [ + "vpcs" + ], + "summary": "Create VPC internet gateway", + "operationId": "internet_gateway_create", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternetGatewayCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternetGateway" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/internet-gateways/{gateway}": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "Fetch internet gateway", + "operationId": "internet_gateway_view", + "parameters": [ + { + "in": "path", + "name": "gateway", + "description": "Name or ID of the gateway", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternetGateway" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "vpcs" + ], + "summary": "Delete internet gateway", + "operationId": "internet_gateway_delete", + "parameters": [ + { + "in": "path", + "name": "gateway", + "description": "Name or ID of the gateway", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "cascade", + "description": "Also delete routes targeting this gateway.", + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/ip-pools": { + "get": { + "tags": [ + "projects" + ], + "summary": "List IP pools", + "operationId": "project_ip_pool_list", + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloIpPoolResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/ip-pools/{pool}": { + "get": { + "tags": [ + "projects" + ], + "summary": "Fetch IP pool", + "operationId": "project_ip_pool_view", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the IP pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloIpPool" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/login/{silo_name}/local": { + "post": { + "tags": [ + "login" + ], + "summary": "Authenticate a user via username and password", + "operationId": "login_local", + "parameters": [ + { + "in": "path", + "name": "silo_name", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UsernamePasswordCredentials" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/logout": { + "post": { + "tags": [ + "console-auth" + ], + "summary": "Log user out of web console by deleting session on client and server", + "operationId": "logout", + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/me": { + "get": { + "tags": [ + "current-user" + ], + "summary": "Fetch user for current session", + "operationId": "current_user_view", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CurrentUser" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/me/access-tokens": { + "get": { + "tags": [ + "tokens" + ], + "summary": "List access tokens", + "description": "List device access tokens for the currently authenticated user.", + "operationId": "current_user_access_token_list", + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeviceAccessTokenResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/me/access-tokens/{token_id}": { + "delete": { + "tags": [ + "tokens" + ], + "summary": "Delete access token", + "description": "Delete a device access token for the currently authenticated user.", + "operationId": "current_user_access_token_delete", + "parameters": [ + { + "in": "path", + "name": "token_id", + "description": "ID of the token", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/me/groups": { + "get": { + "tags": [ + "current-user" + ], + "summary": "Fetch current user's groups", + "operationId": "current_user_groups", + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/me/ssh-keys": { + "get": { + "tags": [ + "current-user" + ], + "summary": "List SSH public keys", + "description": "Lists SSH public keys for the currently authenticated user.", + "operationId": "current_user_ssh_key_list", + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SshKeyResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "current-user" + ], + "summary": "Create SSH public key", + "description": "Create an SSH public key for the currently authenticated user.", + "operationId": "current_user_ssh_key_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SshKeyCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SshKey" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/me/ssh-keys/{ssh_key}": { + "get": { + "tags": [ + "current-user" + ], + "summary": "Fetch SSH public key", + "description": "Fetch SSH public key associated with the currently authenticated user.", + "operationId": "current_user_ssh_key_view", + "parameters": [ + { + "in": "path", + "name": "ssh_key", + "description": "Name or ID of the SSH key", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SshKey" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "current-user" + ], + "summary": "Delete SSH public key", + "description": "Delete an SSH public key associated with the currently authenticated user.", + "operationId": "current_user_ssh_key_delete", + "parameters": [ + { + "in": "path", + "name": "ssh_key", + "description": "Name or ID of the SSH key", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/metrics/{metric_name}": { + "get": { + "tags": [ + "metrics" + ], + "summary": "View metrics", + "description": "View CPU, memory, or storage utilization metrics at the silo or project level.", + "operationId": "silo_metric", + "parameters": [ + { + "in": "path", + "name": "metric_name", + "required": true, + "schema": { + "$ref": "#/components/schemas/SystemMetricName" + } + }, + { + "in": "query", + "name": "end_time", + "description": "An exclusive end time of metrics.", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "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": "order", + "description": "Query result order", + "schema": { + "$ref": "#/components/schemas/PaginationOrder" + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "start_time", + "description": "An inclusive start time of metrics.", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MeasurementResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "end_time", + "start_time" + ] + } + } + }, + "/v1/multicast-groups": { + "get": { + "tags": [ + "experimental" + ], + "summary": "List all multicast groups.", + "operationId": "multicast_group_list", + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "experimental" + ], + "summary": "Create a multicast group.", + "description": "Multicast groups are fleet-scoped resources that can be joined by instances across projects and silos. A single multicast IP serves all group members regardless of project or silo boundaries.", + "operationId": "multicast_group_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroup" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/multicast-groups/{multicast_group}": { + "get": { + "tags": [ + "experimental" + ], + "summary": "Fetch a multicast group.", + "operationId": "multicast_group_view", + "parameters": [ + { + "in": "path", + "name": "multicast_group", + "description": "Name or ID of the multicast group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroup" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "experimental" + ], + "summary": "Update a multicast group.", + "operationId": "multicast_group_update", + "parameters": [ + { + "in": "path", + "name": "multicast_group", + "description": "Name or ID of the multicast group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroup" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "experimental" + ], + "summary": "Delete a multicast group.", + "operationId": "multicast_group_delete", + "parameters": [ + { + "in": "path", + "name": "multicast_group", + "description": "Name or ID of the multicast group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/multicast-groups/{multicast_group}/members": { + "get": { + "tags": [ + "experimental" + ], + "summary": "List members of a multicast group.", + "operationId": "multicast_group_member_list", + "parameters": [ + { + "in": "path", + "name": "multicast_group", + "description": "Name or ID of the multicast group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupMemberResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "experimental" + ], + "summary": "Add instance to a multicast group.", + "description": "Functionally equivalent to updating the instance's `multicast_groups` field. Both approaches modify the same underlying membership and trigger the same reconciliation logic.\n\nSpecify instance by name (requires `?project=`) or UUID.", + "operationId": "multicast_group_member_add", + "parameters": [ + { + "in": "path", + "name": "multicast_group", + "description": "Name or ID of the multicast group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupMemberAdd" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupMember" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/multicast-groups/{multicast_group}/members/{instance}": { + "delete": { + "tags": [ + "experimental" + ], + "summary": "Remove instance from a multicast group.", + "description": "Functionally equivalent to removing the group from the instance's `multicast_groups` field. Both approaches modify the same underlying membership and trigger reconciliation.\n\nSpecify instance by name (requires `?project=`) or UUID.", + "operationId": "multicast_group_member_remove", + "parameters": [ + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "multicast_group", + "description": "Name or ID of the multicast group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/network-interfaces": { + "get": { + "tags": [ + "instances" + ], + "summary": "List network interfaces", + "operationId": "instance_network_interface_list", + "parameters": [ + { + "in": "query", + "name": "instance", + "description": "Name or ID of the instance", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "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" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `instance` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceNetworkInterfaceResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "instance" + ] + } + }, + "post": { + "tags": [ + "instances" + ], + "summary": "Create network interface", + "operationId": "instance_network_interface_create", + "parameters": [ + { + "in": "query", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `instance` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceNetworkInterfaceCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceNetworkInterface" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/network-interfaces/{interface}": { + "get": { + "tags": [ + "instances" + ], + "summary": "Fetch network interface", + "operationId": "instance_network_interface_view", + "parameters": [ + { + "in": "path", + "name": "interface", + "description": "Name or ID of the network interface", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "instance", + "description": "Name or ID of the instance", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `instance` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceNetworkInterface" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "instances" + ], + "summary": "Update network interface", + "operationId": "instance_network_interface_update", + "parameters": [ + { + "in": "path", + "name": "interface", + "description": "Name or ID of the network interface", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "instance", + "description": "Name or ID of the instance", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `instance` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceNetworkInterfaceUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceNetworkInterface" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "instances" + ], + "summary": "Delete network interface", + "description": "Note that the primary interface for an instance cannot be deleted if there are any secondary interfaces. A new primary interface must be designated first. The primary interface can be deleted if there are no secondary interfaces.", + "operationId": "instance_network_interface_delete", + "parameters": [ + { + "in": "path", + "name": "interface", + "description": "Name or ID of the network interface", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "instance", + "description": "Name or ID of the instance", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `instance` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/ping": { + "get": { + "tags": [ + "system/status" + ], + "summary": "Ping API", + "description": "Always responds with Ok if it responds at all.", + "operationId": "ping", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ping" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/policy": { + "get": { + "tags": [ + "silos" + ], + "summary": "Fetch current silo's IAM policy", + "operationId": "policy_view", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "silos" + ], + "summary": "Update current silo's IAM policy", + "operationId": "policy_update", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloRolePolicy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/projects": { + "get": { + "tags": [ + "projects" + ], + "summary": "List projects", + "operationId": "project_list", + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "projects" + ], + "summary": "Create project", + "operationId": "project_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/projects/{project}": { + "get": { + "tags": [ + "projects" + ], + "summary": "Fetch project", + "operationId": "project_view", + "parameters": [ + { + "in": "path", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "projects" + ], + "summary": "Update a project", + "operationId": "project_update", + "parameters": [ + { + "in": "path", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "projects" + ], + "summary": "Delete project", + "operationId": "project_delete", + "parameters": [ + { + "in": "path", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/projects/{project}/policy": { + "get": { + "tags": [ + "projects" + ], + "summary": "Fetch project's IAM policy", + "operationId": "project_policy_view", + "parameters": [ + { + "in": "path", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "projects" + ], + "summary": "Update project's IAM policy", + "operationId": "project_policy_update", + "parameters": [ + { + "in": "path", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectRolePolicy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/snapshots": { + "get": { + "tags": [ + "snapshots" + ], + "summary": "List snapshots", + "operationId": "snapshot_list", + "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" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SnapshotResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "project" + ] + } + }, + "post": { + "tags": [ + "snapshots" + ], + "summary": "Create snapshot", + "description": "Creates a point-in-time snapshot from a disk.", + "operationId": "snapshot_create", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SnapshotCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Snapshot" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/snapshots/{snapshot}": { + "get": { + "tags": [ + "snapshots" + ], + "summary": "Fetch snapshot", + "operationId": "snapshot_view", + "parameters": [ + { + "in": "path", + "name": "snapshot", + "description": "Name or ID of the snapshot", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Snapshot" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "snapshots" + ], + "summary": "Delete snapshot", + "operationId": "snapshot_delete", + "parameters": [ + { + "in": "path", + "name": "snapshot", + "description": "Name or ID of the snapshot", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/audit-log": { + "get": { + "tags": [ + "system/audit-log" + ], + "summary": "View audit log", + "description": "A single item in the audit log represents both the beginning and end of the logged operation (represented by `time_started` and `time_completed`) so that clients do not have to find multiple entries and match them up by request ID to get the full picture of an operation. Because timestamps may not be unique, entries have also have a unique `id` that can be used to deduplicate items fetched from overlapping time intervals.\n\nAudit log entries are designed to be immutable: once you see an entry, fetching it again will never get you a different result. The list is ordered by `time_completed`, not `time_started`. If you fetch the audit log for a time range that is fully in the past, the resulting list is guaranteed to be complete, i.e., fetching the same timespan again later will always produce the same set of entries.", + "operationId": "audit_log_list", + "parameters": [ + { + "in": "query", + "name": "end_time", + "description": "Exclusive", + "schema": { + "nullable": true, + "type": "string", + "format": "date-time" + } + }, + { + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/TimeAndIdSortMode" + } + }, + { + "in": "query", + "name": "start_time", + "description": "Required, inclusive", + "schema": { + "type": "string", + "format": "date-time" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuditLogEntryResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "start_time" + ] + } + } + }, + "/v1/system/hardware/disks": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "List physical disks", + "operationId": "physical_disk_list", + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PhysicalDiskResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/hardware/disks/{disk_id}": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "Get a physical disk", + "operationId": "physical_disk_view", + "parameters": [ + { + "in": "path", + "name": "disk_id", + "description": "ID of the physical disk", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PhysicalDisk" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/hardware/rack-switch-port/{rack_id}/{switch_location}/{port}/lldp/neighbors": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Fetch the LLDP neighbors seen on a switch port", + "operationId": "networking_switch_port_lldp_neighbors", + "parameters": [ + { + "in": "path", + "name": "port", + "description": "A name to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + }, + { + "in": "path", + "name": "rack_id", + "description": "A rack id to use when selecting switch ports.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "path", + "name": "switch_location", + "description": "A switch location to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + }, + { + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LldpNeighborResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/hardware/racks": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "List racks", + "operationId": "rack_list", + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RackResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/hardware/racks/{rack_id}": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "Fetch rack", + "operationId": "rack_view", + "parameters": [ + { + "in": "path", + "name": "rack_id", + "description": "ID of the rack", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Rack" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/hardware/sleds": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "List sleds", + "operationId": "sled_list", + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "system/hardware" + ], + "summary": "Add sled to initialized rack", + "operationId": "sled_add", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UninitializedSledId" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledId" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/hardware/sleds/{sled_id}": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "Fetch sled", + "operationId": "sled_view", + "parameters": [ + { + "in": "path", + "name": "sled_id", + "description": "ID of the sled", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Sled" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/hardware/sleds/{sled_id}/disks": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "List physical disks attached to sleds", + "operationId": "sled_physical_disk_list", + "parameters": [ + { + "in": "path", + "name": "sled_id", + "description": "ID of the sled", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PhysicalDiskResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/hardware/sleds/{sled_id}/instances": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "List instances running on given sled", + "operationId": "sled_instance_list", + "parameters": [ + { + "in": "path", + "name": "sled_id", + "description": "ID of the sled", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledInstanceResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/hardware/sleds/{sled_id}/provision-policy": { + "put": { + "tags": [ + "system/hardware" + ], + "summary": "Set sled provision policy", + "operationId": "sled_set_provision_policy", + "parameters": [ + { + "in": "path", + "name": "sled_id", + "description": "ID of the sled", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledProvisionPolicyParams" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledProvisionPolicyResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/hardware/sleds-uninitialized": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "List uninitialized sleds", + "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", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UninitializedSledResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/hardware/switch-port": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "List switch ports", + "operationId": "networking_switch_port_list", + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + }, + { + "in": "query", + "name": "switch_port_id", + "description": "An optional switch port id to use when listing switch ports.", + "schema": { + "nullable": true, + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchPortResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/hardware/switch-port/{port}/lldp/config": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Fetch the LLDP configuration for a switch port", + "operationId": "networking_switch_port_lldp_config_view", + "parameters": [ + { + "in": "path", + "name": "port", + "description": "A name to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + }, + { + "in": "query", + "name": "rack_id", + "description": "A rack id to use when selecting switch ports.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "switch_location", + "description": "A switch location to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LldpLinkConfig" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "tags": [ + "system/networking" + ], + "summary": "Update the LLDP configuration for a switch port", + "operationId": "networking_switch_port_lldp_config_update", + "parameters": [ + { + "in": "path", + "name": "port", + "description": "A name to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + }, + { + "in": "query", + "name": "rack_id", + "description": "A rack id to use when selecting switch ports.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "switch_location", + "description": "A switch location to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LldpLinkConfig" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/hardware/switch-port/{port}/settings": { + "post": { + "tags": [ + "system/hardware" + ], + "summary": "Apply switch port settings", + "operationId": "networking_switch_port_apply_settings", + "parameters": [ + { + "in": "path", + "name": "port", + "description": "A name to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + }, + { + "in": "query", + "name": "rack_id", + "description": "A rack id to use when selecting switch ports.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "switch_location", + "description": "A switch location to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchPortApplySettings" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "system/hardware" + ], + "summary": "Clear switch port settings", + "operationId": "networking_switch_port_clear_settings", + "parameters": [ + { + "in": "path", + "name": "port", + "description": "A name to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + }, + { + "in": "query", + "name": "rack_id", + "description": "A rack id to use when selecting switch ports.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "switch_location", + "description": "A switch location to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/hardware/switch-port/{port}/status": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "Get switch port status", + "operationId": "networking_switch_port_status", + "parameters": [ + { + "in": "path", + "name": "port", + "description": "A name to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + }, + { + "in": "query", + "name": "rack_id", + "description": "A rack id to use when selecting switch ports.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "switch_location", + "description": "A switch location to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchLinkState" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/hardware/switches": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "List switches", + "operationId": "switch_list", + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/hardware/switches/{switch_id}": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "Fetch switch", + "operationId": "switch_view", + "parameters": [ + { + "in": "path", + "name": "switch_id", + "description": "ID of the switch", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Switch" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/identity-providers": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "List identity providers for silo", + "description": "List identity providers for silo by silo name or ID.", + "operationId": "silo_identity_provider_list", + "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" + } + }, + { + "in": "query", + "name": "silo", + "description": "Name or ID of the silo", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IdentityProviderResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "silo" + ] + } + } + }, + "/v1/system/identity-providers/local/users": { + "post": { + "tags": [ + "system/silos" + ], + "summary": "Create user", + "description": "Users can only be created in Silos with `provision_type` == `Fixed`. Otherwise, Silo users are just-in-time (JIT) provisioned when a user first logs in using an external Identity Provider.", + "operationId": "local_idp_user_create", + "parameters": [ + { + "in": "query", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/identity-providers/local/users/{user_id}": { + "delete": { + "tags": [ + "system/silos" + ], + "summary": "Delete user", + "operationId": "local_idp_user_delete", + "parameters": [ + { + "in": "path", + "name": "user_id", + "description": "The user's internal ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/identity-providers/local/users/{user_id}/set-password": { + "post": { + "tags": [ + "system/silos" + ], + "summary": "Set or invalidate user's password", + "description": "Passwords can only be updated for users in Silos with identity mode `LocalOnly`.", + "operationId": "local_idp_user_set_password", + "parameters": [ + { + "in": "path", + "name": "user_id", + "description": "The user's internal ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPassword" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/identity-providers/saml": { + "post": { + "tags": [ + "system/silos" + ], + "summary": "Create SAML identity provider", + "operationId": "saml_identity_provider_create", + "parameters": [ + { + "in": "query", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SamlIdentityProviderCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SamlIdentityProvider" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/identity-providers/saml/{provider}": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "Fetch SAML identity provider", + "operationId": "saml_identity_provider_view", + "parameters": [ + { + "in": "path", + "name": "provider", + "description": "Name or ID of the SAML identity provider", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "silo", + "description": "Name or ID of the silo", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SamlIdentityProvider" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/ip-pools": { + "get": { + "tags": [ + "system/ip-pools" + ], + "summary": "List IP pools", + "operationId": "ip_pool_list", + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "system/ip-pools" + ], + "summary": "Create IP pool", + "description": "IPv6 is not yet supported for unicast pools.", + "operationId": "ip_pool_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPool" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/ip-pools/{pool}": { + "get": { + "tags": [ + "system/ip-pools" + ], + "summary": "Fetch IP pool", + "operationId": "ip_pool_view", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the IP pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPool" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "system/ip-pools" + ], + "summary": "Update IP pool", + "operationId": "ip_pool_update", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the IP pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPool" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "system/ip-pools" + ], + "summary": "Delete IP pool", + "operationId": "ip_pool_delete", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the IP pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/ip-pools/{pool}/ranges": { + "get": { + "tags": [ + "system/ip-pools" + ], + "summary": "List ranges for IP pool", + "description": "Ranges are ordered by their first address.", + "operationId": "ip_pool_range_list", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the IP pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "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", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolRangeResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/ip-pools/{pool}/ranges/add": { + "post": { + "tags": [ + "system/ip-pools" + ], + "summary": "Add range to IP pool.", + "description": "IPv6 ranges are not allowed yet for unicast pools.\n\nFor multicast pools, all ranges must be either Any-Source Multicast (ASM) or Source-Specific Multicast (SSM), but not both. Mixing ASM and SSM ranges in the same pool is not allowed.\n\nASM: IPv4 addresses outside 232.0.0.0/8, IPv6 addresses with flag field != 3 SSM: IPv4 addresses in 232.0.0.0/8, IPv6 addresses with flag field = 3", + "operationId": "ip_pool_range_add", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the IP pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpRange" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolRange" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/ip-pools/{pool}/ranges/remove": { + "post": { + "tags": [ + "system/ip-pools" + ], + "summary": "Remove range from IP pool", + "operationId": "ip_pool_range_remove", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the IP pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpRange" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/ip-pools/{pool}/silos": { + "get": { + "tags": [ + "system/ip-pools" + ], + "summary": "List IP pool's linked silos", + "operationId": "ip_pool_silo_list", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the IP pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolSiloLinkResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "system/ip-pools" + ], + "summary": "Link IP pool to silo", + "description": "Users in linked silos can allocate external IPs from this pool for their instances. A silo can have at most one default pool. IPs are allocated from the default pool when users ask for one without specifying a pool.", + "operationId": "ip_pool_silo_link", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the IP pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolLinkSilo" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolSiloLink" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/ip-pools/{pool}/silos/{silo}": { + "put": { + "tags": [ + "system/ip-pools" + ], + "summary": "Make IP pool default for silo", + "description": "When a user asks for an IP (e.g., at instance create time) without specifying a pool, the IP comes from the default pool if a default is configured. When a pool is made the default for a silo, any existing default will remain linked to the silo, but will no longer be the default.", + "operationId": "ip_pool_silo_update", + "parameters": [ + { + "in": "path", + "name": "pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolSiloUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolSiloLink" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "system/ip-pools" + ], + "summary": "Unlink IP pool from silo", + "description": "Will fail if there are any outstanding IPs allocated in the silo.", + "operationId": "ip_pool_silo_unlink", + "parameters": [ + { + "in": "path", + "name": "pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/ip-pools/{pool}/utilization": { + "get": { + "tags": [ + "system/ip-pools" + ], + "summary": "Fetch IP pool utilization", + "operationId": "ip_pool_utilization_view", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the IP pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolUtilization" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/ip-pools-service": { + "get": { + "tags": [ + "system/ip-pools" + ], + "summary": "Fetch Oxide service IP pool", + "operationId": "ip_pool_service_view", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPool" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/ip-pools-service/ranges": { + "get": { + "tags": [ + "system/ip-pools" + ], + "summary": "List IP ranges for the Oxide service pool", + "description": "Ranges are ordered by their first address.", + "operationId": "ip_pool_service_range_list", + "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", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolRangeResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/ip-pools-service/ranges/add": { + "post": { + "tags": [ + "system/ip-pools" + ], + "summary": "Add IP range to Oxide service pool", + "description": "IPv6 ranges are not allowed yet.", + "operationId": "ip_pool_service_range_add", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpRange" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolRange" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/ip-pools-service/ranges/remove": { + "post": { + "tags": [ + "system/ip-pools" + ], + "summary": "Remove IP range from Oxide service pool", + "operationId": "ip_pool_service_range_remove", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpRange" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/metrics/{metric_name}": { + "get": { + "tags": [ + "system/metrics" + ], + "summary": "View metrics", + "description": "View CPU, memory, or storage utilization metrics at the fleet or silo level.", + "operationId": "system_metric", + "parameters": [ + { + "in": "path", + "name": "metric_name", + "required": true, + "schema": { + "$ref": "#/components/schemas/SystemMetricName" + } + }, + { + "in": "query", + "name": "end_time", + "description": "An exclusive end time of metrics.", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "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": "order", + "description": "Query result order", + "schema": { + "$ref": "#/components/schemas/PaginationOrder" + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "start_time", + "description": "An inclusive start time of metrics.", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "silo", + "description": "Name or ID of the silo", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MeasurementResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "end_time", + "start_time" + ] + } + } + }, + "/v1/system/multicast-groups/by-ip/{address}": { + "get": { + "tags": [ + "experimental" + ], + "summary": "Look up multicast group by IP address.", + "operationId": "lookup_multicast_group_by_ip", + "parameters": [ + { + "in": "path", + "name": "address", + "description": "IP address of the multicast group", + "required": true, + "schema": { + "type": "string", + "format": "ip" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroup" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/address-lot": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "List address lots", + "operationId": "networking_address_lot_list", + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddressLotResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "system/networking" + ], + "summary": "Create address lot", + "operationId": "networking_address_lot_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddressLotCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddressLotCreateResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/address-lot/{address_lot}": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Fetch address lot", + "operationId": "networking_address_lot_view", + "parameters": [ + { + "in": "path", + "name": "address_lot", + "description": "Name or ID of the address lot", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddressLotViewResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "system/networking" + ], + "summary": "Delete address lot", + "operationId": "networking_address_lot_delete", + "parameters": [ + { + "in": "path", + "name": "address_lot", + "description": "Name or ID of the address lot", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/address-lot/{address_lot}/blocks": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "List blocks in address lot", + "operationId": "networking_address_lot_block_list", + "parameters": [ + { + "in": "path", + "name": "address_lot", + "description": "Name or ID of the address lot", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddressLotBlockResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/networking/allow-list": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Get user-facing services IP allowlist", + "operationId": "networking_allow_list_view", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllowList" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "system/networking" + ], + "summary": "Update user-facing services IP allowlist", + "operationId": "networking_allow_list_update", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllowListUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllowList" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/bfd-disable": { + "post": { + "tags": [ + "system/networking" + ], + "summary": "Disable a BFD session", + "operationId": "networking_bfd_disable", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BfdSessionDisable" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/bfd-enable": { + "post": { + "tags": [ + "system/networking" + ], + "summary": "Enable a BFD session", + "operationId": "networking_bfd_enable", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BfdSessionEnable" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/bfd-status": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Get BFD status", + "operationId": "networking_bfd_status", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_BfdStatus", + "type": "array", + "items": { + "$ref": "#/components/schemas/BfdStatus" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/bgp": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "List BGP configurations", + "operationId": "networking_bgp_config_list", + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BgpConfigResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "system/networking" + ], + "summary": "Create new BGP configuration", + "operationId": "networking_bgp_config_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BgpConfigCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BgpConfig" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "system/networking" + ], + "summary": "Delete BGP configuration", + "operationId": "networking_bgp_config_delete", + "parameters": [ + { + "in": "query", + "name": "name_or_id", + "description": "A name or id to use when selecting BGP config.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/bgp-announce-set": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "List BGP announce sets", + "operationId": "networking_bgp_announce_set_list", + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_BgpAnnounceSet", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpAnnounceSet" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "put": { + "tags": [ + "system/networking" + ], + "summary": "Update BGP announce set", + "description": "If the announce set exists, this endpoint replaces the existing announce set with the one specified.", + "operationId": "networking_bgp_announce_set_update", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BgpAnnounceSetCreate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BgpAnnounceSet" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/bgp-announce-set/{announce_set}": { + "delete": { + "tags": [ + "system/networking" + ], + "summary": "Delete BGP announce set", + "operationId": "networking_bgp_announce_set_delete", + "parameters": [ + { + "in": "path", + "name": "announce_set", + "description": "Name or ID of the announce set", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/bgp-announce-set/{announce_set}/announcement": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Get originated routes for a specified BGP announce set", + "operationId": "networking_bgp_announcement_list", + "parameters": [ + { + "in": "path", + "name": "announce_set", + "description": "Name or ID of the announce set", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_BgpAnnouncement", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpAnnouncement" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/bgp-exported": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Get BGP exported routes", + "operationId": "networking_bgp_exported", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BgpExported" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/bgp-message-history": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Get BGP router message history", + "operationId": "networking_bgp_message_history", + "parameters": [ + { + "in": "query", + "name": "asn", + "description": "The ASN to filter on. Required.", + "required": true, + "schema": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AggregateBgpMessageHistory" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/bgp-routes-ipv4": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Get imported IPv4 BGP routes", + "operationId": "networking_bgp_imported_routes_ipv4", + "parameters": [ + { + "in": "query", + "name": "asn", + "description": "The ASN to filter on. Required.", + "required": true, + "schema": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_BgpImportedRouteIpv4", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpImportedRouteIpv4" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/bgp-status": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Get BGP peer status", + "operationId": "networking_bgp_status", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_BgpPeerStatus", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpPeerStatus" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/inbound-icmp": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Return whether API services can receive limited ICMP traffic", + "operationId": "networking_inbound_icmp_view", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceIcmpConfig" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "system/networking" + ], + "summary": "Set whether API services can receive limited ICMP traffic", + "operationId": "networking_inbound_icmp_update", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceIcmpConfig" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/loopback-address": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "List loopback addresses", + "operationId": "networking_loopback_address_list", + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoopbackAddressResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "system/networking" + ], + "summary": "Create loopback address", + "operationId": "networking_loopback_address_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoopbackAddressCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoopbackAddress" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/loopback-address/{rack_id}/{switch_location}/{address}/{subnet_mask}": { + "delete": { + "tags": [ + "system/networking" + ], + "summary": "Delete loopback address", + "operationId": "networking_loopback_address_delete", + "parameters": [ + { + "in": "path", + "name": "address", + "description": "The IP address and subnet mask to use when selecting the loopback address.", + "required": true, + "schema": { + "type": "string", + "format": "ip" + } + }, + { + "in": "path", + "name": "rack_id", + "description": "The rack to use when selecting the loopback address.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "path", + "name": "subnet_mask", + "description": "The IP address and subnet mask to use when selecting the loopback address.", + "required": true, + "schema": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + { + "in": "path", + "name": "switch_location", + "description": "The switch location to use when selecting the loopback address.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/switch-port-settings": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "List switch port settings", + "operationId": "networking_switch_port_settings_list", + "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" + } + }, + { + "in": "query", + "name": "port_settings", + "description": "An optional name or id to use when selecting port settings.", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchPortSettingsIdentityResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "system/networking" + ], + "summary": "Create switch port settings", + "operationId": "networking_switch_port_settings_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchPortSettingsCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchPortSettings" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "system/networking" + ], + "summary": "Delete switch port settings", + "operationId": "networking_switch_port_settings_delete", + "parameters": [ + { + "in": "query", + "name": "port_settings", + "description": "An optional name or id to use when selecting port settings.", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/switch-port-settings/{port}": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Get information about switch port", + "operationId": "networking_switch_port_settings_view", + "parameters": [ + { + "in": "path", + "name": "port", + "description": "A name or id to use when selecting switch port settings info objects.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchPortSettings" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/policy": { + "get": { + "tags": [ + "policy" + ], + "summary": "Fetch top-level IAM policy", + "operationId": "system_policy_view", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FleetRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "policy" + ], + "summary": "Update top-level IAM policy", + "operationId": "system_policy_update", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FleetRolePolicy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FleetRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/scim/tokens": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "List SCIM tokens", + "description": "Specify the silo by name or ID using the `silo` query parameter.", + "operationId": "scim_token_list", + "parameters": [ + { + "in": "query", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_ScimClientBearerToken", + "type": "array", + "items": { + "$ref": "#/components/schemas/ScimClientBearerToken" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "tags": [ + "system/silos" + ], + "summary": "Create SCIM token", + "description": "Specify the silo by name or ID using the `silo` query parameter. Be sure to save the bearer token in the response. It will not be retrievable later through the token view and list endpoints.", + "operationId": "scim_token_create", + "parameters": [ + { + "in": "query", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScimClientBearerTokenValue" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/scim/tokens/{token_id}": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "Fetch SCIM token", + "description": "Specify the silo by name or ID using the `silo` query parameter.", + "operationId": "scim_token_view", + "parameters": [ + { + "in": "path", + "name": "token_id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScimClientBearerToken" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "system/silos" + ], + "summary": "Delete SCIM token", + "description": "Specify the silo by name or ID using the `silo` query parameter.", + "operationId": "scim_token_delete", + "parameters": [ + { + "in": "path", + "name": "token_id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/silo-quotas": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "Lists resource quotas for all silos", + "operationId": "system_quotas_list", + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloQuotasResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/silos": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "List silos", + "description": "Lists silos that are discoverable based on the current permissions.", + "operationId": "silo_list", + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "system/silos" + ], + "summary": "Create a silo", + "operationId": "silo_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Silo" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/silos/{silo}": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "Fetch silo", + "description": "Fetch silo by name or ID.", + "operationId": "silo_view", + "parameters": [ + { + "in": "path", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Silo" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "system/silos" + ], + "summary": "Delete a silo", + "description": "Delete a silo by name or ID.", + "operationId": "silo_delete", + "parameters": [ + { + "in": "path", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/silos/{silo}/ip-pools": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "List IP pools linked to silo", + "description": "Linked IP pools are available to users in the specified silo. A silo can have at most one default pool. IPs are allocated from the default pool when users ask for one without specifying a pool.", + "operationId": "silo_ip_pool_list", + "parameters": [ + { + "in": "path", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloIpPoolResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/silos/{silo}/policy": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "Fetch silo IAM policy", + "operationId": "silo_policy_view", + "parameters": [ + { + "in": "path", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "system/silos" + ], + "summary": "Update silo IAM policy", + "operationId": "silo_policy_update", + "parameters": [ + { + "in": "path", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloRolePolicy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/silos/{silo}/quotas": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "Fetch resource quotas for silo", + "operationId": "silo_quotas_view", + "parameters": [ + { + "in": "path", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloQuotas" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "system/silos" + ], + "summary": "Update resource quotas for silo", + "description": "If a quota value is not specified, it will remain unchanged.", + "operationId": "silo_quotas_update", + "parameters": [ + { + "in": "path", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloQuotasUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloQuotas" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/timeseries/query": { + "post": { + "tags": [ + "system/metrics" + ], + "summary": "Run timeseries query", + "description": "Queries are written in OxQL.", + "operationId": "system_timeseries_query", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeseriesQuery" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OxqlQueryResult" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/timeseries/schemas": { + "get": { + "tags": [ + "system/metrics" + ], + "summary": "List timeseries schemas", + "operationId": "system_timeseries_schema_list", + "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", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeseriesSchemaResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/update/repositories": { + "get": { + "tags": [ + "system/update" + ], + "summary": "List all TUF repositories", + "description": "Returns a paginated list of all TUF repositories ordered by system version (newest first by default).", + "operationId": "system_update_repository_list", + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/VersionSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TufRepoResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "put": { + "tags": [ + "system/update" + ], + "summary": "Upload system release repository", + "description": "System release repositories are verified by the updates trust store.", + "operationId": "system_update_repository_upload", + "parameters": [ + { + "in": "query", + "name": "file_name", + "description": "The name of the uploaded file.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TufRepoUpload" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/update/repositories/{system_version}": { + "get": { + "tags": [ + "system/update" + ], + "summary": "Fetch system release repository by version", + "operationId": "system_update_repository_view", + "parameters": [ + { + "in": "path", + "name": "system_version", + "description": "The version to get.", + "required": true, + "schema": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TufRepo" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/update/status": { + "get": { + "tags": [ + "system/update" + ], + "summary": "Fetch system update status", + "description": "Returns information about the current target release and the progress of system software updates.", + "operationId": "system_update_status", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateStatus" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/update/target-release": { + "put": { + "tags": [ + "system/update" + ], + "summary": "Set target release", + "description": "Set the current target release of the rack's system software. The rack reconfigurator will treat the software specified here as a goal state for the rack's software, and attempt to asynchronously update to that release. Use the update status endpoint to view the current target release.", + "operationId": "target_release_update", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetTargetReleaseParams" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/update/trust-roots": { + "get": { + "tags": [ + "system/update" + ], + "summary": "List root roles in the updates trust store", + "description": "A root role is a JSON document describing the cryptographic keys that are trusted to sign system release repositories, as described by The Update Framework. Uploading a repository requires its metadata to be signed by keys trusted by the trust store.", + "operationId": "system_update_trust_root_list", + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdatesTrustRootResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "system/update" + ], + "summary": "Add trusted root role to updates trust store", + "operationId": "system_update_trust_root_create", + "requestBody": { + "content": { + "application/json": { + "schema": {} + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdatesTrustRoot" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/update/trust-roots/{trust_root_id}": { + "get": { + "tags": [ + "system/update" + ], + "summary": "Fetch trusted root role", + "operationId": "system_update_trust_root_view", + "parameters": [ + { + "in": "path", + "name": "trust_root_id", + "description": "ID of the trust root", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdatesTrustRoot" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "system/update" + ], + "summary": "Delete trusted root role", + "description": "Note that this method does not currently check for any uploaded system release repositories that would become untrusted after deleting the root role.", + "operationId": "system_update_trust_root_delete", + "parameters": [ + { + "in": "path", + "name": "trust_root_id", + "description": "ID of the trust root", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/users": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "List built-in (system) users in silo", + "operationId": "silo_user_list", + "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" + } + }, + { + "in": "query", + "name": "silo", + "description": "Name or ID of the silo", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "silo" + ] + } + } + }, + "/v1/system/users/{user_id}": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "Fetch built-in (system) user", + "operationId": "silo_user_view", + "parameters": [ + { + "in": "path", + "name": "user_id", + "description": "The user's internal ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/users-builtin": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "List built-in users", + "operationId": "user_builtin_list", + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserBuiltinResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/users-builtin/{user}": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "Fetch built-in user", + "operationId": "user_builtin_view", + "parameters": [ + { + "in": "path", + "name": "user", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserBuiltin" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/utilization/silos": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "List current utilization state for all silos", + "operationId": "silo_utilization_list", + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloUtilizationResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/utilization/silos/{silo}": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "Fetch current utilization for given silo", + "operationId": "silo_utilization_view", + "parameters": [ + { + "in": "path", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloUtilization" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/timeseries/query": { + "post": { + "tags": [ + "experimental" + ], + "summary": "Run project-scoped timeseries query", + "description": "Queries are written in OxQL. Project must be specified by name or ID in URL query parameter. The OxQL query will only return timeseries data from the specified project.", + "operationId": "timeseries_query", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeseriesQuery" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OxqlQueryResult" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/users": { + "get": { + "tags": [ + "silos" + ], + "summary": "List users", + "operationId": "user_list", + "parameters": [ + { + "in": "query", + "name": "group", + "schema": { + "nullable": true, + "type": "string", + "format": "uuid" + } + }, + { + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/users/{user_id}": { + "get": { + "tags": [ + "silos" + ], + "summary": "Fetch user", + "operationId": "user_view", + "parameters": [ + { + "in": "path", + "name": "user_id", + "description": "ID of the user", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/users/{user_id}/access-tokens": { + "get": { + "tags": [ + "silos" + ], + "summary": "List user's access tokens", + "operationId": "user_token_list", + "parameters": [ + { + "in": "path", + "name": "user_id", + "description": "ID of the user", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeviceAccessTokenResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/users/{user_id}/logout": { + "post": { + "tags": [ + "silos" + ], + "summary": "Log user out", + "description": "Silo admins can use this endpoint to log the specified user out by deleting all of their tokens AND sessions. This cannot be undone.", + "operationId": "user_logout", + "parameters": [ + { + "in": "path", + "name": "user_id", + "description": "ID of the user", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/users/{user_id}/sessions": { + "get": { + "tags": [ + "silos" + ], + "summary": "List user's console sessions", + "operationId": "user_session_list", + "parameters": [ + { + "in": "path", + "name": "user_id", + "description": "ID of the user", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "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" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConsoleSessionResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/utilization": { + "get": { + "tags": [ + "silos" + ], + "summary": "Fetch resource utilization for user's current silo", + "operationId": "utilization_view", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Utilization" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpc-firewall-rules": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "List firewall rules", + "operationId": "vpc_firewall_rules_view", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcFirewallRules" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "vpcs" + ], + "summary": "Replace firewall rules", + "description": "The maximum number of rules per VPC is 1024.\n\nTargets are used to specify the set of instances to which a firewall rule applies. You can target instances directly by name, or specify a VPC, VPC subnet, IP, or IP subnet, which will apply the rule to traffic going to all matching instances. Targets are additive: the rule applies to instances matching ANY target. The maximum number of targets is 256.\n\nFilters reduce the scope of a firewall rule. Without filters, the rule applies to all packets to the targets (or from the targets, if it's an outbound rule). With multiple filters, the rule applies only to packets matching ALL filters. The maximum number of each type of filter is 256.", + "operationId": "vpc_firewall_rules_update", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcFirewallRuleUpdateParams" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcFirewallRules" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpc-router-routes": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "List routes", + "description": "List the routes associated with a router in a particular VPC.", + "operationId": "vpc_router_route_list", + "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" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "router", + "description": "Name or ID of the router", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC, only required if `router` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouterRouteResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "router" + ] + } + }, + "post": { + "tags": [ + "vpcs" + ], + "summary": "Create route", + "operationId": "vpc_router_route_create", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "router", + "description": "Name or ID of the router", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC, only required if `router` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouterRouteCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouterRoute" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpc-router-routes/{route}": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "Fetch route", + "operationId": "vpc_router_route_view", + "parameters": [ + { + "in": "path", + "name": "route", + "description": "Name or ID of the route", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "router", + "description": "Name or ID of the router", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC, only required if `router` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouterRoute" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "vpcs" + ], + "summary": "Update route", + "operationId": "vpc_router_route_update", + "parameters": [ + { + "in": "path", + "name": "route", + "description": "Name or ID of the route", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "router", + "description": "Name or ID of the router", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC, only required if `router` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouterRouteUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouterRoute" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "vpcs" + ], + "summary": "Delete route", + "operationId": "vpc_router_route_delete", + "parameters": [ + { + "in": "path", + "name": "route", + "description": "Name or ID of the route", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "router", + "description": "Name or ID of the router", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC, only required if `router` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpc-routers": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "List routers", + "operationId": "vpc_router_list", + "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" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcRouterResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "vpc" + ] + } + }, + "post": { + "tags": [ + "vpcs" + ], + "summary": "Create VPC router", + "operationId": "vpc_router_create", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcRouterCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcRouter" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpc-routers/{router}": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "Fetch router", + "operationId": "vpc_router_view", + "parameters": [ + { + "in": "path", + "name": "router", + "description": "Name or ID of the router", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcRouter" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "vpcs" + ], + "summary": "Update router", + "operationId": "vpc_router_update", + "parameters": [ + { + "in": "path", + "name": "router", + "description": "Name or ID of the router", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcRouterUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcRouter" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "vpcs" + ], + "summary": "Delete router", + "operationId": "vpc_router_delete", + "parameters": [ + { + "in": "path", + "name": "router", + "description": "Name or ID of the router", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpc-subnets": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "List subnets", + "operationId": "vpc_subnet_list", + "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" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcSubnetResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "vpc" + ] + } + }, + "post": { + "tags": [ + "vpcs" + ], + "summary": "Create subnet", + "operationId": "vpc_subnet_create", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcSubnetCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcSubnet" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpc-subnets/{subnet}": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "Fetch subnet", + "operationId": "vpc_subnet_view", + "parameters": [ + { + "in": "path", + "name": "subnet", + "description": "Name or ID of the subnet", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcSubnet" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "vpcs" + ], + "summary": "Update subnet", + "operationId": "vpc_subnet_update", + "parameters": [ + { + "in": "path", + "name": "subnet", + "description": "Name or ID of the subnet", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcSubnetUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcSubnet" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "vpcs" + ], + "summary": "Delete subnet", + "operationId": "vpc_subnet_delete", + "parameters": [ + { + "in": "path", + "name": "subnet", + "description": "Name or ID of the subnet", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpc-subnets/{subnet}/network-interfaces": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "List network interfaces", + "operationId": "vpc_subnet_list_network_interfaces", + "parameters": [ + { + "in": "path", + "name": "subnet", + "description": "Name or ID of the subnet", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "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" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceNetworkInterfaceResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/vpcs": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "List VPCs", + "operationId": "vpc_list", + "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" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "project" + ] + } + }, + "post": { + "tags": [ + "vpcs" + ], + "summary": "Create VPC", + "operationId": "vpc_create", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Vpc" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpcs/{vpc}": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "Fetch VPC", + "operationId": "vpc_view", + "parameters": [ + { + "in": "path", + "name": "vpc", + "description": "Name or ID of the VPC", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Vpc" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "vpcs" + ], + "summary": "Update a VPC", + "operationId": "vpc_update", + "parameters": [ + { + "in": "path", + "name": "vpc", + "description": "Name or ID of the VPC", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Vpc" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "vpcs" + ], + "summary": "Delete VPC", + "operationId": "vpc_delete", + "parameters": [ + { + "in": "path", + "name": "vpc", + "description": "Name or ID of the VPC", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/webhook-receivers": { + "post": { + "tags": [ + "system/alerts" + ], + "summary": "Create webhook receiver", + "operationId": "webhook_receiver_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookReceiver" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/webhook-receivers/{receiver}": { + "put": { + "tags": [ + "system/alerts" + ], + "summary": "Update webhook receiver", + "description": "Note that receiver secrets are NOT added or removed using this endpoint. Instead, use the `/v1/webhooks/{secrets}/?receiver={receiver}` endpoint to add and remove secrets.", + "operationId": "webhook_receiver_update", + "parameters": [ + { + "in": "path", + "name": "receiver", + "description": "The name or ID of the webhook receiver.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookReceiverUpdate" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/webhook-secrets": { + "get": { + "tags": [ + "system/alerts" + ], + "summary": "List webhook receiver secret IDs", + "operationId": "webhook_secrets_list", + "parameters": [ + { + "in": "query", + "name": "receiver", + "description": "The name or ID of the webhook receiver.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookSecrets" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "tags": [ + "system/alerts" + ], + "summary": "Add secret to webhook receiver", + "operationId": "webhook_secrets_add", + "parameters": [ + { + "in": "query", + "name": "receiver", + "description": "The name or ID of the webhook receiver.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookSecretCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookSecret" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/webhook-secrets/{secret_id}": { + "delete": { + "tags": [ + "system/alerts" + ], + "summary": "Remove secret from webhook receiver", + "operationId": "webhook_secrets_delete", + "parameters": [ + { + "in": "path", + "name": "secret_id", + "description": "ID of the secret.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + } + }, + "components": { + "schemas": { + "Address": { + "description": "An address tied to an address lot.", + "type": "object", + "properties": { + "address": { + "description": "The address and prefix length of this address.", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + }, + "address_lot": { + "description": "The address lot this address is drawn from.", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + }, + "vlan_id": { + "nullable": true, + "description": "Optional VLAN ID for this address", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "address", + "address_lot" + ] + }, + "AddressConfig": { + "description": "A set of addresses associated with a port configuration.", + "type": "object", + "properties": { + "addresses": { + "description": "The set of addresses assigned to the port configuration.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Address" + } + }, + "link_name": { + "description": "Link to assign the addresses to. On ports that are not broken out, this is always phy0. On a 2x breakout the options are phy0 and phy1, on 4x phy0-phy3, etc.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + }, + "required": [ + "addresses", + "link_name" + ] + }, + "AddressLot": { + "description": "Represents an address lot object, containing the id of the lot that can be used in other API calls.", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "kind": { + "description": "Desired use of `AddressLot`", + "allOf": [ + { + "$ref": "#/components/schemas/AddressLotKind" + } + ] + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "kind", + "name", + "time_created", + "time_modified" + ] + }, + "AddressLotBlock": { + "description": "An address lot block is a part of an address lot and contains a range of addresses. The range is inclusive.", + "type": "object", + "properties": { + "first_address": { + "description": "The first address of the block (inclusive).", + "type": "string", + "format": "ip" + }, + "id": { + "description": "The id of the address lot block.", + "type": "string", + "format": "uuid" + }, + "last_address": { + "description": "The last address of the block (inclusive).", + "type": "string", + "format": "ip" + } + }, + "required": [ + "first_address", + "id", + "last_address" + ] + }, + "AddressLotBlockCreate": { + "description": "Parameters for creating an address lot block. Fist and last addresses are inclusive.", + "type": "object", + "properties": { + "first_address": { + "description": "The first address in the lot (inclusive).", + "type": "string", + "format": "ip" + }, + "last_address": { + "description": "The last address in the lot (inclusive).", + "type": "string", + "format": "ip" + } + }, + "required": [ + "first_address", + "last_address" + ] + }, + "AddressLotBlockResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/AddressLotBlock" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "AddressLotCreate": { + "description": "Parameters for creating an address lot.", + "type": "object", + "properties": { + "blocks": { + "description": "The blocks to add along with the new address lot.", + "type": "array", + "items": { + "$ref": "#/components/schemas/AddressLotBlockCreate" + } + }, + "description": { + "type": "string" + }, + "kind": { + "description": "The kind of address lot to create.", + "allOf": [ + { + "$ref": "#/components/schemas/AddressLotKind" + } + ] + }, + "name": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "blocks", + "description", + "kind", + "name" + ] + }, + "AddressLotCreateResponse": { + "description": "An address lot and associated blocks resulting from creating an address lot.", + "type": "object", + "properties": { + "blocks": { + "description": "The address lot blocks that were created.", + "type": "array", + "items": { + "$ref": "#/components/schemas/AddressLotBlock" + } + }, + "lot": { + "description": "The address lot that was created.", + "allOf": [ + { + "$ref": "#/components/schemas/AddressLot" + } + ] + } + }, + "required": [ + "blocks", + "lot" + ] + }, + "AddressLotKind": { + "description": "The kind associated with an address lot.", + "oneOf": [ + { + "description": "Infrastructure address lots are used for network infrastructure like addresses assigned to rack switches.", + "type": "string", + "enum": [ + "infra" + ] + }, + { + "description": "Pool address lots are used by IP pools.", + "type": "string", + "enum": [ + "pool" + ] + } + ] + }, + "AddressLotResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/AddressLot" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "AddressLotViewResponse": { + "description": "An address lot and associated blocks resulting from viewing an address lot.", + "type": "object", + "properties": { + "blocks": { + "description": "The address lot blocks.", + "type": "array", + "items": { + "$ref": "#/components/schemas/AddressLotBlock" + } + }, + "lot": { + "description": "The address lot.", + "allOf": [ + { + "$ref": "#/components/schemas/AddressLot" + } + ] + } + }, + "required": [ + "blocks", + "lot" + ] + }, + "AffinityGroup": { + "description": "View of an Affinity Group", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "failure_domain": { + "$ref": "#/components/schemas/FailureDomain" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "policy": { + "$ref": "#/components/schemas/AffinityPolicy" + }, + "project_id": { + "type": "string", + "format": "uuid" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "failure_domain", + "id", + "name", + "policy", + "project_id", + "time_created", + "time_modified" + ] + }, + "AffinityGroupCreate": { + "description": "Create-time parameters for an `AffinityGroup`", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "failure_domain": { + "$ref": "#/components/schemas/FailureDomain" + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "policy": { + "$ref": "#/components/schemas/AffinityPolicy" + } + }, + "required": [ + "description", + "failure_domain", + "name", + "policy" + ] + }, + "AffinityGroupMember": { + "description": "A member of an Affinity Group\n\nMembership in a group is not exclusive - members may belong to multiple affinity / anti-affinity groups.\n\nAffinity Groups can contain up to 32 members.", + "oneOf": [ + { + "description": "An instance belonging to this group\n\nInstances can belong to up to 16 affinity groups.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "instance" + ] + }, + "value": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "run_state": { + "$ref": "#/components/schemas/InstanceState" + } + }, + "required": [ + "id", + "name", + "run_state" + ] + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "AffinityGroupMemberResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/AffinityGroupMember" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "AffinityGroupResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/AffinityGroup" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "AffinityGroupUpdate": { + "description": "Updateable properties of an `AffinityGroup`", + "type": "object", + "properties": { + "description": { + "nullable": true, + "type": "string" + }, + "name": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + } + }, + "AffinityPolicy": { + "description": "Affinity policy used to describe \"what to do when a request cannot be satisfied\"\n\nUsed for both Affinity and Anti-Affinity Groups", + "oneOf": [ + { + "description": "If the affinity request cannot be satisfied, allow it anyway.\n\nThis enables a \"best-effort\" attempt to satisfy the affinity policy.", + "type": "string", + "enum": [ + "allow" + ] + }, + { + "description": "If the affinity request cannot be satisfied, fail explicitly.", + "type": "string", + "enum": [ + "fail" + ] + } + ] + }, + "AggregateBgpMessageHistory": { + "description": "BGP message history for rack switches.", + "type": "object", + "properties": { + "switch_histories": { + "description": "BGP history organized by switch.", + "type": "array", + "items": { + "$ref": "#/components/schemas/SwitchBgpHistory" + } + } + }, + "required": [ + "switch_histories" + ] + }, + "AlertClass": { + "description": "An alert class.", + "type": "object", + "properties": { + "description": { + "description": "A description of what this alert class represents.", + "type": "string" + }, + "name": { + "description": "The name of the alert class.", + "type": "string" + } + }, + "required": [ + "description", + "name" + ] + }, + "AlertClassResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/AlertClass" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "AlertDelivery": { + "description": "A delivery of a webhook event.", + "type": "object", + "properties": { + "alert_class": { + "description": "The event class.", + "type": "string" + }, + "alert_id": { + "description": "The UUID of the event.", + "type": "string", + "format": "uuid" + }, + "attempts": { + "description": "Individual attempts to deliver this webhook event, and their outcomes.", + "allOf": [ + { + "$ref": "#/components/schemas/AlertDeliveryAttempts" + } + ] + }, + "id": { + "description": "The UUID of this delivery attempt.", + "type": "string", + "format": "uuid" + }, + "receiver_id": { + "description": "The UUID of the alert receiver that this event was delivered to.", + "type": "string", + "format": "uuid" + }, + "state": { + "description": "The state of this delivery.", + "allOf": [ + { + "$ref": "#/components/schemas/AlertDeliveryState" + } + ] + }, + "time_started": { + "description": "The time at which this delivery began (i.e. the event was dispatched to the receiver).", + "type": "string", + "format": "date-time" + }, + "trigger": { + "description": "Why this delivery was performed.", + "allOf": [ + { + "$ref": "#/components/schemas/AlertDeliveryTrigger" + } + ] + } + }, + "required": [ + "alert_class", + "alert_id", + "attempts", + "id", + "receiver_id", + "state", + "time_started", + "trigger" + ] + }, + "AlertDeliveryAttempts": { + "description": "A list of attempts to deliver an alert to a receiver.\n\nThe type of the delivery attempt model depends on the receiver type, as it may contain information specific to that delivery mechanism. For example, webhook delivery attempts contain the HTTP status code of the webhook request.", + "oneOf": [ + { + "description": "A list of attempts to deliver an alert to a webhook receiver.", + "type": "object", + "properties": { + "webhook": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebhookDeliveryAttempt" + } + } + }, + "required": [ + "webhook" + ], + "additionalProperties": false + } + ] + }, + "AlertDeliveryId": { + "type": "object", + "properties": { + "delivery_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "delivery_id" + ] + }, + "AlertDeliveryResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/AlertDelivery" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "AlertDeliveryState": { + "description": "The state of a webhook delivery attempt.", + "oneOf": [ + { + "description": "The webhook event has not yet been delivered successfully.\n\nEither no delivery attempts have yet been performed, or the delivery has failed at least once but has retries remaining.", + "type": "string", + "enum": [ + "pending" + ] + }, + { + "description": "The webhook event has been delivered successfully.", + "type": "string", + "enum": [ + "delivered" + ] + }, + { + "description": "The webhook delivery attempt has failed permanently and will not be retried again.", + "type": "string", + "enum": [ + "failed" + ] + } + ] + }, + "AlertDeliveryTrigger": { + "description": "The reason an alert was delivered", + "oneOf": [ + { + "description": "Delivery was triggered by the alert itself.", + "type": "string", + "enum": [ + "alert" + ] + }, + { + "description": "Delivery was triggered by a request to resend the alert.", + "type": "string", + "enum": [ + "resend" + ] + }, + { + "description": "This delivery is a liveness probe.", + "type": "string", + "enum": [ + "probe" + ] + } + ] + }, + "AlertProbeResult": { + "description": "Data describing the result of an alert receiver liveness probe attempt.", + "type": "object", + "properties": { + "probe": { + "description": "The outcome of the probe delivery.", + "allOf": [ + { + "$ref": "#/components/schemas/AlertDelivery" + } + ] + }, + "resends_started": { + "nullable": true, + "description": "If the probe request succeeded, and resending failed deliveries on success was requested, the number of new delivery attempts started. Otherwise, if the probe did not succeed, or resending failed deliveries was not requested, this is null.\n\nNote that this may be 0, if there were no events found which had not been delivered successfully to this receiver.", + "type": "integer", + "format": "uint", + "minimum": 0 + } + }, + "required": [ + "probe" + ] + }, + "AlertReceiver": { + "description": "The configuration for an alert receiver.", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "kind": { + "description": "Configuration specific to the kind of alert receiver that this is.", + "allOf": [ + { + "$ref": "#/components/schemas/AlertReceiverKind" + } + ] + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "subscriptions": { + "description": "The list of alert classes to which this receiver is subscribed.", + "type": "array", + "items": { + "$ref": "#/components/schemas/AlertSubscription" + } + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "kind", + "name", + "subscriptions", + "time_created", + "time_modified" + ] + }, + "AlertReceiverKind": { + "description": "The possible alert delivery mechanisms for an alert receiver.", + "oneOf": [ + { + "description": "Webhook-specific alert receiver configuration.", + "type": "object", + "properties": { + "endpoint": { + "description": "The URL that webhook notification requests are sent to.", + "type": "string", + "format": "uri" + }, + "kind": { + "type": "string", + "enum": [ + "webhook" + ] + }, + "secrets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebhookSecret" + } + } + }, + "required": [ + "endpoint", + "kind", + "secrets" + ] + } + ] + }, + "AlertReceiverResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/AlertReceiver" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "AlertSubscription": { + "title": "A webhook event class subscription", + "description": "A webhook event class subscription matches either a single event class exactly, or a glob pattern including wildcards that may match multiple event classes", + "type": "string", + "pattern": "^([a-zA-Z0-9_]+|\\*|\\*\\*)(\\.([a-zA-Z0-9_]+|\\*|\\*\\*))*$" + }, + "AlertSubscriptionCreate": { + "type": "object", + "properties": { + "subscription": { + "description": "The event class pattern to subscribe to.", + "allOf": [ + { + "$ref": "#/components/schemas/AlertSubscription" + } + ] + } + }, + "required": [ + "subscription" + ] + }, + "AlertSubscriptionCreated": { + "type": "object", + "properties": { + "subscription": { + "description": "The new subscription added to the receiver.", + "allOf": [ + { + "$ref": "#/components/schemas/AlertSubscription" + } + ] + } + }, + "required": [ + "subscription" + ] + }, + "AllowList": { + "description": "Allowlist of IPs or subnets that can make requests to user-facing services.", + "type": "object", + "properties": { + "allowed_ips": { + "description": "The allowlist of IPs or subnets.", + "allOf": [ + { + "$ref": "#/components/schemas/AllowedSourceIps" + } + ] + }, + "time_created": { + "description": "Time the list was created.", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "Time the list was last modified.", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "allowed_ips", + "time_created", + "time_modified" + ] + }, + "AllowListUpdate": { + "description": "Parameters for updating allowed source IPs", + "type": "object", + "properties": { + "allowed_ips": { + "description": "The new list of allowed source IPs.", + "allOf": [ + { + "$ref": "#/components/schemas/AllowedSourceIps" + } + ] + } + }, + "required": [ + "allowed_ips" + ] + }, + "AllowedSourceIps": { + "description": "Description of source IPs allowed to reach rack services.", + "oneOf": [ + { + "description": "Allow traffic from any external IP address.", + "type": "object", + "properties": { + "allow": { + "type": "string", + "enum": [ + "any" + ] + } + }, + "required": [ + "allow" + ] + }, + { + "description": "Restrict access to a specific set of source IP addresses or subnets.\n\nAll others are prevented from reaching rack services.", + "type": "object", + "properties": { + "allow": { + "type": "string", + "enum": [ + "list" + ] + }, + "ips": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IpNet" + } + } + }, + "required": [ + "allow", + "ips" + ] + } + ] + }, + "AntiAffinityGroup": { + "description": "View of an Anti-Affinity Group", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "failure_domain": { + "$ref": "#/components/schemas/FailureDomain" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "policy": { + "$ref": "#/components/schemas/AffinityPolicy" + }, + "project_id": { + "type": "string", + "format": "uuid" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "failure_domain", + "id", + "name", + "policy", + "project_id", + "time_created", + "time_modified" + ] + }, + "AntiAffinityGroupCreate": { + "description": "Create-time parameters for an `AntiAffinityGroup`", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "failure_domain": { + "$ref": "#/components/schemas/FailureDomain" + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "policy": { + "$ref": "#/components/schemas/AffinityPolicy" + } + }, + "required": [ + "description", + "failure_domain", + "name", + "policy" + ] + }, + "AntiAffinityGroupMember": { + "description": "A member of an Anti-Affinity Group\n\nMembership in a group is not exclusive - members may belong to multiple affinity / anti-affinity groups.\n\nAnti-Affinity Groups can contain up to 32 members.", + "oneOf": [ + { + "description": "An instance belonging to this group\n\nInstances can belong to up to 16 anti-affinity groups.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "instance" + ] + }, + "value": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "run_state": { + "$ref": "#/components/schemas/InstanceState" + } + }, + "required": [ + "id", + "name", + "run_state" + ] + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "AntiAffinityGroupMemberResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/AntiAffinityGroupMember" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "AntiAffinityGroupResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/AntiAffinityGroup" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "AntiAffinityGroupUpdate": { + "description": "Updateable properties of an `AntiAffinityGroup`", + "type": "object", + "properties": { + "description": { + "nullable": true, + "type": "string" + }, + "name": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + } + }, + "AuditLogEntry": { + "description": "Audit log entry", + "type": "object", + "properties": { + "actor": { + "$ref": "#/components/schemas/AuditLogEntryActor" + }, + "auth_method": { + "nullable": true, + "description": "How the user authenticated the request. Possible values are \"session_cookie\" and \"access_token\". Optional because it will not be defined on unauthenticated requests like login attempts.", + "type": "string" + }, + "id": { + "description": "Unique identifier for the audit log entry", + "type": "string", + "format": "uuid" + }, + "operation_id": { + "description": "API endpoint ID, e.g., `project_create`", + "type": "string" + }, + "request_id": { + "description": "Request ID for tracing requests through the system", + "type": "string" + }, + "request_uri": { + "description": "URI of the request, truncated to 512 characters. Will only include host and scheme for HTTP/2 requests. For HTTP/1.1, the URI will consist of only the path and query.", + "type": "string" + }, + "result": { + "description": "Result of the operation", + "allOf": [ + { + "$ref": "#/components/schemas/AuditLogEntryResult" + } + ] + }, + "source_ip": { + "description": "IP address that made the request", + "type": "string", + "format": "ip" + }, + "time_completed": { + "description": "Time operation completed", + "type": "string", + "format": "date-time" + }, + "time_started": { + "description": "When the request was received", + "type": "string", + "format": "date-time" + }, + "user_agent": { + "nullable": true, + "description": "User agent string from the request, truncated to 256 characters.", + "type": "string" + } + }, + "required": [ + "actor", + "id", + "operation_id", + "request_id", + "request_uri", + "result", + "source_ip", + "time_completed", + "time_started" + ] + }, + "AuditLogEntryActor": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "user_builtin" + ] + }, + "user_builtin_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "kind", + "user_builtin_id" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "silo_user" + ] + }, + "silo_id": { + "type": "string", + "format": "uuid" + }, + "silo_user_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "kind", + "silo_id", + "silo_user_id" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "scim" + ] + }, + "silo_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "kind", + "silo_id" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "unauthenticated" + ] + } + }, + "required": [ + "kind" + ] + } + ] + }, + "AuditLogEntryResult": { + "description": "Result of an audit log entry", + "oneOf": [ + { + "description": "The operation completed successfully", + "type": "object", + "properties": { + "http_status_code": { + "description": "HTTP status code", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "kind": { + "type": "string", + "enum": [ + "success" + ] + } + }, + "required": [ + "http_status_code", + "kind" + ] + }, + { + "description": "The operation failed", + "type": "object", + "properties": { + "error_code": { + "nullable": true, + "type": "string" + }, + "error_message": { + "type": "string" + }, + "http_status_code": { + "description": "HTTP status code", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "kind": { + "type": "string", + "enum": [ + "error" + ] + } + }, + "required": [ + "error_message", + "http_status_code", + "kind" + ] + }, + { + "description": "After the logged operation completed, our attempt to write the result to the audit log failed, so it was automatically marked completed later by a background job. This does not imply that the operation itself timed out or failed, only our attempts to log its result.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "unknown" + ] + } + }, + "required": [ + "kind" + ] + } + ] + }, + "AuditLogEntryResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/AuditLogEntry" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "AuthzScope": { + "description": "Authorization scope for a timeseries.\n\nThis describes the level at which a user must be authorized to read data from a timeseries. For example, fleet-scoping means the data is only visible to an operator or fleet reader. Project-scoped, on the other hand, indicates that a user will see data limited to the projects on which they have read permissions.", + "oneOf": [ + { + "description": "Timeseries data is limited to fleet readers.", + "type": "string", + "enum": [ + "fleet" + ] + }, + { + "description": "Timeseries data is limited to the authorized silo for a user.", + "type": "string", + "enum": [ + "silo" + ] + }, + { + "description": "Timeseries data is limited to the authorized projects for a user.", + "type": "string", + "enum": [ + "project" + ] + }, + { + "description": "The timeseries is viewable to all without limitation.", + "type": "string", + "enum": [ + "viewable_to_all" + ] + } + ] + }, + "Baseboard": { + "description": "Properties that uniquely identify an Oxide hardware component", + "type": "object", + "properties": { + "part": { + "type": "string" + }, + "revision": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "serial": { + "type": "string" + } + }, + "required": [ + "part", + "revision", + "serial" + ] + }, + "BfdMode": { + "description": "BFD connection mode.", + "type": "string", + "enum": [ + "single_hop", + "multi_hop" + ] + }, + "BfdSessionDisable": { + "description": "Information needed to disable a BFD session", + "type": "object", + "properties": { + "remote": { + "description": "Address of the remote peer to disable a BFD session for.", + "type": "string", + "format": "ip" + }, + "switch": { + "description": "The switch to enable this session on. Must be `switch0` or `switch1`.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + }, + "required": [ + "remote", + "switch" + ] + }, + "BfdSessionEnable": { + "description": "Information about a bidirectional forwarding detection (BFD) session.", + "type": "object", + "properties": { + "detection_threshold": { + "description": "The negotiated Control packet transmission interval, multiplied by this variable, will be the Detection Time for this session (as seen by the remote system)", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "local": { + "nullable": true, + "description": "Address the Oxide switch will listen on for BFD traffic. If `None` then the unspecified address (0.0.0.0 or ::) is used.", + "type": "string", + "format": "ip" + }, + "mode": { + "description": "Select either single-hop (RFC 5881) or multi-hop (RFC 5883)", + "allOf": [ + { + "$ref": "#/components/schemas/BfdMode" + } + ] + }, + "remote": { + "description": "Address of the remote peer to establish a BFD session with.", + "type": "string", + "format": "ip" + }, + "required_rx": { + "description": "The minimum interval, in microseconds, between received BFD Control packets that this system requires", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "switch": { + "description": "The switch to enable this session on. Must be `switch0` or `switch1`.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + }, + "required": [ + "detection_threshold", + "mode", + "remote", + "required_rx", + "switch" + ] + }, + "BfdState": { + "oneOf": [ + { + "description": "A stable down state. Non-responsive to incoming messages.", + "type": "string", + "enum": [ + "admin_down" + ] + }, + { + "description": "The initial state.", + "type": "string", + "enum": [ + "down" + ] + }, + { + "description": "The peer has detected a remote peer in the down state.", + "type": "string", + "enum": [ + "init" + ] + }, + { + "description": "The peer has detected a remote peer in the up or init state while in the init state.", + "type": "string", + "enum": [ + "up" + ] + } + ] + }, + "BfdStatus": { + "type": "object", + "properties": { + "detection_threshold": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "local": { + "nullable": true, + "type": "string", + "format": "ip" + }, + "mode": { + "$ref": "#/components/schemas/BfdMode" + }, + "peer": { + "type": "string", + "format": "ip" + }, + "required_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "state": { + "$ref": "#/components/schemas/BfdState" + }, + "switch": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "detection_threshold", + "mode", + "peer", + "required_rx", + "state", + "switch" + ] + }, + "BgpAnnounceSet": { + "description": "Represents a BGP announce set by id. The id can be used with other API calls to view and manage the announce set.", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "name", + "time_created", + "time_modified" + ] + }, + "BgpAnnounceSetCreate": { + "description": "Parameters for creating a named set of BGP announcements.", + "type": "object", + "properties": { + "announcement": { + "description": "The announcements in this set.", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpAnnouncementCreate" + } + }, + "description": { + "type": "string" + }, + "name": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "announcement", + "description", + "name" + ] + }, + "BgpAnnouncement": { + "description": "A BGP announcement tied to an address lot block.", + "type": "object", + "properties": { + "address_lot_block_id": { + "description": "The address block the IP network being announced is drawn from.", + "type": "string", + "format": "uuid" + }, + "announce_set_id": { + "description": "The id of the set this announcement is a part of.", + "type": "string", + "format": "uuid" + }, + "network": { + "description": "The IP network being announced.", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + } + }, + "required": [ + "address_lot_block_id", + "announce_set_id", + "network" + ] + }, + "BgpAnnouncementCreate": { + "description": "A BGP announcement tied to a particular address lot block.", + "type": "object", + "properties": { + "address_lot_block": { + "description": "Address lot this announcement is drawn from.", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + }, + "network": { + "description": "The network being announced.", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + } + }, + "required": [ + "address_lot_block", + "network" + ] + }, + "BgpConfig": { + "description": "A base BGP configuration.", + "type": "object", + "properties": { + "asn": { + "description": "The autonomous system number of this BGP configuration.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + }, + "vrf": { + "nullable": true, + "description": "Optional virtual routing and forwarding identifier for this BGP configuration.", + "type": "string" + } + }, + "required": [ + "asn", + "description", + "id", + "name", + "time_created", + "time_modified" + ] + }, + "BgpConfigCreate": { + "description": "Parameters for creating a BGP configuration. This includes and autonomous system number (ASN) and a virtual routing and forwarding (VRF) identifier.", + "type": "object", + "properties": { + "asn": { + "description": "The autonomous system number of this BGP configuration.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "bgp_announce_set_id": { + "$ref": "#/components/schemas/NameOrId" + }, + "description": { + "type": "string" + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "vrf": { + "nullable": true, + "description": "Optional virtual routing and forwarding identifier for this BGP configuration.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + }, + "required": [ + "asn", + "bgp_announce_set_id", + "description", + "name" + ] + }, + "BgpConfigResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpConfig" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "BgpExported": { + "description": "The current status of a BGP peer.", + "type": "object", + "properties": { + "exports": { + "description": "Exported routes indexed by peer address.", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv4Net" + } + } + } + }, + "required": [ + "exports" + ] + }, + "BgpImportedRouteIpv4": { + "description": "A route imported from a BGP peer.", + "type": "object", + "properties": { + "id": { + "description": "BGP identifier of the originating router.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "nexthop": { + "description": "The nexthop the prefix is reachable through.", + "type": "string", + "format": "ipv4" + }, + "prefix": { + "description": "The destination network prefix.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4Net" + } + ] + }, + "switch": { + "description": "Switch the route is imported into.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchLocation" + } + ] + } + }, + "required": [ + "id", + "nexthop", + "prefix", + "switch" + ] + }, + "BgpMessageHistory": {}, + "BgpPeer": { + "description": "A BGP peer configuration for an interface. Includes the set of announcements that will be advertised to the peer identified by `addr`. The `bgp_config` parameter is a reference to global BGP parameters. The `interface_name` indicates what interface the peer should be contacted on.", + "type": "object", + "properties": { + "addr": { + "nullable": true, + "description": "The address of the host to peer with. If not provided, this is an unnumbered BGP session that will be established over the interface specified by `interface_name`.", + "type": "string", + "format": "ip" + }, + "allowed_export": { + "description": "Define export policy for a peer.", + "allOf": [ + { + "$ref": "#/components/schemas/ImportExportPolicy" + } + ] + }, + "allowed_import": { + "description": "Define import policy for a peer.", + "allOf": [ + { + "$ref": "#/components/schemas/ImportExportPolicy" + } + ] + }, + "bgp_config": { + "description": "The global BGP configuration used for establishing a session with this peer.", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + }, + "communities": { + "description": "Include the provided communities in updates sent to the peer.", + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "connect_retry": { + "description": "How long to to wait between TCP connection retries (seconds).", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "delay_open": { + "description": "How long to delay sending an open request after establishing a TCP session (seconds).", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "enforce_first_as": { + "description": "Enforce that the first AS in paths received from this peer is the peer's AS.", + "type": "boolean" + }, + "hold_time": { + "description": "How long to hold peer connections between keepalives (seconds).", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "idle_hold_time": { + "description": "How long to hold a peer in idle before attempting a new session (seconds).", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "interface_name": { + "description": "The name of interface to peer on. This is relative to the port configuration this BGP peer configuration is a part of. For example this value could be phy0 to refer to a primary physical interface. Or it could be vlan47 to refer to a VLAN interface.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "keepalive": { + "description": "How often to send keepalive requests (seconds).", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "local_pref": { + "nullable": true, + "description": "Apply a local preference to routes received from this peer.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "md5_auth_key": { + "nullable": true, + "description": "Use the given key for TCP-MD5 authentication with the peer.", + "type": "string" + }, + "min_ttl": { + "nullable": true, + "description": "Require messages from a peer have a minimum IP time to live field.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "multi_exit_discriminator": { + "nullable": true, + "description": "Apply the provided multi-exit discriminator (MED) updates sent to the peer.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "remote_asn": { + "nullable": true, + "description": "Require that a peer has a specified ASN.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "vlan_id": { + "nullable": true, + "description": "Associate a VLAN ID with a peer.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "allowed_export", + "allowed_import", + "bgp_config", + "communities", + "connect_retry", + "delay_open", + "enforce_first_as", + "hold_time", + "idle_hold_time", + "interface_name", + "keepalive" + ] + }, + "BgpPeerConfig": { + "type": "object", + "properties": { + "link_name": { + "description": "Link that the peer is reachable on. On ports that are not broken out, this is always phy0. On a 2x breakout the options are phy0 and phy1, on 4x phy0-phy3, etc.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "peers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpPeer" + } + } + }, + "required": [ + "link_name", + "peers" + ] + }, + "BgpPeerState": { + "description": "The current state of a BGP peer.", + "oneOf": [ + { + "description": "Initial state. Refuse all incoming BGP connections. No resources allocated to peer.", + "type": "string", + "enum": [ + "idle" + ] + }, + { + "description": "Waiting for the TCP connection to be completed.", + "type": "string", + "enum": [ + "connect" + ] + }, + { + "description": "Trying to acquire peer by listening for and accepting a TCP connection.", + "type": "string", + "enum": [ + "active" + ] + }, + { + "description": "Waiting for open message from peer.", + "type": "string", + "enum": [ + "open_sent" + ] + }, + { + "description": "Waiting for keepaliave or notification from peer.", + "type": "string", + "enum": [ + "open_confirm" + ] + }, + { + "description": "There is an ongoing Connection Collision that hasn't yet been resolved. Two connections are maintained until one connection receives an Open or is able to progress into Established.", + "type": "string", + "enum": [ + "connection_collision" + ] + }, + { + "description": "Synchronizing with peer.", + "type": "string", + "enum": [ + "session_setup" + ] + }, + { + "description": "Session established. Able to exchange update, notification and keepalive messages with peers.", + "type": "string", + "enum": [ + "established" + ] + } + ] + }, + "BgpPeerStatus": { + "description": "The current status of a BGP peer.", + "type": "object", + "properties": { + "addr": { + "description": "IP address of the peer.", + "type": "string", + "format": "ip" + }, + "local_asn": { + "description": "Local autonomous system number.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "remote_asn": { + "description": "Remote autonomous system number.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "state": { + "description": "State of the peer.", + "allOf": [ + { + "$ref": "#/components/schemas/BgpPeerState" + } + ] + }, + "state_duration_millis": { + "description": "Time of last state change.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "switch": { + "description": "Switch with the peer session.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchLocation" + } + ] + } + }, + "required": [ + "addr", + "local_asn", + "remote_asn", + "state", + "state_duration_millis", + "switch" + ] + }, + "BinRangedouble": { + "description": "A type storing a range over `T`.\n\nThis type supports ranges similar to the `RangeTo`, `Range` and `RangeFrom` types in the standard library. Those cover `(..end)`, `(start..end)`, and `(start..)` respectively.", + "oneOf": [ + { + "description": "A range unbounded below and exclusively above, `..end`.", + "type": "object", + "properties": { + "end": { + "type": "number", + "format": "double" + }, + "type": { + "type": "string", + "enum": [ + "range_to" + ] + } + }, + "required": [ + "end", + "type" + ] + }, + { + "description": "A range bounded inclusively below and exclusively above, `start..end`.", + "type": "object", + "properties": { + "end": { + "type": "number", + "format": "double" + }, + "start": { + "type": "number", + "format": "double" + }, + "type": { + "type": "string", + "enum": [ + "range" + ] + } + }, + "required": [ + "end", + "start", + "type" + ] + }, + { + "description": "A range bounded inclusively below and unbounded above, `start..`.", + "type": "object", + "properties": { + "start": { + "type": "number", + "format": "double" + }, + "type": { + "type": "string", + "enum": [ + "range_from" + ] + } + }, + "required": [ + "start", + "type" + ] + } + ] + }, + "BinRangefloat": { + "description": "A type storing a range over `T`.\n\nThis type supports ranges similar to the `RangeTo`, `Range` and `RangeFrom` types in the standard library. Those cover `(..end)`, `(start..end)`, and `(start..)` respectively.", + "oneOf": [ + { + "description": "A range unbounded below and exclusively above, `..end`.", + "type": "object", + "properties": { + "end": { + "type": "number", + "format": "float" + }, + "type": { + "type": "string", + "enum": [ + "range_to" + ] + } + }, + "required": [ + "end", + "type" + ] + }, + { + "description": "A range bounded inclusively below and exclusively above, `start..end`.", + "type": "object", + "properties": { + "end": { + "type": "number", + "format": "float" + }, + "start": { + "type": "number", + "format": "float" + }, + "type": { + "type": "string", + "enum": [ + "range" + ] + } + }, + "required": [ + "end", + "start", + "type" + ] + }, + { + "description": "A range bounded inclusively below and unbounded above, `start..`.", + "type": "object", + "properties": { + "start": { + "type": "number", + "format": "float" + }, + "type": { + "type": "string", + "enum": [ + "range_from" + ] + } + }, + "required": [ + "start", + "type" + ] + } + ] + }, + "BinRangeint16": { + "description": "A type storing a range over `T`.\n\nThis type supports ranges similar to the `RangeTo`, `Range` and `RangeFrom` types in the standard library. Those cover `(..end)`, `(start..end)`, and `(start..)` respectively.", + "oneOf": [ + { + "description": "A range unbounded below and exclusively above, `..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "int16" + }, + "type": { + "type": "string", + "enum": [ + "range_to" + ] + } + }, + "required": [ + "end", + "type" + ] + }, + { + "description": "A range bounded inclusively below and exclusively above, `start..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "int16" + }, + "start": { + "type": "integer", + "format": "int16" + }, + "type": { + "type": "string", + "enum": [ + "range" + ] + } + }, + "required": [ + "end", + "start", + "type" + ] + }, + { + "description": "A range bounded inclusively below and unbounded above, `start..`.", + "type": "object", + "properties": { + "start": { + "type": "integer", + "format": "int16" + }, + "type": { + "type": "string", + "enum": [ + "range_from" + ] + } + }, + "required": [ + "start", + "type" + ] + } + ] + }, + "BinRangeint32": { + "description": "A type storing a range over `T`.\n\nThis type supports ranges similar to the `RangeTo`, `Range` and `RangeFrom` types in the standard library. Those cover `(..end)`, `(start..end)`, and `(start..)` respectively.", + "oneOf": [ + { + "description": "A range unbounded below and exclusively above, `..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string", + "enum": [ + "range_to" + ] + } + }, + "required": [ + "end", + "type" + ] + }, + { + "description": "A range bounded inclusively below and exclusively above, `start..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "int32" + }, + "start": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string", + "enum": [ + "range" + ] + } + }, + "required": [ + "end", + "start", + "type" + ] + }, + { + "description": "A range bounded inclusively below and unbounded above, `start..`.", + "type": "object", + "properties": { + "start": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string", + "enum": [ + "range_from" + ] + } + }, + "required": [ + "start", + "type" + ] + } + ] + }, + "BinRangeint64": { + "description": "A type storing a range over `T`.\n\nThis type supports ranges similar to the `RangeTo`, `Range` and `RangeFrom` types in the standard library. Those cover `(..end)`, `(start..end)`, and `(start..)` respectively.", + "oneOf": [ + { + "description": "A range unbounded below and exclusively above, `..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "int64" + }, + "type": { + "type": "string", + "enum": [ + "range_to" + ] + } + }, + "required": [ + "end", + "type" + ] + }, + { + "description": "A range bounded inclusively below and exclusively above, `start..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "int64" + }, + "start": { + "type": "integer", + "format": "int64" + }, + "type": { + "type": "string", + "enum": [ + "range" + ] + } + }, + "required": [ + "end", + "start", + "type" + ] + }, + { + "description": "A range bounded inclusively below and unbounded above, `start..`.", + "type": "object", + "properties": { + "start": { + "type": "integer", + "format": "int64" + }, + "type": { + "type": "string", + "enum": [ + "range_from" + ] + } + }, + "required": [ + "start", + "type" + ] + } + ] + }, + "BinRangeint8": { + "description": "A type storing a range over `T`.\n\nThis type supports ranges similar to the `RangeTo`, `Range` and `RangeFrom` types in the standard library. Those cover `(..end)`, `(start..end)`, and `(start..)` respectively.", + "oneOf": [ + { + "description": "A range unbounded below and exclusively above, `..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "int8" + }, + "type": { + "type": "string", + "enum": [ + "range_to" + ] + } + }, + "required": [ + "end", + "type" + ] + }, + { + "description": "A range bounded inclusively below and exclusively above, `start..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "int8" + }, + "start": { + "type": "integer", + "format": "int8" + }, + "type": { + "type": "string", + "enum": [ + "range" + ] + } + }, + "required": [ + "end", + "start", + "type" + ] + }, + { + "description": "A range bounded inclusively below and unbounded above, `start..`.", + "type": "object", + "properties": { + "start": { + "type": "integer", + "format": "int8" + }, + "type": { + "type": "string", + "enum": [ + "range_from" + ] + } + }, + "required": [ + "start", + "type" + ] + } + ] + }, + "BinRangeuint16": { + "description": "A type storing a range over `T`.\n\nThis type supports ranges similar to the `RangeTo`, `Range` and `RangeFrom` types in the standard library. Those cover `(..end)`, `(start..end)`, and `(start..)` respectively.", + "oneOf": [ + { + "description": "A range unbounded below and exclusively above, `..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "range_to" + ] + } + }, + "required": [ + "end", + "type" + ] + }, + { + "description": "A range bounded inclusively below and exclusively above, `start..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "start": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "range" + ] + } + }, + "required": [ + "end", + "start", + "type" + ] + }, + { + "description": "A range bounded inclusively below and unbounded above, `start..`.", + "type": "object", + "properties": { + "start": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "range_from" + ] + } + }, + "required": [ + "start", + "type" + ] + } + ] + }, + "BinRangeuint32": { + "description": "A type storing a range over `T`.\n\nThis type supports ranges similar to the `RangeTo`, `Range` and `RangeFrom` types in the standard library. Those cover `(..end)`, `(start..end)`, and `(start..)` respectively.", + "oneOf": [ + { + "description": "A range unbounded below and exclusively above, `..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "range_to" + ] + } + }, + "required": [ + "end", + "type" + ] + }, + { + "description": "A range bounded inclusively below and exclusively above, `start..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "start": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "range" + ] + } + }, + "required": [ + "end", + "start", + "type" + ] + }, + { + "description": "A range bounded inclusively below and unbounded above, `start..`.", + "type": "object", + "properties": { + "start": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "range_from" + ] + } + }, + "required": [ + "start", + "type" + ] + } + ] + }, + "BinRangeuint64": { + "description": "A type storing a range over `T`.\n\nThis type supports ranges similar to the `RangeTo`, `Range` and `RangeFrom` types in the standard library. Those cover `(..end)`, `(start..end)`, and `(start..)` respectively.", + "oneOf": [ + { + "description": "A range unbounded below and exclusively above, `..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "range_to" + ] + } + }, + "required": [ + "end", + "type" + ] + }, + { + "description": "A range bounded inclusively below and exclusively above, `start..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "start": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "range" + ] + } + }, + "required": [ + "end", + "start", + "type" + ] + }, + { + "description": "A range bounded inclusively below and unbounded above, `start..`.", + "type": "object", + "properties": { + "start": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "range_from" + ] + } + }, + "required": [ + "start", + "type" + ] + } + ] + }, + "BinRangeuint8": { + "description": "A type storing a range over `T`.\n\nThis type supports ranges similar to the `RangeTo`, `Range` and `RangeFrom` types in the standard library. Those cover `(..end)`, `(start..end)`, and `(start..)` respectively.", + "oneOf": [ + { + "description": "A range unbounded below and exclusively above, `..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "range_to" + ] + } + }, + "required": [ + "end", + "type" + ] + }, + { + "description": "A range bounded inclusively below and exclusively above, `start..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "start": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "range" + ] + } + }, + "required": [ + "end", + "start", + "type" + ] + }, + { + "description": "A range bounded inclusively below and unbounded above, `start..`.", + "type": "object", + "properties": { + "start": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "range_from" + ] + } + }, + "required": [ + "start", + "type" + ] + } + ] + }, + "Bindouble": { + "description": "Type storing bin edges and a count of samples within it.", + "type": "object", + "properties": { + "count": { + "description": "The total count of samples in this bin.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "range": { + "description": "The range of the support covered by this bin.", + "allOf": [ + { + "$ref": "#/components/schemas/BinRangedouble" + } + ] + } + }, + "required": [ + "count", + "range" + ] + }, + "Binfloat": { + "description": "Type storing bin edges and a count of samples within it.", + "type": "object", + "properties": { + "count": { + "description": "The total count of samples in this bin.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "range": { + "description": "The range of the support covered by this bin.", + "allOf": [ + { + "$ref": "#/components/schemas/BinRangefloat" + } + ] + } + }, + "required": [ + "count", + "range" + ] + }, + "Binint16": { + "description": "Type storing bin edges and a count of samples within it.", + "type": "object", + "properties": { + "count": { + "description": "The total count of samples in this bin.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "range": { + "description": "The range of the support covered by this bin.", + "allOf": [ + { + "$ref": "#/components/schemas/BinRangeint16" + } + ] + } + }, + "required": [ + "count", + "range" + ] + }, + "Binint32": { + "description": "Type storing bin edges and a count of samples within it.", + "type": "object", + "properties": { + "count": { + "description": "The total count of samples in this bin.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "range": { + "description": "The range of the support covered by this bin.", + "allOf": [ + { + "$ref": "#/components/schemas/BinRangeint32" + } + ] + } + }, + "required": [ + "count", + "range" + ] + }, + "Binint64": { + "description": "Type storing bin edges and a count of samples within it.", + "type": "object", + "properties": { + "count": { + "description": "The total count of samples in this bin.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "range": { + "description": "The range of the support covered by this bin.", + "allOf": [ + { + "$ref": "#/components/schemas/BinRangeint64" + } + ] + } + }, + "required": [ + "count", + "range" + ] + }, + "Binint8": { + "description": "Type storing bin edges and a count of samples within it.", + "type": "object", + "properties": { + "count": { + "description": "The total count of samples in this bin.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "range": { + "description": "The range of the support covered by this bin.", + "allOf": [ + { + "$ref": "#/components/schemas/BinRangeint8" + } + ] + } + }, + "required": [ + "count", + "range" + ] + }, + "Binuint16": { + "description": "Type storing bin edges and a count of samples within it.", + "type": "object", + "properties": { + "count": { + "description": "The total count of samples in this bin.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "range": { + "description": "The range of the support covered by this bin.", + "allOf": [ + { + "$ref": "#/components/schemas/BinRangeuint16" + } + ] + } + }, + "required": [ + "count", + "range" + ] + }, + "Binuint32": { + "description": "Type storing bin edges and a count of samples within it.", + "type": "object", + "properties": { + "count": { + "description": "The total count of samples in this bin.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "range": { + "description": "The range of the support covered by this bin.", + "allOf": [ + { + "$ref": "#/components/schemas/BinRangeuint32" + } + ] + } + }, + "required": [ + "count", + "range" + ] + }, + "Binuint64": { + "description": "Type storing bin edges and a count of samples within it.", + "type": "object", + "properties": { + "count": { + "description": "The total count of samples in this bin.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "range": { + "description": "The range of the support covered by this bin.", + "allOf": [ + { + "$ref": "#/components/schemas/BinRangeuint64" + } + ] + } + }, + "required": [ + "count", + "range" + ] + }, + "Binuint8": { + "description": "Type storing bin edges and a count of samples within it.", + "type": "object", + "properties": { + "count": { + "description": "The total count of samples in this bin.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "range": { + "description": "The range of the support covered by this bin.", + "allOf": [ + { + "$ref": "#/components/schemas/BinRangeuint8" + } + ] + } + }, + "required": [ + "count", + "range" + ] + }, + "BlockSize": { + "title": "disk block size in bytes", + "type": "integer", + "enum": [ + 512, + 2048, + 4096 + ] + }, + "ByteCount": { + "description": "Byte count to express memory or storage capacity.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "Certificate": { + "description": "View of a Certificate", + "type": "object", + "properties": { + "cert": { + "description": "PEM-formatted string containing public certificate chain", + "type": "string" + }, + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "service": { + "description": "The service using this certificate", + "allOf": [ + { + "$ref": "#/components/schemas/ServiceUsingCertificate" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "cert", + "description", + "id", + "name", + "service", + "time_created", + "time_modified" + ] + }, + "CertificateCreate": { + "description": "Create-time parameters for a `Certificate`", + "type": "object", + "properties": { + "cert": { + "description": "PEM-formatted string containing public certificate chain", + "type": "string" + }, + "description": { + "type": "string" + }, + "key": { + "description": "PEM-formatted string containing private key", + "type": "string" + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "service": { + "description": "The service using this certificate", + "allOf": [ + { + "$ref": "#/components/schemas/ServiceUsingCertificate" + } + ] + } + }, + "required": [ + "cert", + "description", + "key", + "name", + "service" + ] + }, + "CertificateResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Certificate" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "ConsoleSession": { + "description": "View of a console session", + "type": "object", + "properties": { + "id": { + "description": "A unique, immutable, system-controlled identifier for the session", + "type": "string", + "format": "uuid" + }, + "time_created": { + "type": "string", + "format": "date-time" + }, + "time_last_used": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "time_created", + "time_last_used" + ] + }, + "ConsoleSessionResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/ConsoleSession" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "Cumulativedouble": { + "description": "A cumulative or counter data type.", + "type": "object", + "properties": { + "start_time": { + "type": "string", + "format": "date-time" + }, + "value": { + "type": "number", + "format": "double" + } + }, + "required": [ + "start_time", + "value" + ] + }, + "Cumulativefloat": { + "description": "A cumulative or counter data type.", + "type": "object", + "properties": { + "start_time": { + "type": "string", + "format": "date-time" + }, + "value": { + "type": "number", + "format": "float" + } + }, + "required": [ + "start_time", + "value" + ] + }, + "Cumulativeint64": { + "description": "A cumulative or counter data type.", + "type": "object", + "properties": { + "start_time": { + "type": "string", + "format": "date-time" + }, + "value": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "start_time", + "value" + ] + }, + "Cumulativeuint64": { + "description": "A cumulative or counter data type.", + "type": "object", + "properties": { + "start_time": { + "type": "string", + "format": "date-time" + }, + "value": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "start_time", + "value" + ] + }, + "CurrentUser": { + "description": "Info about the current user", + "type": "object", + "properties": { + "display_name": { + "description": "Human-readable name that can identify the user", + "type": "string" + }, + "fleet_viewer": { + "description": "Whether this user has the viewer role on the fleet. Used by the web console to determine whether to show system-level UI.", + "type": "boolean" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "silo_admin": { + "description": "Whether this user has the admin role on their silo. Used by the web console to determine whether to show admin-only UI elements.", + "type": "boolean" + }, + "silo_id": { + "description": "Uuid of the silo to which this user belongs", + "type": "string", + "format": "uuid" + }, + "silo_name": { + "description": "Name of the silo to which this user belongs.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + }, + "required": [ + "display_name", + "fleet_viewer", + "id", + "silo_admin", + "silo_id", + "silo_name" + ] + }, + "Datum": { + "description": "A `Datum` is a single sampled data point from a metric.", + "oneOf": [ + { + "type": "object", + "properties": { + "datum": { + "type": "boolean" + }, + "type": { + "type": "string", + "enum": [ + "bool" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "type": "integer", + "format": "int8" + }, + "type": { + "type": "string", + "enum": [ + "i8" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "u8" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "type": "integer", + "format": "int16" + }, + "type": { + "type": "string", + "enum": [ + "i16" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "u16" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string", + "enum": [ + "i32" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "u32" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "type": "integer", + "format": "int64" + }, + "type": { + "type": "string", + "enum": [ + "i64" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "u64" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "type": "number", + "format": "float" + }, + "type": { + "type": "string", + "enum": [ + "f32" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "type": "number", + "format": "double" + }, + "type": { + "type": "string", + "enum": [ + "f64" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "string" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "type": { + "type": "string", + "enum": [ + "bytes" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Cumulativeint64" + }, + "type": { + "type": "string", + "enum": [ + "cumulative_i64" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Cumulativeuint64" + }, + "type": { + "type": "string", + "enum": [ + "cumulative_u64" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Cumulativefloat" + }, + "type": { + "type": "string", + "enum": [ + "cumulative_f32" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Cumulativedouble" + }, + "type": { + "type": "string", + "enum": [ + "cumulative_f64" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Histogramint8" + }, + "type": { + "type": "string", + "enum": [ + "histogram_i8" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Histogramuint8" + }, + "type": { + "type": "string", + "enum": [ + "histogram_u8" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Histogramint16" + }, + "type": { + "type": "string", + "enum": [ + "histogram_i16" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Histogramuint16" + }, + "type": { + "type": "string", + "enum": [ + "histogram_u16" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Histogramint32" + }, + "type": { + "type": "string", + "enum": [ + "histogram_i32" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Histogramuint32" + }, + "type": { + "type": "string", + "enum": [ + "histogram_u32" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Histogramint64" + }, + "type": { + "type": "string", + "enum": [ + "histogram_i64" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Histogramuint64" + }, + "type": { + "type": "string", + "enum": [ + "histogram_u64" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Histogramfloat" + }, + "type": { + "type": "string", + "enum": [ + "histogram_f32" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Histogramdouble" + }, + "type": { + "type": "string", + "enum": [ + "histogram_f64" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/MissingDatum" + }, + "type": { + "type": "string", + "enum": [ + "missing" + ] + } + }, + "required": [ + "datum", + "type" + ] + } + ] + }, + "DatumType": { + "description": "The type of an individual datum of a metric.", + "type": "string", + "enum": [ + "bool", + "i8", + "u8", + "i16", + "u16", + "i32", + "u32", + "i64", + "u64", + "f32", + "f64", + "string", + "bytes", + "cumulative_i64", + "cumulative_u64", + "cumulative_f32", + "cumulative_f64", + "histogram_i8", + "histogram_u8", + "histogram_i16", + "histogram_u16", + "histogram_i32", + "histogram_u32", + "histogram_i64", + "histogram_u64", + "histogram_f32", + "histogram_f64" + ] + }, + "DerEncodedKeyPair": { + "type": "object", + "properties": { + "private_key": { + "description": "request signing RSA private key in PKCS#1 format (base64 encoded der file)", + "type": "string" + }, + "public_cert": { + "description": "request signing public certificate (base64 encoded der file)", + "type": "string" + } + }, + "required": [ + "private_key", + "public_cert" + ] + }, + "DeviceAccessToken": { + "description": "View of a device access token", + "type": "object", + "properties": { + "id": { + "description": "A unique, immutable, system-controlled identifier for the token. Note that this ID is not the bearer token itself, which starts with \"oxide-token-\"", + "type": "string", + "format": "uuid" + }, + "time_created": { + "type": "string", + "format": "date-time" + }, + "time_expires": { + "nullable": true, + "description": "Expiration timestamp. A null value means the token does not automatically expire.", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "time_created" + ] + }, + "DeviceAccessTokenRequest": { + "type": "object", + "properties": { + "client_id": { + "type": "string", + "format": "uuid" + }, + "device_code": { + "type": "string" + }, + "grant_type": { + "type": "string" + } + }, + "required": [ + "client_id", + "device_code", + "grant_type" + ] + }, + "DeviceAccessTokenResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/DeviceAccessToken" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "DeviceAuthRequest": { + "type": "object", + "properties": { + "client_id": { + "type": "string", + "format": "uuid" + }, + "ttl_seconds": { + "nullable": true, + "description": "Optional lifetime for the access token in seconds.\n\nThis value will be validated during the confirmation step. If not specified, it defaults to the silo's max TTL, which can be seen at `/v1/auth-settings`. If specified, must not exceed the silo's max TTL.\n\nSome special logic applies when authenticating the confirmation request with an existing device token: the requested TTL must not produce an expiration time later than the authenticating token's expiration. If no TTL is specified, the expiration will be the lesser of the silo max and the authenticating token's expiration time. To get the longest allowed lifetime, omit the TTL and authenticate with a web console session.", + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + "required": [ + "client_id" + ] + }, + "DeviceAuthVerify": { + "type": "object", + "properties": { + "user_code": { + "type": "string" + } + }, + "required": [ + "user_code" + ] + }, + "Digest": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "sha256" + ] + }, + "value": { + "type": "string" + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "Disk": { + "description": "View of a Disk", + "type": "object", + "properties": { + "block_size": { + "$ref": "#/components/schemas/ByteCount" + }, + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "device_path": { + "type": "string" + }, + "disk_type": { + "$ref": "#/components/schemas/DiskType" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "image_id": { + "nullable": true, + "description": "ID of image from which disk was created, if any", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "project_id": { + "type": "string", + "format": "uuid" + }, + "size": { + "$ref": "#/components/schemas/ByteCount" + }, + "snapshot_id": { + "nullable": true, + "description": "ID of snapshot from which disk was created, if any", + "type": "string", + "format": "uuid" + }, + "state": { + "$ref": "#/components/schemas/DiskState" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "block_size", + "description", + "device_path", + "disk_type", + "id", + "name", + "project_id", + "size", + "state", + "time_created", + "time_modified" + ] + }, + "DiskBackend": { + "description": "The source of a `Disk`'s blocks", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "local" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "disk_source": { + "description": "The initial source for this disk", + "allOf": [ + { + "$ref": "#/components/schemas/DiskSource" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "distributed" + ] + } + }, + "required": [ + "disk_source", + "type" + ] + } + ] + }, + "DiskCreate": { + "description": "Create-time parameters for a `Disk`", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "disk_backend": { + "description": "The source for this `Disk`'s blocks", + "allOf": [ + { + "$ref": "#/components/schemas/DiskBackend" + } + ] + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "size": { + "description": "The total size of the Disk (in bytes)", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + } + }, + "required": [ + "description", + "disk_backend", + "name", + "size" + ] + }, + "DiskPath": { + "type": "object", + "properties": { + "disk": { + "description": "Name or ID of the disk", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + } + }, + "required": [ + "disk" + ] + }, + "DiskResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Disk" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "DiskSource": { + "description": "Different sources for a Distributed Disk", + "oneOf": [ + { + "description": "Create a blank disk", + "type": "object", + "properties": { + "block_size": { + "description": "size of blocks for this Disk. valid values are: 512, 2048, or 4096", + "allOf": [ + { + "$ref": "#/components/schemas/BlockSize" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "blank" + ] + } + }, + "required": [ + "block_size", + "type" + ] + }, + { + "description": "Create a disk from a disk snapshot", + "type": "object", + "properties": { + "snapshot_id": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "enum": [ + "snapshot" + ] + } + }, + "required": [ + "snapshot_id", + "type" + ] + }, + { + "description": "Create a disk from an image", + "type": "object", + "properties": { + "image_id": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "enum": [ + "image" + ] + } + }, + "required": [ + "image_id", + "type" + ] + }, + { + "description": "Create a blank disk that will accept bulk writes or pull blocks from an external source.", + "type": "object", + "properties": { + "block_size": { + "$ref": "#/components/schemas/BlockSize" + }, + "type": { + "type": "string", + "enum": [ + "importing_blocks" + ] + } + }, + "required": [ + "block_size", + "type" + ] + } + ] + }, + "DiskState": { + "description": "State of a Disk", + "oneOf": [ + { + "description": "Disk is being initialized", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "creating" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is ready but detached from any Instance", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "detached" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is ready to receive blocks from an external source", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "import_ready" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is importing blocks from a URL", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "importing_from_url" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is importing blocks from bulk writes", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "importing_from_bulk_writes" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is being finalized to state Detached", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "finalizing" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is undergoing maintenance", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "maintenance" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is being attached to the given Instance", + "type": "object", + "properties": { + "instance": { + "type": "string", + "format": "uuid" + }, + "state": { + "type": "string", + "enum": [ + "attaching" + ] + } + }, + "required": [ + "instance", + "state" + ] + }, + { + "description": "Disk is attached to the given Instance", + "type": "object", + "properties": { + "instance": { + "type": "string", + "format": "uuid" + }, + "state": { + "type": "string", + "enum": [ + "attached" + ] + } + }, + "required": [ + "instance", + "state" + ] + }, + { + "description": "Disk is being detached from the given Instance", + "type": "object", + "properties": { + "instance": { + "type": "string", + "format": "uuid" + }, + "state": { + "type": "string", + "enum": [ + "detaching" + ] + } + }, + "required": [ + "instance", + "state" + ] + }, + { + "description": "Disk has been destroyed", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "destroyed" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is unavailable", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "faulted" + ] + } + }, + "required": [ + "state" + ] + } + ] + }, + "DiskType": { + "type": "string", + "enum": [ + "distributed", + "local" + ] + }, + "Distributiondouble": { + "description": "A distribution is a sequence of bins and counts in those bins, and some statistical information tracked to compute the mean, standard deviation, and quantile estimates.\n\nMin, max, and the p-* quantiles are treated as optional due to the possibility of distribution operations, like subtraction.", + "type": "object", + "properties": { + "bins": { + "type": "array", + "items": { + "type": "number", + "format": "double" + } + }, + "counts": { + "type": "array", + "items": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "max": { + "nullable": true, + "type": "number", + "format": "double" + }, + "min": { + "nullable": true, + "type": "number", + "format": "double" + }, + "p50": { + "nullable": true, + "type": "number", + "format": "double" + }, + "p90": { + "nullable": true, + "type": "number", + "format": "double" + }, + "p99": { + "nullable": true, + "type": "number", + "format": "double" + }, + "squared_mean": { + "type": "number", + "format": "double" + }, + "sum_of_samples": { + "type": "number", + "format": "double" + } + }, + "required": [ + "bins", + "counts", + "squared_mean", + "sum_of_samples" + ] + }, + "Distributionint64": { + "description": "A distribution is a sequence of bins and counts in those bins, and some statistical information tracked to compute the mean, standard deviation, and quantile estimates.\n\nMin, max, and the p-* quantiles are treated as optional due to the possibility of distribution operations, like subtraction.", + "type": "object", + "properties": { + "bins": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "counts": { + "type": "array", + "items": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "max": { + "nullable": true, + "type": "integer", + "format": "int64" + }, + "min": { + "nullable": true, + "type": "integer", + "format": "int64" + }, + "p50": { + "nullable": true, + "type": "number", + "format": "double" + }, + "p90": { + "nullable": true, + "type": "number", + "format": "double" + }, + "p99": { + "nullable": true, + "type": "number", + "format": "double" + }, + "squared_mean": { + "type": "number", + "format": "double" + }, + "sum_of_samples": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "bins", + "counts", + "squared_mean", + "sum_of_samples" + ] + }, + "EphemeralIpCreate": { + "description": "Parameters for creating an ephemeral IP address for an instance.", + "type": "object", + "properties": { + "ip_version": { + "nullable": true, + "description": "IP version to use when allocating from the default pool. Only used when `pool` is not specified. Required if multiple default pools of different IP versions exist. Allocation fails if no pool of the requested version is available.", + "default": null, + "allOf": [ + { + "$ref": "#/components/schemas/IpVersion" + } + ] + }, + "pool": { + "nullable": true, + "description": "Name or ID of the IP pool used to allocate an address. If unspecified, the default IP pool will be used.", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + } + } + }, + "Error": { + "description": "Error information from a response.", + "type": "object", + "properties": { + "error_code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "request_id": { + "type": "string" + } + }, + "required": [ + "message", + "request_id" + ] + }, + "ExternalIp": { + "oneOf": [ + { + "description": "A source NAT IP address.\n\nSNAT addresses are ephemeral addresses used only for outbound connectivity.", + "type": "object", + "properties": { + "first_port": { + "description": "The first usable port within the IP address.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "ip": { + "description": "The IP address.", + "type": "string", + "format": "ip" + }, + "ip_pool_id": { + "description": "ID of the IP Pool from which the address is taken.", + "type": "string", + "format": "uuid" + }, + "kind": { + "type": "string", + "enum": [ + "snat" + ] + }, + "last_port": { + "description": "The last usable port within the IP address.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "first_port", + "ip", + "ip_pool_id", + "kind", + "last_port" + ] + }, + { + "type": "object", + "properties": { + "ip": { + "type": "string", + "format": "ip" + }, + "ip_pool_id": { + "type": "string", + "format": "uuid" + }, + "kind": { + "type": "string", + "enum": [ + "ephemeral" + ] + } + }, + "required": [ + "ip", + "ip_pool_id", + "kind" + ] + }, + { + "description": "A Floating IP is a well-known IP address which can be attached and detached from instances.", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "instance_id": { + "nullable": true, + "description": "The ID of the instance that this Floating IP is attached to, if it is presently in use.", + "type": "string", + "format": "uuid" + }, + "ip": { + "description": "The IP address held by this resource.", + "type": "string", + "format": "ip" + }, + "ip_pool_id": { + "description": "The ID of the IP pool this resource belongs to.", + "type": "string", + "format": "uuid" + }, + "kind": { + "type": "string", + "enum": [ + "floating" + ] + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "project_id": { + "description": "The project this resource exists within.", + "type": "string", + "format": "uuid" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "ip", + "ip_pool_id", + "kind", + "name", + "project_id", + "time_created", + "time_modified" + ] + } + ] + }, + "ExternalIpCreate": { + "description": "Parameters for creating an external IP address for instances.", + "oneOf": [ + { + "description": "An IP address providing both inbound and outbound access. The address is automatically assigned from the provided IP pool or the default IP pool if not specified.", + "type": "object", + "properties": { + "ip_version": { + "nullable": true, + "description": "IP version to use when allocating from the default pool. Only used when `pool` is not specified. Required if multiple default pools of different IP versions exist. Allocation fails if no pool of the requested version is available.", + "default": null, + "allOf": [ + { + "$ref": "#/components/schemas/IpVersion" + } + ] + }, + "pool": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "ephemeral" + ] + } + }, + "required": [ + "type" + ] + }, + { + "description": "An IP address providing both inbound and outbound access. The address is an existing floating IP object assigned to the current project.\n\nThe floating IP must not be in use by another instance or service.", + "type": "object", + "properties": { + "floating_ip": { + "$ref": "#/components/schemas/NameOrId" + }, + "type": { + "type": "string", + "enum": [ + "floating" + ] + } + }, + "required": [ + "floating_ip", + "type" + ] + } + ] + }, + "ExternalIpResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/ExternalIp" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "FailureDomain": { + "description": "Describes the scope of affinity for the purposes of co-location.", + "oneOf": [ + { + "description": "Instances are considered co-located if they are on the same sled", + "type": "string", + "enum": [ + "sled" + ] + } + ] + }, + "FieldSchema": { + "description": "The name and type information for a field of a timeseries schema.", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "field_type": { + "$ref": "#/components/schemas/FieldType" + }, + "name": { + "type": "string" + }, + "source": { + "$ref": "#/components/schemas/FieldSource" + } + }, + "required": [ + "description", + "field_type", + "name", + "source" + ] + }, + "FieldSource": { + "description": "The source from which a field is derived, the target or metric.", + "type": "string", + "enum": [ + "target", + "metric" + ] + }, + "FieldType": { + "description": "The `FieldType` identifies the data type of a target or metric field.", + "type": "string", + "enum": [ + "string", + "i8", + "u8", + "i16", + "u16", + "i32", + "u32", + "i64", + "u64", + "ip_addr", + "uuid", + "bool" + ] + }, + "FieldValue": { + "description": "The `FieldValue` contains the value of a target or metric field.", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "string" + ] + }, + "value": { + "type": "string" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "i8" + ] + }, + "value": { + "type": "integer", + "format": "int8" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "u8" + ] + }, + "value": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "i16" + ] + }, + "value": { + "type": "integer", + "format": "int16" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "u16" + ] + }, + "value": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "i32" + ] + }, + "value": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "u32" + ] + }, + "value": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "i64" + ] + }, + "value": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "u64" + ] + }, + "value": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ip_addr" + ] + }, + "value": { + "type": "string", + "format": "ip" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "uuid" + ] + }, + "value": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "bool" + ] + }, + "value": { + "type": "boolean" + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "FinalizeDisk": { + "description": "Parameters for finalizing a disk", + "type": "object", + "properties": { + "snapshot_name": { + "nullable": true, + "description": "If specified a snapshot of the disk will be created with the given name during finalization. If not specified, a snapshot for the disk will _not_ be created. A snapshot can be manually created once the disk transitions into the `Detached` state.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + } + }, + "FleetRole": { + "type": "string", + "enum": [ + "admin", + "collaborator", + "viewer" + ] + }, + "FleetRolePolicy": { + "description": "Policy for a particular resource\n\nNote that the Policy only describes access granted explicitly for this resource. The policies of parent resources can also cause a user to have access to this resource.", + "type": "object", + "properties": { + "role_assignments": { + "description": "Roles directly assigned on this resource", + "type": "array", + "items": { + "$ref": "#/components/schemas/FleetRoleRoleAssignment" + } + } + }, + "required": [ + "role_assignments" + ] + }, + "FleetRoleRoleAssignment": { + "description": "Describes the assignment of a particular role on a particular resource to a particular identity (user, group, etc.)\n\nThe resource is not part of this structure. Rather, `RoleAssignment`s are put into a `Policy` and that Policy is applied to a particular resource.", + "type": "object", + "properties": { + "identity_id": { + "type": "string", + "format": "uuid" + }, + "identity_type": { + "$ref": "#/components/schemas/IdentityType" + }, + "role_name": { + "$ref": "#/components/schemas/FleetRole" + } + }, + "required": [ + "identity_id", + "identity_type", + "role_name" + ] + }, + "FloatingIp": { + "description": "A Floating IP is a well-known IP address which can be attached and detached from instances.", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "instance_id": { + "nullable": true, + "description": "The ID of the instance that this Floating IP is attached to, if it is presently in use.", + "type": "string", + "format": "uuid" + }, + "ip": { + "description": "The IP address held by this resource.", + "type": "string", + "format": "ip" + }, + "ip_pool_id": { + "description": "The ID of the IP pool this resource belongs to.", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "project_id": { + "description": "The project this resource exists within.", + "type": "string", + "format": "uuid" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "ip", + "ip_pool_id", + "name", + "project_id", + "time_created", + "time_modified" + ] + }, + "FloatingIpAttach": { + "description": "Parameters for attaching a floating IP address to another resource", + "type": "object", + "properties": { + "kind": { + "description": "The type of `parent`'s resource", + "allOf": [ + { + "$ref": "#/components/schemas/FloatingIpParentKind" + } + ] + }, + "parent": { + "description": "Name or ID of the resource that this IP address should be attached to", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + } + }, + "required": [ + "kind", + "parent" + ] + }, + "FloatingIpCreate": { + "description": "Parameters for creating a new floating IP address for instances.", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "ip": { + "nullable": true, + "description": "An IP address to reserve for use as a floating IP. This field is optional: when not set, an address will be automatically chosen from `pool`. If set, then the IP must be available in the resolved `pool`.", + "type": "string", + "format": "ip" + }, + "ip_version": { + "nullable": true, + "description": "IP version to use when allocating from the default pool. Only used when both `ip` and `pool` are not specified. Required if multiple default pools of different IP versions exist. Allocation fails if no pool of the requested version is available.", + "default": null, + "allOf": [ + { + "$ref": "#/components/schemas/IpVersion" + } + ] + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "pool": { + "nullable": true, + "description": "The parent IP pool that a floating IP is pulled from. If unset, the default pool is selected.", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + } + }, + "required": [ + "description", + "name" + ] + }, + "FloatingIpParentKind": { + "description": "The type of resource that a floating IP is attached to", + "type": "string", + "enum": [ + "instance" + ] + }, + "FloatingIpResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/FloatingIp" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "FloatingIpUpdate": { + "description": "Updateable identity-related parameters", + "type": "object", + "properties": { + "description": { + "nullable": true, + "type": "string" + }, + "name": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + } + }, + "Group": { + "description": "View of a Group", + "type": "object", + "properties": { + "display_name": { + "description": "Human-readable name that can identify the group", + "type": "string" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "silo_id": { + "description": "Uuid of the silo to which this group belongs", + "type": "string", + "format": "uuid" + } + }, + "required": [ + "display_name", + "id", + "silo_id" + ] + }, + "GroupResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Group" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "Histogramdouble": { + "description": "Histogram metric\n\nA histogram maintains the count of any number of samples, over a set of bins. Bins are specified on construction via their _left_ edges, inclusive. There can't be any \"gaps\" in the bins, and an additional bin may be added to the left, right, or both so that the bins extend to the entire range of the support.\n\nNote that any gaps, unsorted bins, or non-finite values will result in an error.", + "type": "object", + "properties": { + "bins": { + "description": "The bins of the histogram.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Bindouble" + } + }, + "max": { + "description": "The maximum value of all samples in the histogram.", + "type": "number", + "format": "double" + }, + "min": { + "description": "The minimum value of all samples in the histogram.", + "type": "number", + "format": "double" + }, + "n_samples": { + "description": "The total number of samples in the histogram.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "p50": { + "description": "p50 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p90": { + "description": "p95 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p99": { + "description": "p99 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "squared_mean": { + "description": "M2 for Welford's algorithm for variance calculation.\n\nRead about [Welford's algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm) for more information on the algorithm.", + "type": "number", + "format": "double" + }, + "start_time": { + "description": "The start time of the histogram.", + "type": "string", + "format": "date-time" + }, + "sum_of_samples": { + "description": "The sum of all samples in the histogram.", + "type": "number", + "format": "double" + } + }, + "required": [ + "bins", + "max", + "min", + "n_samples", + "p50", + "p90", + "p99", + "squared_mean", + "start_time", + "sum_of_samples" + ] + }, + "Histogramfloat": { + "description": "Histogram metric\n\nA histogram maintains the count of any number of samples, over a set of bins. Bins are specified on construction via their _left_ edges, inclusive. There can't be any \"gaps\" in the bins, and an additional bin may be added to the left, right, or both so that the bins extend to the entire range of the support.\n\nNote that any gaps, unsorted bins, or non-finite values will result in an error.", + "type": "object", + "properties": { + "bins": { + "description": "The bins of the histogram.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Binfloat" + } + }, + "max": { + "description": "The maximum value of all samples in the histogram.", + "type": "number", + "format": "float" + }, + "min": { + "description": "The minimum value of all samples in the histogram.", + "type": "number", + "format": "float" + }, + "n_samples": { + "description": "The total number of samples in the histogram.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "p50": { + "description": "p50 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p90": { + "description": "p95 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p99": { + "description": "p99 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "squared_mean": { + "description": "M2 for Welford's algorithm for variance calculation.\n\nRead about [Welford's algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm) for more information on the algorithm.", + "type": "number", + "format": "double" + }, + "start_time": { + "description": "The start time of the histogram.", + "type": "string", + "format": "date-time" + }, + "sum_of_samples": { + "description": "The sum of all samples in the histogram.", + "type": "number", + "format": "double" + } + }, + "required": [ + "bins", + "max", + "min", + "n_samples", + "p50", + "p90", + "p99", + "squared_mean", + "start_time", + "sum_of_samples" + ] + }, + "Histogramint16": { + "description": "Histogram metric\n\nA histogram maintains the count of any number of samples, over a set of bins. Bins are specified on construction via their _left_ edges, inclusive. There can't be any \"gaps\" in the bins, and an additional bin may be added to the left, right, or both so that the bins extend to the entire range of the support.\n\nNote that any gaps, unsorted bins, or non-finite values will result in an error.", + "type": "object", + "properties": { + "bins": { + "description": "The bins of the histogram.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Binint16" + } + }, + "max": { + "description": "The maximum value of all samples in the histogram.", + "type": "integer", + "format": "int16" + }, + "min": { + "description": "The minimum value of all samples in the histogram.", + "type": "integer", + "format": "int16" + }, + "n_samples": { + "description": "The total number of samples in the histogram.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "p50": { + "description": "p50 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p90": { + "description": "p95 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p99": { + "description": "p99 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "squared_mean": { + "description": "M2 for Welford's algorithm for variance calculation.\n\nRead about [Welford's algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm) for more information on the algorithm.", + "type": "number", + "format": "double" + }, + "start_time": { + "description": "The start time of the histogram.", + "type": "string", + "format": "date-time" + }, + "sum_of_samples": { + "description": "The sum of all samples in the histogram.", + "type": "integer", + "format": "int64" + } + }, + "required": [ + "bins", + "max", + "min", + "n_samples", + "p50", + "p90", + "p99", + "squared_mean", + "start_time", + "sum_of_samples" + ] + }, + "Histogramint32": { + "description": "Histogram metric\n\nA histogram maintains the count of any number of samples, over a set of bins. Bins are specified on construction via their _left_ edges, inclusive. There can't be any \"gaps\" in the bins, and an additional bin may be added to the left, right, or both so that the bins extend to the entire range of the support.\n\nNote that any gaps, unsorted bins, or non-finite values will result in an error.", + "type": "object", + "properties": { + "bins": { + "description": "The bins of the histogram.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Binint32" + } + }, + "max": { + "description": "The maximum value of all samples in the histogram.", + "type": "integer", + "format": "int32" + }, + "min": { + "description": "The minimum value of all samples in the histogram.", + "type": "integer", + "format": "int32" + }, + "n_samples": { + "description": "The total number of samples in the histogram.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "p50": { + "description": "p50 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p90": { + "description": "p95 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p99": { + "description": "p99 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "squared_mean": { + "description": "M2 for Welford's algorithm for variance calculation.\n\nRead about [Welford's algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm) for more information on the algorithm.", + "type": "number", + "format": "double" + }, + "start_time": { + "description": "The start time of the histogram.", + "type": "string", + "format": "date-time" + }, + "sum_of_samples": { + "description": "The sum of all samples in the histogram.", + "type": "integer", + "format": "int64" + } + }, + "required": [ + "bins", + "max", + "min", + "n_samples", + "p50", + "p90", + "p99", + "squared_mean", + "start_time", + "sum_of_samples" + ] + }, + "Histogramint64": { + "description": "Histogram metric\n\nA histogram maintains the count of any number of samples, over a set of bins. Bins are specified on construction via their _left_ edges, inclusive. There can't be any \"gaps\" in the bins, and an additional bin may be added to the left, right, or both so that the bins extend to the entire range of the support.\n\nNote that any gaps, unsorted bins, or non-finite values will result in an error.", + "type": "object", + "properties": { + "bins": { + "description": "The bins of the histogram.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Binint64" + } + }, + "max": { + "description": "The maximum value of all samples in the histogram.", + "type": "integer", + "format": "int64" + }, + "min": { + "description": "The minimum value of all samples in the histogram.", + "type": "integer", + "format": "int64" + }, + "n_samples": { + "description": "The total number of samples in the histogram.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "p50": { + "description": "p50 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p90": { + "description": "p95 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p99": { + "description": "p99 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "squared_mean": { + "description": "M2 for Welford's algorithm for variance calculation.\n\nRead about [Welford's algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm) for more information on the algorithm.", + "type": "number", + "format": "double" + }, + "start_time": { + "description": "The start time of the histogram.", + "type": "string", + "format": "date-time" + }, + "sum_of_samples": { + "description": "The sum of all samples in the histogram.", + "type": "integer", + "format": "int64" + } + }, + "required": [ + "bins", + "max", + "min", + "n_samples", + "p50", + "p90", + "p99", + "squared_mean", + "start_time", + "sum_of_samples" + ] + }, + "Histogramint8": { + "description": "Histogram metric\n\nA histogram maintains the count of any number of samples, over a set of bins. Bins are specified on construction via their _left_ edges, inclusive. There can't be any \"gaps\" in the bins, and an additional bin may be added to the left, right, or both so that the bins extend to the entire range of the support.\n\nNote that any gaps, unsorted bins, or non-finite values will result in an error.", + "type": "object", + "properties": { + "bins": { + "description": "The bins of the histogram.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Binint8" + } + }, + "max": { + "description": "The maximum value of all samples in the histogram.", + "type": "integer", + "format": "int8" + }, + "min": { + "description": "The minimum value of all samples in the histogram.", + "type": "integer", + "format": "int8" + }, + "n_samples": { + "description": "The total number of samples in the histogram.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "p50": { + "description": "p50 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p90": { + "description": "p95 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p99": { + "description": "p99 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "squared_mean": { + "description": "M2 for Welford's algorithm for variance calculation.\n\nRead about [Welford's algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm) for more information on the algorithm.", + "type": "number", + "format": "double" + }, + "start_time": { + "description": "The start time of the histogram.", + "type": "string", + "format": "date-time" + }, + "sum_of_samples": { + "description": "The sum of all samples in the histogram.", + "type": "integer", + "format": "int64" + } + }, + "required": [ + "bins", + "max", + "min", + "n_samples", + "p50", + "p90", + "p99", + "squared_mean", + "start_time", + "sum_of_samples" + ] + }, + "Histogramuint16": { + "description": "Histogram metric\n\nA histogram maintains the count of any number of samples, over a set of bins. Bins are specified on construction via their _left_ edges, inclusive. There can't be any \"gaps\" in the bins, and an additional bin may be added to the left, right, or both so that the bins extend to the entire range of the support.\n\nNote that any gaps, unsorted bins, or non-finite values will result in an error.", + "type": "object", + "properties": { + "bins": { + "description": "The bins of the histogram.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Binuint16" + } + }, + "max": { + "description": "The maximum value of all samples in the histogram.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "min": { + "description": "The minimum value of all samples in the histogram.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "n_samples": { + "description": "The total number of samples in the histogram.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "p50": { + "description": "p50 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p90": { + "description": "p95 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p99": { + "description": "p99 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "squared_mean": { + "description": "M2 for Welford's algorithm for variance calculation.\n\nRead about [Welford's algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm) for more information on the algorithm.", + "type": "number", + "format": "double" + }, + "start_time": { + "description": "The start time of the histogram.", + "type": "string", + "format": "date-time" + }, + "sum_of_samples": { + "description": "The sum of all samples in the histogram.", + "type": "integer", + "format": "int64" + } + }, + "required": [ + "bins", + "max", + "min", + "n_samples", + "p50", + "p90", + "p99", + "squared_mean", + "start_time", + "sum_of_samples" + ] + }, + "Histogramuint32": { + "description": "Histogram metric\n\nA histogram maintains the count of any number of samples, over a set of bins. Bins are specified on construction via their _left_ edges, inclusive. There can't be any \"gaps\" in the bins, and an additional bin may be added to the left, right, or both so that the bins extend to the entire range of the support.\n\nNote that any gaps, unsorted bins, or non-finite values will result in an error.", + "type": "object", + "properties": { + "bins": { + "description": "The bins of the histogram.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Binuint32" + } + }, + "max": { + "description": "The maximum value of all samples in the histogram.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "min": { + "description": "The minimum value of all samples in the histogram.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "n_samples": { + "description": "The total number of samples in the histogram.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "p50": { + "description": "p50 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p90": { + "description": "p95 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p99": { + "description": "p99 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "squared_mean": { + "description": "M2 for Welford's algorithm for variance calculation.\n\nRead about [Welford's algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm) for more information on the algorithm.", + "type": "number", + "format": "double" + }, + "start_time": { + "description": "The start time of the histogram.", + "type": "string", + "format": "date-time" + }, + "sum_of_samples": { + "description": "The sum of all samples in the histogram.", + "type": "integer", + "format": "int64" + } + }, + "required": [ + "bins", + "max", + "min", + "n_samples", + "p50", + "p90", + "p99", + "squared_mean", + "start_time", + "sum_of_samples" + ] + }, + "Histogramuint64": { + "description": "Histogram metric\n\nA histogram maintains the count of any number of samples, over a set of bins. Bins are specified on construction via their _left_ edges, inclusive. There can't be any \"gaps\" in the bins, and an additional bin may be added to the left, right, or both so that the bins extend to the entire range of the support.\n\nNote that any gaps, unsorted bins, or non-finite values will result in an error.", + "type": "object", + "properties": { + "bins": { + "description": "The bins of the histogram.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Binuint64" + } + }, + "max": { + "description": "The maximum value of all samples in the histogram.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "min": { + "description": "The minimum value of all samples in the histogram.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "n_samples": { + "description": "The total number of samples in the histogram.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "p50": { + "description": "p50 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p90": { + "description": "p95 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p99": { + "description": "p99 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "squared_mean": { + "description": "M2 for Welford's algorithm for variance calculation.\n\nRead about [Welford's algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm) for more information on the algorithm.", + "type": "number", + "format": "double" + }, + "start_time": { + "description": "The start time of the histogram.", + "type": "string", + "format": "date-time" + }, + "sum_of_samples": { + "description": "The sum of all samples in the histogram.", + "type": "integer", + "format": "int64" + } + }, + "required": [ + "bins", + "max", + "min", + "n_samples", + "p50", + "p90", + "p99", + "squared_mean", + "start_time", + "sum_of_samples" + ] + }, + "Histogramuint8": { + "description": "Histogram metric\n\nA histogram maintains the count of any number of samples, over a set of bins. Bins are specified on construction via their _left_ edges, inclusive. There can't be any \"gaps\" in the bins, and an additional bin may be added to the left, right, or both so that the bins extend to the entire range of the support.\n\nNote that any gaps, unsorted bins, or non-finite values will result in an error.", + "type": "object", + "properties": { + "bins": { + "description": "The bins of the histogram.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Binuint8" + } + }, + "max": { + "description": "The maximum value of all samples in the histogram.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "min": { + "description": "The minimum value of all samples in the histogram.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "n_samples": { + "description": "The total number of samples in the histogram.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "p50": { + "description": "p50 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p90": { + "description": "p95 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p99": { + "description": "p99 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "squared_mean": { + "description": "M2 for Welford's algorithm for variance calculation.\n\nRead about [Welford's algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm) for more information on the algorithm.", + "type": "number", + "format": "double" + }, + "start_time": { + "description": "The start time of the histogram.", + "type": "string", + "format": "date-time" + }, + "sum_of_samples": { + "description": "The sum of all samples in the histogram.", + "type": "integer", + "format": "int64" + } + }, + "required": [ + "bins", + "max", + "min", + "n_samples", + "p50", + "p90", + "p99", + "squared_mean", + "start_time", + "sum_of_samples" + ] + }, + "Hostname": { + "title": "An RFC-1035-compliant hostname", + "description": "A hostname identifies a host on a network, and is usually a dot-delimited sequence of labels, where each label contains only letters, digits, or the hyphen. See RFCs 1035 and 952 for more details.", + "type": "string", + "pattern": "^([a-zA-Z0-9]+[a-zA-Z0-9\\-]*(? 2**53 addresses), integer precision will be lost, in exchange for representing the entire range. In such a case the pool still has many available addresses.", + "type": "object", + "properties": { + "capacity": { + "description": "The total number of addresses in the pool.", + "type": "number", + "format": "double" + }, + "remaining": { + "description": "The number of remaining addresses in the pool.", + "type": "number", + "format": "double" + } + }, + "required": [ + "capacity", + "remaining" + ] + }, + "IpRange": { + "oneOf": [ + { + "title": "v4", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4Range" + } + ] + }, + { + "title": "v6", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Range" + } + ] + } + ] + }, + "IpVersion": { + "description": "The IP address version.", + "type": "string", + "enum": [ + "v4", + "v6" + ] + }, + "Ipv4Assignment": { + "description": "How a VPC-private IP address is assigned to a network interface.", + "oneOf": [ + { + "description": "Automatically assign an IP address from the VPC Subnet.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "auto" + ] + } + }, + "required": [ + "type" + ] + }, + { + "description": "Explicitly assign a specific address, if available.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "explicit" + ] + }, + "value": { + "type": "string", + "format": "ipv4" + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "Ipv4Net": { + "example": "192.168.1.0/24", + "title": "An IPv4 subnet", + "description": "An IPv4 subnet, including prefix and prefix length", + "x-rust-type": { + "crate": "oxnet", + "path": "oxnet::Ipv4Net", + "version": "0.1.0" + }, + "type": "string", + "pattern": "^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])/([0-9]|1[0-9]|2[0-9]|3[0-2])$" + }, + "Ipv4Range": { + "description": "A non-decreasing IPv4 address range, inclusive of both ends.\n\nThe first address must be less than or equal to the last address.", + "type": "object", + "properties": { + "first": { + "type": "string", + "format": "ipv4" + }, + "last": { + "type": "string", + "format": "ipv4" + } + }, + "required": [ + "first", + "last" + ] + }, + "Ipv6Assignment": { + "description": "How a VPC-private IP address is assigned to a network interface.", + "oneOf": [ + { + "description": "Automatically assign an IP address from the VPC Subnet.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "auto" + ] + } + }, + "required": [ + "type" + ] + }, + { + "description": "Explicitly assign a specific address, if available.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "explicit" + ] + }, + "value": { + "type": "string", + "format": "ipv6" + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "Ipv6Net": { + "example": "fd12:3456::/64", + "title": "An IPv6 subnet", + "description": "An IPv6 subnet, including prefix and subnet mask", + "x-rust-type": { + "crate": "oxnet", + "path": "oxnet::Ipv6Net", + "version": "0.1.0" + }, + "type": "string", + "pattern": "^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$" + }, + "Ipv6Range": { + "description": "A non-decreasing IPv6 address range, inclusive of both ends.\n\nThe first address must be less than or equal to the last address.", + "type": "object", + "properties": { + "first": { + "type": "string", + "format": "ipv6" + }, + "last": { + "type": "string", + "format": "ipv6" + } + }, + "required": [ + "first", + "last" + ] + }, + "L4PortRange": { + "example": "22", + "title": "A range of IP ports", + "description": "An inclusive-inclusive range of IP ports. The second port may be omitted to represent a single port.", + "type": "string", + "pattern": "^[0-9]{1,5}(-[0-9]{1,5})?$", + "minLength": 1, + "maxLength": 11 + }, + "LinkConfigCreate": { + "description": "Switch link configuration.", + "type": "object", + "properties": { + "autoneg": { + "description": "Whether or not to set autonegotiation.", + "type": "boolean" + }, + "fec": { + "nullable": true, + "description": "The requested forward-error correction method. If this is not specified, the standard FEC for the underlying media will be applied if it can be determined.", + "allOf": [ + { + "$ref": "#/components/schemas/LinkFec" + } + ] + }, + "link_name": { + "description": "Link name. On ports that are not broken out, this is always phy0. On a 2x breakout the options are phy0 and phy1, on 4x phy0-phy3, etc.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "lldp": { + "description": "The link-layer discovery protocol (LLDP) configuration for the link.", + "allOf": [ + { + "$ref": "#/components/schemas/LldpLinkConfigCreate" + } + ] + }, + "mtu": { + "description": "Maximum transmission unit for the link.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "speed": { + "description": "The speed of the link.", + "allOf": [ + { + "$ref": "#/components/schemas/LinkSpeed" + } + ] + }, + "tx_eq": { + "nullable": true, + "description": "Optional tx_eq settings.", + "allOf": [ + { + "$ref": "#/components/schemas/TxEqConfig" + } + ] + } + }, + "required": [ + "autoneg", + "link_name", + "lldp", + "mtu", + "speed" + ] + }, + "LinkFec": { + "description": "The forward error correction mode of a link.", + "oneOf": [ + { + "description": "Firecode forward error correction.", + "type": "string", + "enum": [ + "firecode" + ] + }, + { + "description": "No forward error correction.", + "type": "string", + "enum": [ + "none" + ] + }, + { + "description": "Reed-Solomon forward error correction.", + "type": "string", + "enum": [ + "rs" + ] + } + ] + }, + "LinkSpeed": { + "description": "The speed of a link.", + "oneOf": [ + { + "description": "Zero gigabits per second.", + "type": "string", + "enum": [ + "speed0_g" + ] + }, + { + "description": "1 gigabit per second.", + "type": "string", + "enum": [ + "speed1_g" + ] + }, + { + "description": "10 gigabits per second.", + "type": "string", + "enum": [ + "speed10_g" + ] + }, + { + "description": "25 gigabits per second.", + "type": "string", + "enum": [ + "speed25_g" + ] + }, + { + "description": "40 gigabits per second.", + "type": "string", + "enum": [ + "speed40_g" + ] + }, + { + "description": "50 gigabits per second.", + "type": "string", + "enum": [ + "speed50_g" + ] + }, + { + "description": "100 gigabits per second.", + "type": "string", + "enum": [ + "speed100_g" + ] + }, + { + "description": "200 gigabits per second.", + "type": "string", + "enum": [ + "speed200_g" + ] + }, + { + "description": "400 gigabits per second.", + "type": "string", + "enum": [ + "speed400_g" + ] + } + ] + }, + "LldpLinkConfig": { + "description": "A link layer discovery protocol (LLDP) service configuration.", + "type": "object", + "properties": { + "chassis_id": { + "nullable": true, + "description": "The LLDP chassis identifier TLV.", + "type": "string" + }, + "enabled": { + "description": "Whether or not the LLDP service is enabled.", + "type": "boolean" + }, + "id": { + "description": "The id of this LLDP service instance.", + "type": "string", + "format": "uuid" + }, + "link_description": { + "nullable": true, + "description": "The LLDP link description TLV.", + "type": "string" + }, + "link_name": { + "nullable": true, + "description": "The LLDP link name TLV.", + "type": "string" + }, + "management_ip": { + "nullable": true, + "description": "The LLDP management IP TLV.", + "type": "string", + "format": "ip" + }, + "system_description": { + "nullable": true, + "description": "The LLDP system description TLV.", + "type": "string" + }, + "system_name": { + "nullable": true, + "description": "The LLDP system name TLV.", + "type": "string" + } + }, + "required": [ + "enabled", + "id" + ] + }, + "LldpLinkConfigCreate": { + "description": "The LLDP configuration associated with a port.", + "type": "object", + "properties": { + "chassis_id": { + "nullable": true, + "description": "The LLDP chassis identifier TLV.", + "type": "string" + }, + "enabled": { + "description": "Whether or not LLDP is enabled.", + "type": "boolean" + }, + "link_description": { + "nullable": true, + "description": "The LLDP link description TLV.", + "type": "string" + }, + "link_name": { + "nullable": true, + "description": "The LLDP link name TLV.", + "type": "string" + }, + "management_ip": { + "nullable": true, + "description": "The LLDP management IP TLV.", + "type": "string", + "format": "ip" + }, + "system_description": { + "nullable": true, + "description": "The LLDP system description TLV.", + "type": "string" + }, + "system_name": { + "nullable": true, + "description": "The LLDP system name TLV.", + "type": "string" + } + }, + "required": [ + "enabled" + ] + }, + "LldpNeighbor": { + "description": "Information about LLDP advertisements from other network entities directly connected to a switch port. This structure contains both metadata about when and where the neighbor was seen, as well as the specific information the neighbor was advertising.", + "type": "object", + "properties": { + "chassis_id": { + "description": "The LLDP chassis identifier advertised by the neighbor", + "type": "string" + }, + "first_seen": { + "description": "Initial sighting of this LldpNeighbor", + "type": "string", + "format": "date-time" + }, + "last_seen": { + "description": "Most recent sighting of this LldpNeighbor", + "type": "string", + "format": "date-time" + }, + "link_description": { + "nullable": true, + "description": "The LLDP link description advertised by the neighbor", + "type": "string" + }, + "link_name": { + "description": "The LLDP link name advertised by the neighbor", + "type": "string" + }, + "local_port": { + "description": "The port on which the neighbor was seen", + "type": "string" + }, + "management_ip": { + "description": "The LLDP management IP(s) advertised by the neighbor", + "type": "array", + "items": { + "$ref": "#/components/schemas/ManagementAddress" + } + }, + "system_description": { + "nullable": true, + "description": "The LLDP system description advertised by the neighbor", + "type": "string" + }, + "system_name": { + "nullable": true, + "description": "The LLDP system name advertised by the neighbor", + "type": "string" + } + }, + "required": [ + "chassis_id", + "first_seen", + "last_seen", + "link_name", + "local_port", + "management_ip" + ] + }, + "LldpNeighborResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/LldpNeighbor" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "LoopbackAddress": { + "description": "A loopback address is an address that is assigned to a rack switch but is not associated with any particular port.", + "type": "object", + "properties": { + "address": { + "description": "The loopback IP address and prefix length.", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + }, + "address_lot_block_id": { + "description": "The address lot block this address came from.", + "type": "string", + "format": "uuid" + }, + "id": { + "description": "The id of the loopback address.", + "type": "string", + "format": "uuid" + }, + "rack_id": { + "description": "The id of the rack where this loopback address is assigned.", + "type": "string", + "format": "uuid" + }, + "switch_location": { + "description": "Switch location where this loopback address is assigned.", + "type": "string" + } + }, + "required": [ + "address", + "address_lot_block_id", + "id", + "rack_id", + "switch_location" + ] + }, + "LoopbackAddressCreate": { + "description": "Parameters for creating a loopback address on a particular rack switch.", + "type": "object", + "properties": { + "address": { + "description": "The address to create.", + "type": "string", + "format": "ip" + }, + "address_lot": { + "description": "The name or id of the address lot this loopback address will pull an address from.", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + }, + "anycast": { + "description": "Address is an anycast address. This allows the address to be assigned to multiple locations simultaneously.", + "type": "boolean" + }, + "mask": { + "description": "The subnet mask to use for the address.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "rack_id": { + "description": "The rack containing the switch this loopback address will be configured on.", + "type": "string", + "format": "uuid" + }, + "switch_location": { + "description": "The location of the switch within the rack this loopback address will be configured on.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + }, + "required": [ + "address", + "address_lot", + "anycast", + "mask", + "rack_id", + "switch_location" + ] + }, + "LoopbackAddressResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/LoopbackAddress" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "MacAddr": { + "example": "ff:ff:ff:ff:ff:ff", + "title": "A MAC address", + "description": "A Media Access Control address, in EUI-48 format", + "type": "string", + "pattern": "^([0-9a-fA-F]{0,2}:){5}[0-9a-fA-F]{0,2}$", + "minLength": 5, + "maxLength": 17 + }, + "ManagementAddress": { + "type": "object", + "properties": { + "addr": { + "$ref": "#/components/schemas/NetworkAddress" + }, + "interface_num": { + "$ref": "#/components/schemas/InterfaceNum" + }, + "oid": { + "nullable": true, + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + } + }, + "required": [ + "addr", + "interface_num" + ] + }, + "Measurement": { + "description": "A `Measurement` is a timestamped datum from a single metric", + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Datum" + }, + "timestamp": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "datum", + "timestamp" + ] + }, + "MeasurementResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Measurement" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "MetricType": { + "description": "The type of the metric itself, indicating what its values represent.", + "oneOf": [ + { + "description": "The value represents an instantaneous measurement in time.", + "type": "string", + "enum": [ + "gauge" + ] + }, + { + "description": "The value represents a difference between two points in time.", + "type": "string", + "enum": [ + "delta" + ] + }, + { + "description": "The value represents an accumulation between two points in time.", + "type": "string", + "enum": [ + "cumulative" + ] + } + ] + }, + "MissingDatum": { + "type": "object", + "properties": { + "datum_type": { + "$ref": "#/components/schemas/DatumType" + }, + "start_time": { + "nullable": true, + "type": "string", + "format": "date-time" + } + }, + "required": [ + "datum_type" + ] + }, + "MulticastGroup": { + "description": "View of a Multicast Group", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "ip_pool_id": { + "description": "The ID of the IP pool this resource belongs to.", + "type": "string", + "format": "uuid" + }, + "multicast_ip": { + "description": "The multicast IP address held by this resource.", + "type": "string", + "format": "ip" + }, + "mvlan": { + "nullable": true, + "description": "Multicast VLAN (MVLAN) for egress multicast traffic to upstream networks. None means no VLAN tagging on egress.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "source_ips": { + "description": "Source IP addresses for Source-Specific Multicast (SSM). Empty array means any source is allowed.", + "type": "array", + "items": { + "type": "string", + "format": "ip" + } + }, + "state": { + "description": "Current state of the multicast group.", + "type": "string" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "ip_pool_id", + "multicast_ip", + "name", + "source_ips", + "state", + "time_created", + "time_modified" + ] + }, + "MulticastGroupCreate": { + "description": "Create-time parameters for a multicast group.", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "multicast_ip": { + "nullable": true, + "description": "The multicast IP address to allocate. If None, one will be allocated from the default pool.", + "default": null, + "type": "string", + "format": "ip" + }, + "mvlan": { + "nullable": true, + "description": "Multicast VLAN (MVLAN) for egress multicast traffic to upstream networks. Tags packets leaving the rack to traverse VLAN-segmented upstream networks.\n\nValid range: 2-4094 (VLAN IDs 0-1 are reserved by IEEE 802.1Q standard).", + "default": null, + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "pool": { + "nullable": true, + "description": "Name or ID of the IP pool to allocate from. If None, uses the default multicast pool.", + "default": null, + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + }, + "source_ips": { + "nullable": true, + "description": "Source IP addresses for Source-Specific Multicast (SSM).\n\nNone uses default behavior (Any-Source Multicast). Empty list explicitly allows any source (Any-Source Multicast). Non-empty list restricts to specific sources (SSM).", + "default": null, + "type": "array", + "items": { + "type": "string", + "format": "ip" + } + } + }, + "required": [ + "description", + "name" + ] + }, + "MulticastGroupMember": { + "description": "View of a Multicast Group Member (instance belonging to a multicast group)", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "instance_id": { + "description": "The ID of the instance that is a member of this group.", + "type": "string", + "format": "uuid" + }, + "multicast_group_id": { + "description": "The ID of the multicast group this member belongs to.", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "state": { + "description": "Current state of the multicast group membership.", + "type": "string" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "instance_id", + "multicast_group_id", + "name", + "state", + "time_created", + "time_modified" + ] + }, + "MulticastGroupMemberAdd": { + "description": "Parameters for adding an instance to a multicast group.", + "type": "object", + "properties": { + "instance": { + "description": "Name or ID of the instance to add to the multicast group", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + } + }, + "required": [ + "instance" + ] + }, + "MulticastGroupMemberResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/MulticastGroupMember" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "MulticastGroupResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/MulticastGroup" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "MulticastGroupUpdate": { + "description": "Update-time parameters for a multicast group.", + "type": "object", + "properties": { + "description": { + "nullable": true, + "type": "string" + }, + "mvlan": { + "nullable": true, + "description": "Multicast VLAN (MVLAN) for egress multicast traffic to upstream networks. Set to null to clear the MVLAN. Valid range: 2-4094 when provided. Omit the field to leave mvlan unchanged.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "name": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "source_ips": { + "nullable": true, + "type": "array", + "items": { + "type": "string", + "format": "ip" + } + } + } + }, + "Name": { + "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, but they may contain a UUID. They can be at most 63 characters long.", + "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-zA-Z0-9-]*[a-zA-Z0-9]+)?$", + "minLength": 1, + "maxLength": 63 + }, + "NameOrId": { + "oneOf": [ + { + "title": "id", + "allOf": [ + { + "type": "string", + "format": "uuid" + } + ] + }, + { + "title": "name", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + ] + }, + "NetworkAddress": { + "oneOf": [ + { + "type": "object", + "properties": { + "ip_addr": { + "type": "string", + "format": "ip" + } + }, + "required": [ + "ip_addr" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "i_e_e_e802": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + } + }, + "required": [ + "i_e_e_e802" + ], + "additionalProperties": false + } + ] + }, + "NetworkInterface": { + "description": "Information required to construct a virtual network interface", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "ip_config": { + "$ref": "#/components/schemas/PrivateIpConfig" + }, + "kind": { + "$ref": "#/components/schemas/NetworkInterfaceKind" + }, + "mac": { + "$ref": "#/components/schemas/MacAddr" + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "primary": { + "type": "boolean" + }, + "slot": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "vni": { + "$ref": "#/components/schemas/Vni" + } + }, + "required": [ + "id", + "ip_config", + "kind", + "mac", + "name", + "primary", + "slot", + "vni" + ] + }, + "NetworkInterfaceKind": { + "description": "The type of network interface", + "oneOf": [ + { + "description": "A vNIC attached to a guest instance", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "enum": [ + "instance" + ] + } + }, + "required": [ + "id", + "type" + ] + }, + { + "description": "A vNIC associated with an internal service", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "enum": [ + "service" + ] + } + }, + "required": [ + "id", + "type" + ] + }, + { + "description": "A vNIC associated with a probe", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "enum": [ + "probe" + ] + } + }, + "required": [ + "id", + "type" + ] + } + ] + }, + "OxqlQueryResult": { + "description": "The result of a successful OxQL query.", + "type": "object", + "properties": { + "tables": { + "description": "Tables resulting from the query, each containing timeseries.", + "type": "array", + "items": { + "$ref": "#/components/schemas/OxqlTable" + } + } + }, + "required": [ + "tables" + ] + }, + "OxqlTable": { + "description": "A table represents one or more timeseries with the same schema.\n\nA table is the result of an OxQL query. It contains a name, usually the name of the timeseries schema from which the data is derived, and any number of timeseries, which contain the actual data.", + "type": "object", + "properties": { + "name": { + "description": "The name of the table.", + "type": "string" + }, + "timeseries": { + "description": "The set of timeseries in the table, ordered by key.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Timeseries" + } + } + }, + "required": [ + "name", + "timeseries" + ] + }, + "Password": { + "title": "A password used to authenticate a user", + "description": "Passwords may be subject to additional constraints.", + "type": "string", + "maxLength": 512 + }, + "PhysicalDisk": { + "description": "View of a Physical Disk\n\nPhysical disks reside in a particular sled and are used to store both Instance Disk data as well as internal metadata.", + "type": "object", + "properties": { + "form_factor": { + "$ref": "#/components/schemas/PhysicalDiskKind" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "model": { + "type": "string" + }, + "policy": { + "description": "The operator-defined policy for a physical disk.", + "allOf": [ + { + "$ref": "#/components/schemas/PhysicalDiskPolicy" + } + ] + }, + "serial": { + "type": "string" + }, + "sled_id": { + "nullable": true, + "description": "The sled to which this disk is attached, if any.", + "type": "string", + "format": "uuid" + }, + "state": { + "description": "The current state Nexus believes the disk to be in.", + "allOf": [ + { + "$ref": "#/components/schemas/PhysicalDiskState" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + }, + "vendor": { + "type": "string" + } + }, + "required": [ + "form_factor", + "id", + "model", + "policy", + "serial", + "state", + "time_created", + "time_modified", + "vendor" + ] + }, + "PhysicalDiskKind": { + "description": "Describes the form factor of physical disks.", + "type": "string", + "enum": [ + "m2", + "u2" + ] + }, + "PhysicalDiskPolicy": { + "description": "The operator-defined policy of a physical disk.", + "oneOf": [ + { + "description": "The operator has indicated that the disk is in-service.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "in_service" + ] + } + }, + "required": [ + "kind" + ] + }, + { + "description": "The operator has indicated that the disk has been permanently removed from service.\n\nThis is a terminal state: once a particular disk ID is expunged, it will never return to service. (The actual hardware may be reused, but it will be treated as a brand-new disk.)\n\nAn expunged disk is always non-provisionable.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "expunged" + ] + } + }, + "required": [ + "kind" + ] + } + ] + }, + "PhysicalDiskResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/PhysicalDisk" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "PhysicalDiskState": { + "description": "The current state of the disk, as determined by Nexus.", + "oneOf": [ + { + "description": "The disk is currently active, and has resources allocated on it.", + "type": "string", + "enum": [ + "active" + ] + }, + { + "description": "The disk has been permanently removed from service.\n\nThis is a terminal state: once a particular disk ID is decommissioned, it will never return to service. (The actual hardware may be reused, but it will be treated as a brand-new disk.)", + "type": "string", + "enum": [ + "decommissioned" + ] + } + ] + }, + "Ping": { + "type": "object", + "properties": { + "status": { + "description": "Whether the external API is reachable. Will always be Ok if the endpoint returns anything at all.", + "allOf": [ + { + "$ref": "#/components/schemas/PingStatus" + } + ] + } + }, + "required": [ + "status" + ] + }, + "PingStatus": { + "type": "string", + "enum": [ + "ok" + ] + }, + "Points": { + "description": "Timepoints and values for one timeseries.", + "type": "object", + "properties": { + "start_times": { + "nullable": true, + "type": "array", + "items": { + "type": "string", + "format": "date-time" + } + }, + "timestamps": { + "type": "array", + "items": { + "type": "string", + "format": "date-time" + } + }, + "values": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Values" + } + } + }, + "required": [ + "timestamps", + "values" + ] + }, + "PrivateIpConfig": { + "description": "VPC-private IP address configuration for a network interface.", + "oneOf": [ + { + "description": "The interface has only an IPv4 configuration.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "v4" + ] + }, + "value": { + "$ref": "#/components/schemas/PrivateIpv4Config" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The interface has only an IPv6 configuration.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "v6" + ] + }, + "value": { + "$ref": "#/components/schemas/PrivateIpv6Config" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The interface is dual-stack.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "dual_stack" + ] + }, + "value": { + "type": "object", + "properties": { + "v4": { + "description": "The interface's IPv4 configuration.", + "allOf": [ + { + "$ref": "#/components/schemas/PrivateIpv4Config" + } + ] + }, + "v6": { + "description": "The interface's IPv6 configuration.", + "allOf": [ + { + "$ref": "#/components/schemas/PrivateIpv6Config" + } + ] + } + }, + "required": [ + "v4", + "v6" + ] + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "PrivateIpStack": { + "description": "The VPC-private IP stack for a network interface.", + "oneOf": [ + { + "description": "The interface has only an IPv4 stack.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "v4" + ] + }, + "value": { + "$ref": "#/components/schemas/PrivateIpv4Stack" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The interface has only an IPv6 stack.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "v6" + ] + }, + "value": { + "$ref": "#/components/schemas/PrivateIpv6Stack" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The interface is dual-stack IPv4 and IPv6.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "dual_stack" + ] + }, + "value": { + "type": "object", + "properties": { + "v4": { + "$ref": "#/components/schemas/PrivateIpv4Stack" + }, + "v6": { + "$ref": "#/components/schemas/PrivateIpv6Stack" + } + }, + "required": [ + "v4", + "v6" + ] + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "PrivateIpStackCreate": { + "description": "Create parameters for a network interface's IP stack.", + "oneOf": [ + { + "description": "The interface has only an IPv4 stack.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "v4" + ] + }, + "value": { + "$ref": "#/components/schemas/PrivateIpv4StackCreate" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The interface has only an IPv6 stack.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "v6" + ] + }, + "value": { + "$ref": "#/components/schemas/PrivateIpv6StackCreate" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The interface has both an IPv4 and IPv6 stack.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "dual_stack" + ] + }, + "value": { + "type": "object", + "properties": { + "v4": { + "$ref": "#/components/schemas/PrivateIpv4StackCreate" + }, + "v6": { + "$ref": "#/components/schemas/PrivateIpv6StackCreate" + } + }, + "required": [ + "v4", + "v6" + ] + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "PrivateIpv4Config": { + "description": "VPC-private IPv4 configuration for a network interface.", + "type": "object", + "properties": { + "ip": { + "description": "VPC-private IP address.", + "type": "string", + "format": "ipv4" + }, + "subnet": { + "description": "The IP subnet.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4Net" + } + ] + }, + "transit_ips": { + "description": "Additional networks on which the interface can send / receive traffic.", + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv4Net" + } + } + }, + "required": [ + "ip", + "subnet" + ] + }, + "PrivateIpv4Stack": { + "description": "The VPC-private IPv4 stack for a network interface", + "type": "object", + "properties": { + "ip": { + "description": "The VPC-private IPv4 address for the interface.", + "type": "string", + "format": "ipv4" + }, + "transit_ips": { + "description": "A set of additional IPv4 networks that this interface may send and receive traffic on.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv4Net" + } + } + }, + "required": [ + "ip", + "transit_ips" + ] + }, + "PrivateIpv4StackCreate": { + "description": "Configuration for a network interface's IPv4 addressing.", + "type": "object", + "properties": { + "ip": { + "description": "The VPC-private address to assign to the interface.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4Assignment" + } + ] + }, + "transit_ips": { + "description": "Additional IP networks the interface can send / receive on.", + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv4Net" + } + } + }, + "required": [ + "ip" + ] + }, + "PrivateIpv6Config": { + "description": "VPC-private IPv6 configuration for a network interface.", + "type": "object", + "properties": { + "ip": { + "description": "VPC-private IP address.", + "type": "string", + "format": "ipv6" + }, + "subnet": { + "description": "The IP subnet.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Net" + } + ] + }, + "transit_ips": { + "description": "Additional networks on which the interface can send / receive traffic.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv6Net" + } + } + }, + "required": [ + "ip", + "subnet", + "transit_ips" + ] + }, + "PrivateIpv6Stack": { + "description": "The VPC-private IPv6 stack for a network interface", + "type": "object", + "properties": { + "ip": { + "description": "The VPC-private IPv6 address for the interface.", + "type": "string", + "format": "ipv6" + }, + "transit_ips": { + "description": "A set of additional IPv6 networks that this interface may send and receive traffic on.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv6Net" + } + } + }, + "required": [ + "ip", + "transit_ips" + ] + }, + "PrivateIpv6StackCreate": { + "description": "Configuration for a network interface's IPv6 addressing.", + "type": "object", + "properties": { + "ip": { + "description": "The VPC-private address to assign to the interface.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Assignment" + } + ] + }, + "transit_ips": { + "description": "Additional IP networks the interface can send / receive on.", + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv6Net" + } + } + }, + "required": [ + "ip" + ] + }, + "Probe": { + "description": "Identity-related metadata that's included in nearly all public API objects", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "sled": { + "type": "string", + "format": "uuid" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "name", + "sled", + "time_created", + "time_modified" + ] + }, + "ProbeCreate": { + "description": "Create time parameters for probes.", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "ip_pool": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "sled": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "description", + "name", + "sled" + ] + }, + "ProbeExternalIp": { + "type": "object", + "properties": { + "first_port": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "ip": { + "type": "string", + "format": "ip" + }, + "kind": { + "$ref": "#/components/schemas/ProbeExternalIpKind" + }, + "last_port": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "first_port", + "ip", + "kind", + "last_port" + ] + }, + "ProbeExternalIpKind": { + "type": "string", + "enum": [ + "snat", + "floating", + "ephemeral" + ] + }, + "ProbeInfo": { + "type": "object", + "properties": { + "external_ips": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProbeExternalIp" + } + }, + "id": { + "type": "string", + "format": "uuid" + }, + "interface": { + "$ref": "#/components/schemas/NetworkInterface" + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "sled": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "external_ips", + "id", + "interface", + "name", + "sled" + ] + }, + "ProbeInfoResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/ProbeInfo" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "Project": { + "description": "View of a Project", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "name", + "time_created", + "time_modified" + ] + }, + "ProjectCreate": { + "description": "Create-time parameters for a `Project`", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "description", + "name" + ] + }, + "ProjectResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Project" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "ProjectRole": { + "type": "string", + "enum": [ + "admin", + "collaborator", + "limited_collaborator", + "viewer" + ] + }, + "ProjectRolePolicy": { + "description": "Policy for a particular resource\n\nNote that the Policy only describes access granted explicitly for this resource. The policies of parent resources can also cause a user to have access to this resource.", + "type": "object", + "properties": { + "role_assignments": { + "description": "Roles directly assigned on this resource", + "type": "array", + "items": { + "$ref": "#/components/schemas/ProjectRoleRoleAssignment" + } + } + }, + "required": [ + "role_assignments" + ] + }, + "ProjectRoleRoleAssignment": { + "description": "Describes the assignment of a particular role on a particular resource to a particular identity (user, group, etc.)\n\nThe resource is not part of this structure. Rather, `RoleAssignment`s are put into a `Policy` and that Policy is applied to a particular resource.", + "type": "object", + "properties": { + "identity_id": { + "type": "string", + "format": "uuid" + }, + "identity_type": { + "$ref": "#/components/schemas/IdentityType" + }, + "role_name": { + "$ref": "#/components/schemas/ProjectRole" + } + }, + "required": [ + "identity_id", + "identity_type", + "role_name" + ] + }, + "ProjectUpdate": { + "description": "Updateable properties of a `Project`", + "type": "object", + "properties": { + "description": { + "nullable": true, + "type": "string" + }, + "name": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + } + }, + "Quantile": { + "description": "Structure for estimating the p-quantile of a population.\n\nThis is based on the P² algorithm for estimating quantiles using constant space.\n\nThe algorithm consists of maintaining five markers: the minimum, the p/2-, p-, and (1 + p)/2 quantiles, and the maximum.", + "type": "object", + "properties": { + "desired_marker_positions": { + "description": "The desired marker positions.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "minItems": 5, + "maxItems": 5 + }, + "marker_heights": { + "description": "The heights of the markers.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "minItems": 5, + "maxItems": 5 + }, + "marker_positions": { + "description": "The positions of the markers.\n\nWe track sample size in the 5th position, as useful observations won't start until we've filled the heights at the 6th sample anyway This does deviate from the paper, but it's a more useful representation that works according to the paper's algorithm.", + "type": "array", + "items": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "minItems": 5, + "maxItems": 5 + }, + "p": { + "description": "The p value for the quantile.", + "type": "number", + "format": "double" + } + }, + "required": [ + "desired_marker_positions", + "marker_heights", + "marker_positions", + "p" + ] + }, + "Rack": { + "description": "View of an Rack", + "type": "object", + "properties": { + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "time_created", + "time_modified" + ] + }, + "RackResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Rack" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "Route": { + "description": "A route to a destination network through a gateway address.", + "type": "object", + "properties": { + "dst": { + "description": "The route destination.", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + }, + "gw": { + "description": "The route gateway.", + "type": "string", + "format": "ip" + }, + "rib_priority": { + "nullable": true, + "description": "Route RIB priority. Higher priority indicates precedence within and across protocols.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "vid": { + "nullable": true, + "description": "VLAN id the gateway is reachable over.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "dst", + "gw" + ] + }, + "RouteConfig": { + "description": "Route configuration data associated with a switch port configuration.", + "type": "object", + "properties": { + "link_name": { + "description": "Link name. On ports that are not broken out, this is always phy0. On a 2x breakout the options are phy0 and phy1, on 4x phy0-phy3, etc.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "routes": { + "description": "The set of routes assigned to a switch port.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Route" + } + } + }, + "required": [ + "link_name", + "routes" + ] + }, + "RouteDestination": { + "description": "A `RouteDestination` is used to match traffic with a routing rule based on the destination of that traffic.\n\nWhen traffic is to be sent to a destination that is within a given `RouteDestination`, the corresponding `RouterRoute` applies, and traffic will be forward to the `RouteTarget` for that rule.", + "oneOf": [ + { + "description": "Route applies to traffic destined for the specified IP address", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ip" + ] + }, + "value": { + "type": "string", + "format": "ip" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "Route applies to traffic destined for the specified IP subnet", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ip_net" + ] + }, + "value": { + "$ref": "#/components/schemas/IpNet" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "Route applies to traffic destined for the specified VPC", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "vpc" + ] + }, + "value": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "Route applies to traffic destined for the specified VPC subnet", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "subnet" + ] + }, + "value": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "RouteTarget": { + "description": "A `RouteTarget` describes the possible locations that traffic matching a route destination can be sent.", + "oneOf": [ + { + "description": "Forward traffic to a particular IP address.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ip" + ] + }, + "value": { + "type": "string", + "format": "ip" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "Forward traffic to a VPC", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "vpc" + ] + }, + "value": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "Forward traffic to a VPC Subnet", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "subnet" + ] + }, + "value": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "Forward traffic to a specific instance", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "instance" + ] + }, + "value": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "Forward traffic to an internet gateway", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "internet_gateway" + ] + }, + "value": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "Drop matching traffic", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "drop" + ] + } + }, + "required": [ + "type" + ] + } + ] + }, + "RouterRoute": { + "description": "A route defines a rule that governs where traffic should be sent based on its destination.", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "destination": { + "description": "Selects which traffic this routing rule will apply to", + "allOf": [ + { + "$ref": "#/components/schemas/RouteDestination" + } + ] + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "kind": { + "description": "Describes the kind of router. Set at creation. `read-only`", + "allOf": [ + { + "$ref": "#/components/schemas/RouterRouteKind" + } + ] + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "target": { + "description": "The location that matched packets should be forwarded to", + "allOf": [ + { + "$ref": "#/components/schemas/RouteTarget" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + }, + "vpc_router_id": { + "description": "The ID of the VPC Router to which the route belongs", + "type": "string", + "format": "uuid" + } + }, + "required": [ + "description", + "destination", + "id", + "kind", + "name", + "target", + "time_created", + "time_modified", + "vpc_router_id" + ] + }, + "RouterRouteCreate": { + "description": "Create-time parameters for a `RouterRoute`", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "destination": { + "description": "Selects which traffic this routing rule will apply to.", + "allOf": [ + { + "$ref": "#/components/schemas/RouteDestination" + } + ] + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "target": { + "description": "The location that matched packets should be forwarded to.", + "allOf": [ + { + "$ref": "#/components/schemas/RouteTarget" + } + ] + } + }, + "required": [ + "description", + "destination", + "name", + "target" + ] + }, + "RouterRouteKind": { + "description": "The kind of a `RouterRoute`\n\nThe kind determines certain attributes such as if the route is modifiable and describes how or where the route was created.", + "oneOf": [ + { + "description": "Determines the default destination of traffic, such as whether it goes to the internet or not.\n\n`Destination: An Internet Gateway` `Modifiable: true`", + "type": "string", + "enum": [ + "default" + ] + }, + { + "description": "Automatically added for each VPC Subnet in the VPC\n\n`Destination: A VPC Subnet` `Modifiable: false`", + "type": "string", + "enum": [ + "vpc_subnet" + ] + }, + { + "description": "Automatically added when VPC peering is established\n\n`Destination: A different VPC` `Modifiable: false`", + "type": "string", + "enum": [ + "vpc_peering" + ] + }, + { + "description": "Created by a user; see `RouteTarget`\n\n`Destination: User defined` `Modifiable: true`", + "type": "string", + "enum": [ + "custom" + ] + } + ] + }, + "RouterRouteResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/RouterRoute" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "RouterRouteUpdate": { + "description": "Updateable properties of a `RouterRoute`", + "type": "object", + "properties": { + "description": { + "nullable": true, + "type": "string" + }, + "destination": { + "description": "Selects which traffic this routing rule will apply to.", + "allOf": [ + { + "$ref": "#/components/schemas/RouteDestination" + } + ] + }, + "name": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "target": { + "description": "The location that matched packets should be forwarded to.", + "allOf": [ + { + "$ref": "#/components/schemas/RouteTarget" + } + ] + } + }, + "required": [ + "destination", + "target" + ] + }, + "SamlIdentityProvider": { + "description": "Identity-related metadata that's included in nearly all public API objects", + "type": "object", + "properties": { + "acs_url": { + "description": "Service provider endpoint where the response will be sent", + "type": "string" + }, + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "group_attribute_name": { + "nullable": true, + "description": "If set, attributes with this name will be considered to denote a user's group membership, where the values will be the group names.", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "idp_entity_id": { + "description": "IdP's entity id", + "type": "string" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "public_cert": { + "nullable": true, + "description": "Optional request signing public certificate (base64 encoded der file)", + "type": "string" + }, + "slo_url": { + "description": "Service provider endpoint where the idp should send log out requests", + "type": "string" + }, + "sp_client_id": { + "description": "SP's client id", + "type": "string" + }, + "technical_contact_email": { + "description": "Customer's technical contact for saml configuration", + "type": "string" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "acs_url", + "description", + "id", + "idp_entity_id", + "name", + "slo_url", + "sp_client_id", + "technical_contact_email", + "time_created", + "time_modified" + ] + }, + "SamlIdentityProviderCreate": { + "description": "Create-time identity-related parameters", + "type": "object", + "properties": { + "acs_url": { + "description": "service provider endpoint where the response will be sent", + "type": "string" + }, + "description": { + "type": "string" + }, + "group_attribute_name": { + "nullable": true, + "description": "If set, SAML attributes with this name will be considered to denote a user's group membership, where the attribute value(s) should be a comma-separated list of group names.", + "type": "string" + }, + "idp_entity_id": { + "description": "idp's entity id", + "type": "string" + }, + "idp_metadata_source": { + "description": "the source of an identity provider metadata descriptor", + "allOf": [ + { + "$ref": "#/components/schemas/IdpMetadataSource" + } + ] + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "signing_keypair": { + "nullable": true, + "description": "request signing key pair", + "default": null, + "allOf": [ + { + "$ref": "#/components/schemas/DerEncodedKeyPair" + } + ] + }, + "slo_url": { + "description": "service provider endpoint where the idp should send log out requests", + "type": "string" + }, + "sp_client_id": { + "description": "sp's client id", + "type": "string" + }, + "technical_contact_email": { + "description": "customer's technical contact for saml configuration", + "type": "string" + } + }, + "required": [ + "acs_url", + "description", + "idp_entity_id", + "idp_metadata_source", + "name", + "slo_url", + "sp_client_id", + "technical_contact_email" + ] + }, + "ScimClientBearerToken": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "time_created": { + "type": "string", + "format": "date-time" + }, + "time_expires": { + "nullable": true, + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "time_created" + ] + }, + "ScimClientBearerTokenValue": { + "description": "The POST response is the only time the generated bearer token is returned to the client.", + "type": "object", + "properties": { + "bearer_token": { + "type": "string" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "time_created": { + "type": "string", + "format": "date-time" + }, + "time_expires": { + "nullable": true, + "type": "string", + "format": "date-time" + } + }, + "required": [ + "bearer_token", + "id", + "time_created" + ] + }, + "ServiceIcmpConfig": { + "description": "Configuration of inbound ICMP allowed by API services.", + "type": "object", + "properties": { + "enabled": { + "description": "When enabled, Nexus is able to receive ICMP Destination Unreachable type 3 (port unreachable) and type 4 (fragmentation needed), Redirect, and Time Exceeded messages. These enable Nexus to perform Path MTU discovery and better cope with fragmentation issues. Otherwise all inbound ICMP traffic will be dropped.", + "type": "boolean" + } + }, + "required": [ + "enabled" + ] + }, + "ServiceUsingCertificate": { + "description": "The service intended to use this certificate.", + "oneOf": [ + { + "description": "This certificate is intended for access to the external API.", + "type": "string", + "enum": [ + "external_api" + ] + } + ] + }, + "SetTargetReleaseParams": { + "description": "Parameters for PUT requests to `/v1/system/update/target-release`.", + "type": "object", + "properties": { + "system_version": { + "description": "Version of the system software to make the target release.", + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + } + }, + "required": [ + "system_version" + ] + }, + "Silo": { + "description": "View of a Silo\n\nA Silo is the highest level unit of isolation.", + "type": "object", + "properties": { + "admin_group_name": { + "nullable": true, + "description": "Optionally, silos can have a group name that is automatically granted the silo admin role.", + "type": "string" + }, + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "discoverable": { + "description": "A silo where discoverable is false can be retrieved only by its id - it will not be part of the \"list all silos\" output.", + "type": "boolean" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "identity_mode": { + "description": "How users and groups are managed in this Silo", + "allOf": [ + { + "$ref": "#/components/schemas/SiloIdentityMode" + } + ] + }, + "mapped_fleet_roles": { + "description": "Mapping of which Fleet roles are conferred by each Silo role\n\nThe default is that no Fleet roles are conferred by any Silo roles unless there's a corresponding entry in this map.", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FleetRole" + }, + "uniqueItems": true + } + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "discoverable", + "id", + "identity_mode", + "mapped_fleet_roles", + "name", + "time_created", + "time_modified" + ] + }, + "SiloAuthSettings": { + "description": "View of silo authentication settings", + "type": "object", + "properties": { + "device_token_max_ttl_seconds": { + "nullable": true, + "description": "Maximum lifetime of a device token in seconds. If set to null, users will be able to create tokens that do not expire.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "silo_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "silo_id" + ] + }, + "SiloAuthSettingsUpdate": { + "description": "Updateable properties of a silo's settings.", + "type": "object", + "properties": { + "device_token_max_ttl_seconds": { + "nullable": true, + "description": "Maximum lifetime of a device token in seconds. If set to null, users will be able to create tokens that do not expire.", + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + "required": [ + "device_token_max_ttl_seconds" + ] + }, + "SiloCreate": { + "description": "Create-time parameters for a `Silo`", + "type": "object", + "properties": { + "admin_group_name": { + "nullable": true, + "description": "If set, this group will be created during Silo creation and granted the \"Silo Admin\" role. Identity providers can assert that users belong to this group and those users can log in and further initialize the Silo.\n\nNote that if configuring a SAML based identity provider, group_attribute_name must be set for users to be considered part of a group. See `SamlIdentityProviderCreate` for more information.", + "type": "string" + }, + "description": { + "type": "string" + }, + "discoverable": { + "type": "boolean" + }, + "identity_mode": { + "$ref": "#/components/schemas/SiloIdentityMode" + }, + "mapped_fleet_roles": { + "description": "Mapping of which Fleet roles are conferred by each Silo role\n\nThe default is that no Fleet roles are conferred by any Silo roles unless there's a corresponding entry in this map.", + "default": {}, + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FleetRole" + }, + "uniqueItems": true + } + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "quotas": { + "description": "Limits the amount of provisionable CPU, memory, and storage in the Silo. CPU and memory are only consumed by running instances, while storage is consumed by any disk or snapshot. A value of 0 means that resource is *not* provisionable.", + "allOf": [ + { + "$ref": "#/components/schemas/SiloQuotasCreate" + } + ] + }, + "tls_certificates": { + "description": "Initial TLS certificates to be used for the new Silo's console and API endpoints. These should be valid for the Silo's DNS name(s).", + "type": "array", + "items": { + "$ref": "#/components/schemas/CertificateCreate" + } + } + }, + "required": [ + "description", + "discoverable", + "identity_mode", + "name", + "quotas", + "tls_certificates" + ] + }, + "SiloIdentityMode": { + "description": "Describes how identities are managed and users are authenticated in this Silo", + "oneOf": [ + { + "description": "Users are authenticated with SAML using an external authentication provider. The system updates information about users and groups only during successful authentication (i.e,. \"JIT provisioning\" of users and groups).", + "type": "string", + "enum": [ + "saml_jit" + ] + }, + { + "description": "The system is the source of truth about users. There is no linkage to an external authentication provider or identity provider.", + "type": "string", + "enum": [ + "local_only" + ] + }, + { + "description": "Users are authenticated with SAML using an external authentication provider. Users and groups are managed with SCIM API calls, likely from the same authentication provider.", + "type": "string", + "enum": [ + "saml_scim" + ] + } + ] + }, + "SiloIpPool": { + "description": "An IP pool in the context of a silo", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "ip_version": { + "description": "The IP version for the pool.", + "allOf": [ + { + "$ref": "#/components/schemas/IpVersion" + } + ] + }, + "is_default": { + "description": "When a pool is the default for a silo, floating IPs and instance ephemeral IPs will come from that pool when no other pool is specified.\n\nA silo can have at most one default pool per combination of pool type (unicast or multicast) and IP version (IPv4 or IPv6), allowing up to 4 default pools total.", + "type": "boolean" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "pool_type": { + "description": "Type of IP pool (unicast or multicast).", + "allOf": [ + { + "$ref": "#/components/schemas/IpPoolType" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "ip_version", + "is_default", + "name", + "pool_type", + "time_created", + "time_modified" + ] + }, + "SiloIpPoolResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/SiloIpPool" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "SiloQuotas": { + "description": "A collection of resource counts used to set the virtual capacity of a silo", + "type": "object", + "properties": { + "cpus": { + "description": "Number of virtual CPUs", + "type": "integer", + "format": "int64" + }, + "memory": { + "description": "Amount of memory in bytes", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + }, + "silo_id": { + "type": "string", + "format": "uuid" + }, + "storage": { + "description": "Amount of disk storage in bytes", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + } + }, + "required": [ + "cpus", + "memory", + "silo_id", + "storage" + ] + }, + "SiloQuotasCreate": { + "description": "The amount of provisionable resources for a Silo", + "type": "object", + "properties": { + "cpus": { + "description": "The amount of virtual CPUs available for running instances in the Silo", + "type": "integer", + "format": "int64" + }, + "memory": { + "description": "The amount of RAM (in bytes) available for running instances in the Silo", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + }, + "storage": { + "description": "The amount of storage (in bytes) available for disks or snapshots", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + } + }, + "required": [ + "cpus", + "memory", + "storage" + ] + }, + "SiloQuotasResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/SiloQuotas" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "SiloQuotasUpdate": { + "description": "Updateable properties of a Silo's resource limits. If a value is omitted it will not be updated.", + "type": "object", + "properties": { + "cpus": { + "nullable": true, + "description": "The amount of virtual CPUs available for running instances in the Silo", + "type": "integer", + "format": "int64" + }, + "memory": { + "nullable": true, + "description": "The amount of RAM (in bytes) available for running instances in the Silo", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + }, + "storage": { + "nullable": true, + "description": "The amount of storage (in bytes) available for disks or snapshots", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + } + } + }, + "SiloResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Silo" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "SiloRole": { + "type": "string", + "enum": [ + "admin", + "collaborator", + "limited_collaborator", + "viewer" + ] + }, + "SiloRolePolicy": { + "description": "Policy for a particular resource\n\nNote that the Policy only describes access granted explicitly for this resource. The policies of parent resources can also cause a user to have access to this resource.", + "type": "object", + "properties": { + "role_assignments": { + "description": "Roles directly assigned on this resource", + "type": "array", + "items": { + "$ref": "#/components/schemas/SiloRoleRoleAssignment" + } + } + }, + "required": [ + "role_assignments" + ] + }, + "SiloRoleRoleAssignment": { + "description": "Describes the assignment of a particular role on a particular resource to a particular identity (user, group, etc.)\n\nThe resource is not part of this structure. Rather, `RoleAssignment`s are put into a `Policy` and that Policy is applied to a particular resource.", + "type": "object", + "properties": { + "identity_id": { + "type": "string", + "format": "uuid" + }, + "identity_type": { + "$ref": "#/components/schemas/IdentityType" + }, + "role_name": { + "$ref": "#/components/schemas/SiloRole" + } + }, + "required": [ + "identity_id", + "identity_type", + "role_name" + ] + }, + "SiloUtilization": { + "description": "View of a silo's resource utilization and capacity", + "type": "object", + "properties": { + "allocated": { + "description": "Accounts for the total amount of resources reserved for silos via their quotas", + "allOf": [ + { + "$ref": "#/components/schemas/VirtualResourceCounts" + } + ] + }, + "provisioned": { + "description": "Accounts for resources allocated by in silos like CPU or memory for running instances and storage for disks and snapshots Note that CPU and memory resources associated with a stopped instances are not counted here", + "allOf": [ + { + "$ref": "#/components/schemas/VirtualResourceCounts" + } + ] + }, + "silo_id": { + "type": "string", + "format": "uuid" + }, + "silo_name": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "allocated", + "provisioned", + "silo_id", + "silo_name" + ] + }, + "SiloUtilizationResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/SiloUtilization" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "Sled": { + "description": "An operator's view of a Sled.", + "type": "object", + "properties": { + "baseboard": { + "$ref": "#/components/schemas/Baseboard" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "policy": { + "description": "The operator-defined policy of a sled.", + "allOf": [ + { + "$ref": "#/components/schemas/SledPolicy" + } + ] + }, + "rack_id": { + "description": "The rack to which this Sled is currently attached", + "type": "string", + "format": "uuid" + }, + "state": { + "description": "The current state of the sled.", + "allOf": [ + { + "$ref": "#/components/schemas/SledState" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + }, + "usable_hardware_threads": { + "description": "The number of hardware threads which can execute on this sled", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "usable_physical_ram": { + "description": "Amount of RAM which may be used by the Sled's OS", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + } + }, + "required": [ + "baseboard", + "id", + "policy", + "rack_id", + "state", + "time_created", + "time_modified", + "usable_hardware_threads", + "usable_physical_ram" + ] + }, + "SledId": { + "description": "The unique ID of a sled.", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "id" + ] + }, + "SledInstance": { + "description": "An operator's view of an instance running on a given sled", + "type": "object", + "properties": { + "active_sled_id": { + "type": "string", + "format": "uuid" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "memory": { + "type": "integer", + "format": "int64" + }, + "migration_id": { + "nullable": true, + "type": "string", + "format": "uuid" + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "ncpus": { + "type": "integer", + "format": "int64" + }, + "project_name": { + "$ref": "#/components/schemas/Name" + }, + "silo_name": { + "$ref": "#/components/schemas/Name" + }, + "state": { + "$ref": "#/components/schemas/InstanceState" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "active_sled_id", + "id", + "memory", + "name", + "ncpus", + "project_name", + "silo_name", + "state", + "time_created", + "time_modified" + ] + }, + "SledInstanceResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/SledInstance" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "SledPolicy": { + "description": "The operator-defined policy of a sled.", + "oneOf": [ + { + "description": "The operator has indicated that the sled is in-service.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "in_service" + ] + }, + "provision_policy": { + "description": "Determines whether new resources can be provisioned onto the sled.", + "allOf": [ + { + "$ref": "#/components/schemas/SledProvisionPolicy" + } + ] + } + }, + "required": [ + "kind", + "provision_policy" + ] + }, + { + "description": "The operator has indicated that the sled has been permanently removed from service.\n\nThis is a terminal state: once a particular sled ID is expunged, it will never return to service. (The actual hardware may be reused, but it will be treated as a brand-new sled.)\n\nAn expunged sled is always non-provisionable.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "expunged" + ] + } + }, + "required": [ + "kind" + ] + } + ] + }, + "SledProvisionPolicy": { + "description": "The operator-defined provision policy of a sled.\n\nThis controls whether new resources are going to be provisioned on this sled.", + "oneOf": [ + { + "description": "New resources will be provisioned on this sled.", + "type": "string", + "enum": [ + "provisionable" + ] + }, + { + "description": "New resources will not be provisioned on this sled. However, if the sled is currently in service, existing resources will continue to be on this sled unless manually migrated off.", + "type": "string", + "enum": [ + "non_provisionable" + ] + } + ] + }, + "SledProvisionPolicyParams": { + "description": "Parameters for `sled_set_provision_policy`.", + "type": "object", + "properties": { + "state": { + "description": "The provision state.", + "allOf": [ + { + "$ref": "#/components/schemas/SledProvisionPolicy" + } + ] + } + }, + "required": [ + "state" + ] + }, + "SledProvisionPolicyResponse": { + "description": "Response to `sled_set_provision_policy`.", + "type": "object", + "properties": { + "new_state": { + "description": "The new provision state.", + "allOf": [ + { + "$ref": "#/components/schemas/SledProvisionPolicy" + } + ] + }, + "old_state": { + "description": "The old provision state.", + "allOf": [ + { + "$ref": "#/components/schemas/SledProvisionPolicy" + } + ] + } + }, + "required": [ + "new_state", + "old_state" + ] + }, + "SledResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Sled" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "SledState": { + "description": "The current state of the sled.", + "oneOf": [ + { + "description": "The sled is currently active, and has resources allocated on it.", + "type": "string", + "enum": [ + "active" + ] + }, + { + "description": "The sled has been permanently removed from service.\n\nThis is a terminal state: once a particular sled ID is decommissioned, it will never return to service. (The actual hardware may be reused, but it will be treated as a brand-new sled.)", + "type": "string", + "enum": [ + "decommissioned" + ] + } + ] + }, + "Snapshot": { + "description": "View of a Snapshot", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "disk_id": { + "type": "string", + "format": "uuid" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "project_id": { + "type": "string", + "format": "uuid" + }, + "size": { + "$ref": "#/components/schemas/ByteCount" + }, + "state": { + "$ref": "#/components/schemas/SnapshotState" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "disk_id", + "id", + "name", + "project_id", + "size", + "state", + "time_created", + "time_modified" + ] + }, + "SnapshotCreate": { + "description": "Create-time parameters for a `Snapshot`", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "disk": { + "description": "The disk to be snapshotted", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + }, + "name": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "description", + "disk", + "name" + ] + }, + "SnapshotResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Snapshot" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "SnapshotState": { + "type": "string", + "enum": [ + "creating", + "ready", + "faulted", + "destroyed" + ] + }, + "SshKey": { + "description": "View of an SSH Key", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "public_key": { + "description": "SSH public key, e.g., `\"ssh-ed25519 AAAAC3NzaC...\"`", + "type": "string" + }, + "silo_user_id": { + "description": "The user to whom this key belongs", + "type": "string", + "format": "uuid" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "name", + "public_key", + "silo_user_id", + "time_created", + "time_modified" + ] + }, + "SshKeyCreate": { + "description": "Create-time parameters for an `SshKey`", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "public_key": { + "description": "SSH public key, e.g., `\"ssh-ed25519 AAAAC3NzaC...\"`", + "type": "string" + } + }, + "required": [ + "description", + "name", + "public_key" + ] + }, + "SshKeyResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/SshKey" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "SupportBundleCreate": { + "type": "object", + "properties": { + "user_comment": { + "nullable": true, + "description": "User comment for the support bundle", + "type": "string" + } + } + }, + "SupportBundleInfo": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "reason_for_creation": { + "type": "string" + }, + "reason_for_failure": { + "nullable": true, + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/SupportBundleState" + }, + "time_created": { + "type": "string", + "format": "date-time" + }, + "user_comment": { + "nullable": true, + "type": "string" + } + }, + "required": [ + "id", + "reason_for_creation", + "state", + "time_created" + ] + }, + "SupportBundleInfoResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/SupportBundleInfo" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "SupportBundleState": { + "oneOf": [ + { + "description": "Support Bundle still actively being collected.\n\nThis is the initial state for a Support Bundle, and it will automatically transition to either \"Failing\" or \"Active\".\n\nIf a user no longer wants to access a Support Bundle, they can request cancellation, which will transition to the \"Destroying\" state.", + "type": "string", + "enum": [ + "collecting" + ] + }, + { + "description": "Support Bundle is being destroyed.\n\nOnce backing storage has been freed, this bundle is destroyed.", + "type": "string", + "enum": [ + "destroying" + ] + }, + { + "description": "Support Bundle was not created successfully, or was created and has lost backing storage.\n\nThe record of the bundle still exists for readability, but the only valid operation on these bundles is to destroy them.", + "type": "string", + "enum": [ + "failed" + ] + }, + { + "description": "Support Bundle has been processed, and is ready for usage.", + "type": "string", + "enum": [ + "active" + ] + } + ] + }, + "SupportBundleUpdate": { + "type": "object", + "properties": { + "user_comment": { + "nullable": true, + "description": "User comment for the support bundle", + "type": "string" + } + } + }, + "Switch": { + "description": "An operator's view of a Switch.", + "type": "object", + "properties": { + "baseboard": { + "$ref": "#/components/schemas/Baseboard" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "rack_id": { + "description": "The rack to which this Switch is currently attached", + "type": "string", + "format": "uuid" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "baseboard", + "id", + "rack_id", + "time_created", + "time_modified" + ] + }, + "SwitchBgpHistory": { + "description": "BGP message history for a particular switch.", + "type": "object", + "properties": { + "history": { + "description": "Message history indexed by peer address.", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/BgpMessageHistory" + } + }, + "switch": { + "description": "Switch this message history is associated with.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchLocation" + } + ] + } + }, + "required": [ + "history", + "switch" + ] + }, + "SwitchInterfaceConfig": { + "description": "A switch port interface configuration for a port settings object.", + "type": "object", + "properties": { + "id": { + "description": "A unique identifier for this switch interface.", + "type": "string", + "format": "uuid" + }, + "interface_name": { + "description": "The name of this switch interface.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "kind": { + "description": "The switch interface kind.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchInterfaceKind2" + } + ] + }, + "port_settings_id": { + "description": "The port settings object this switch interface configuration belongs to.", + "type": "string", + "format": "uuid" + }, + "v6_enabled": { + "description": "Whether or not IPv6 is enabled on this interface.", + "type": "boolean" + } + }, + "required": [ + "id", + "interface_name", + "kind", + "port_settings_id", + "v6_enabled" + ] + }, + "SwitchInterfaceConfigCreate": { + "description": "A layer-3 switch interface configuration. When IPv6 is enabled, a link local address will be created for the interface.", + "type": "object", + "properties": { + "kind": { + "description": "What kind of switch interface this configuration represents.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchInterfaceKind" + } + ] + }, + "link_name": { + "description": "Link name. On ports that are not broken out, this is always phy0. On a 2x breakout the options are phy0 and phy1, on 4x phy0-phy3, etc.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "v6_enabled": { + "description": "Whether or not IPv6 is enabled.", + "type": "boolean" + } + }, + "required": [ + "kind", + "link_name", + "v6_enabled" + ] + }, + "SwitchInterfaceKind": { + "description": "Indicates the kind for a switch interface.", + "oneOf": [ + { + "description": "Primary interfaces are associated with physical links. There is exactly one primary interface per physical link.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "primary" + ] + } + }, + "required": [ + "type" + ] + }, + { + "description": "VLAN interfaces allow physical interfaces to be multiplexed onto multiple logical links, each distinguished by a 12-bit 802.1Q Ethernet tag.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "vlan" + ] + }, + "vid": { + "description": "The virtual network id (VID) that distinguishes this interface and is used for producing and consuming 802.1Q Ethernet tags. This field has a maximum value of 4095 as 802.1Q tags are twelve bits.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "type", + "vid" + ] + }, + { + "description": "Loopback interfaces are anchors for IP addresses that are not specific to any particular port.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "loopback" + ] + } + }, + "required": [ + "type" + ] + } + ] + }, + "SwitchInterfaceKind2": { + "description": "Describes the kind of an switch interface.", + "oneOf": [ + { + "description": "Primary interfaces are associated with physical links. There is exactly one primary interface per physical link.", + "type": "string", + "enum": [ + "primary" + ] + }, + { + "description": "VLAN interfaces allow physical interfaces to be multiplexed onto multiple logical links, each distinguished by a 12-bit 802.1Q Ethernet tag.", + "type": "string", + "enum": [ + "vlan" + ] + }, + { + "description": "Loopback interfaces are anchors for IP addresses that are not specific to any particular port.", + "type": "string", + "enum": [ + "loopback" + ] + } + ] + }, + "SwitchLinkState": {}, + "SwitchLocation": { + "description": "Identifies switch physical location", + "oneOf": [ + { + "description": "Switch in upper slot", + "type": "string", + "enum": [ + "switch0" + ] + }, + { + "description": "Switch in lower slot", + "type": "string", + "enum": [ + "switch1" + ] + } + ] + }, + "SwitchPort": { + "description": "A switch port represents a physical external port on a rack switch.", + "type": "object", + "properties": { + "id": { + "description": "The id of the switch port.", + "type": "string", + "format": "uuid" + }, + "port_name": { + "description": "The name of this switch port.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "port_settings_id": { + "nullable": true, + "description": "The primary settings group of this switch port. Will be `None` until this switch port is configured.", + "type": "string", + "format": "uuid" + }, + "rack_id": { + "description": "The rack this switch port belongs to.", + "type": "string", + "format": "uuid" + }, + "switch_location": { + "description": "The switch location of this switch port.", + "type": "string" + } + }, + "required": [ + "id", + "port_name", + "rack_id", + "switch_location" + ] + }, + "SwitchPortAddressView": { + "description": "An IP address configuration for a port settings object.", + "type": "object", + "properties": { + "address": { + "description": "The IP address and prefix.", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + }, + "address_lot_block_id": { + "description": "The id of the address lot block this address is drawn from.", + "type": "string", + "format": "uuid" + }, + "address_lot_id": { + "description": "The id of the address lot this address is drawn from.", + "type": "string", + "format": "uuid" + }, + "address_lot_name": { + "description": "The name of the address lot this address is drawn from.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "interface_name": { + "description": "The interface name this address belongs to.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "port_settings_id": { + "description": "The port settings object this address configuration belongs to.", + "type": "string", + "format": "uuid" + }, + "vlan_id": { + "nullable": true, + "description": "An optional VLAN ID", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "address", + "address_lot_block_id", + "address_lot_id", + "address_lot_name", + "interface_name", + "port_settings_id" + ] + }, + "SwitchPortApplySettings": { + "description": "Parameters for applying settings to switch ports.", + "type": "object", + "properties": { + "port_settings": { + "description": "A name or id to use when applying switch port settings.", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + } + }, + "required": [ + "port_settings" + ] + }, + "SwitchPortConfig": { + "description": "A physical port configuration for a port settings object.", + "type": "object", + "properties": { + "geometry": { + "description": "The physical link geometry of the port.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchPortGeometry2" + } + ] + }, + "port_settings_id": { + "description": "The id of the port settings object this configuration belongs to.", + "type": "string", + "format": "uuid" + } + }, + "required": [ + "geometry", + "port_settings_id" + ] + }, + "SwitchPortConfigCreate": { + "description": "Physical switch port configuration.", + "type": "object", + "properties": { + "geometry": { + "description": "Link geometry for the switch port.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchPortGeometry" + } + ] + } + }, + "required": [ + "geometry" + ] + }, + "SwitchPortGeometry": { + "description": "The link geometry associated with a switch port.", + "oneOf": [ + { + "description": "The port contains a single QSFP28 link with four lanes.", + "type": "string", + "enum": [ + "qsfp28x1" + ] + }, + { + "description": "The port contains two QSFP28 links each with two lanes.", + "type": "string", + "enum": [ + "qsfp28x2" + ] + }, + { + "description": "The port contains four SFP28 links each with one lane.", + "type": "string", + "enum": [ + "sfp28x4" + ] + } + ] + }, + "SwitchPortGeometry2": { + "description": "The link geometry associated with a switch port.", + "oneOf": [ + { + "description": "The port contains a single QSFP28 link with four lanes.", + "type": "string", + "enum": [ + "qsfp28x1" + ] + }, + { + "description": "The port contains two QSFP28 links each with two lanes.", + "type": "string", + "enum": [ + "qsfp28x2" + ] + }, + { + "description": "The port contains four SFP28 links each with one lane.", + "type": "string", + "enum": [ + "sfp28x4" + ] + } + ] + }, + "SwitchPortLinkConfig": { + "description": "A link configuration for a port settings object.", + "type": "object", + "properties": { + "autoneg": { + "description": "Whether or not the link has autonegotiation enabled.", + "type": "boolean" + }, + "fec": { + "nullable": true, + "description": "The requested forward-error correction method. If this is not specified, the standard FEC for the underlying media will be applied if it can be determined.", + "allOf": [ + { + "$ref": "#/components/schemas/LinkFec" + } + ] + }, + "link_name": { + "description": "The name of this link.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "lldp_link_config": { + "nullable": true, + "description": "The link-layer discovery protocol service configuration for this link.", + "allOf": [ + { + "$ref": "#/components/schemas/LldpLinkConfig" + } + ] + }, + "mtu": { + "description": "The maximum transmission unit for this link.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "port_settings_id": { + "description": "The port settings this link configuration belongs to.", + "type": "string", + "format": "uuid" + }, + "speed": { + "description": "The configured speed of the link.", + "allOf": [ + { + "$ref": "#/components/schemas/LinkSpeed" + } + ] + }, + "tx_eq_config": { + "nullable": true, + "description": "The tx_eq configuration for this link.", + "allOf": [ + { + "$ref": "#/components/schemas/TxEqConfig2" + } + ] + } + }, + "required": [ + "autoneg", + "link_name", + "mtu", + "port_settings_id", + "speed" + ] + }, + "SwitchPortResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/SwitchPort" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "SwitchPortRouteConfig": { + "description": "A route configuration for a port settings object.", + "type": "object", + "properties": { + "dst": { + "description": "The route's destination network.", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + }, + "gw": { + "description": "The route's gateway address.", + "type": "string", + "format": "ip" + }, + "interface_name": { + "description": "The interface name this route configuration is assigned to.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "port_settings_id": { + "description": "The port settings object this route configuration belongs to.", + "type": "string", + "format": "uuid" + }, + "rib_priority": { + "nullable": true, + "description": "Route RIB priority. Higher priority indicates precedence within and across protocols.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "vlan_id": { + "nullable": true, + "description": "The VLAN identifier for the route. Use this if the gateway is reachable over an 802.1Q tagged L2 segment.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "dst", + "gw", + "interface_name", + "port_settings_id" + ] + }, + "SwitchPortSettings": { + "description": "This structure contains all port settings information in one place. It's a convenience data structure for getting a complete view of a particular port's settings.", + "type": "object", + "properties": { + "addresses": { + "description": "Layer 3 IP address settings.", + "type": "array", + "items": { + "$ref": "#/components/schemas/SwitchPortAddressView" + } + }, + "bgp_peers": { + "description": "BGP peer settings.", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpPeer" + } + }, + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "groups": { + "description": "Switch port settings included from other switch port settings groups.", + "type": "array", + "items": { + "$ref": "#/components/schemas/SwitchPortSettingsGroups" + } + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "interfaces": { + "description": "Layer 3 interface settings.", + "type": "array", + "items": { + "$ref": "#/components/schemas/SwitchInterfaceConfig" + } + }, + "links": { + "description": "Layer 2 link settings.", + "type": "array", + "items": { + "$ref": "#/components/schemas/SwitchPortLinkConfig" + } + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "port": { + "description": "Layer 1 physical port settings.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchPortConfig" + } + ] + }, + "routes": { + "description": "IP route settings.", + "type": "array", + "items": { + "$ref": "#/components/schemas/SwitchPortRouteConfig" + } + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + }, + "vlan_interfaces": { + "description": "Vlan interface settings.", + "type": "array", + "items": { + "$ref": "#/components/schemas/SwitchVlanInterfaceConfig" + } + } + }, + "required": [ + "addresses", + "bgp_peers", + "description", + "groups", + "id", + "interfaces", + "links", + "name", + "port", + "routes", + "time_created", + "time_modified", + "vlan_interfaces" + ] + }, + "SwitchPortSettingsCreate": { + "description": "Parameters for creating switch port settings. Switch port settings are the central data structure for setting up external networking. Switch port settings include link, interface, route, address and dynamic network protocol configuration.", + "type": "object", + "properties": { + "addresses": { + "description": "Address configurations.", + "type": "array", + "items": { + "$ref": "#/components/schemas/AddressConfig" + } + }, + "bgp_peers": { + "description": "BGP peer configurations.", + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpPeerConfig" + } + }, + "description": { + "type": "string" + }, + "groups": { + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/NameOrId" + } + }, + "interfaces": { + "description": "Interface configurations.", + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/SwitchInterfaceConfigCreate" + } + }, + "links": { + "description": "Link configurations.", + "type": "array", + "items": { + "$ref": "#/components/schemas/LinkConfigCreate" + } + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "port_config": { + "$ref": "#/components/schemas/SwitchPortConfigCreate" + }, + "routes": { + "description": "Route configurations.", + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/RouteConfig" + } + } + }, + "required": [ + "addresses", + "description", + "links", + "name", + "port_config" + ] + }, + "SwitchPortSettingsGroups": { + "description": "This structure maps a port settings object to a port settings groups. Port settings objects may inherit settings from groups. This mapping defines the relationship between settings objects and the groups they reference.", + "type": "object", + "properties": { + "port_settings_group_id": { + "description": "The id of a port settings group being referenced by a port settings object.", + "type": "string", + "format": "uuid" + }, + "port_settings_id": { + "description": "The id of a port settings object referencing a port settings group.", + "type": "string", + "format": "uuid" + } + }, + "required": [ + "port_settings_group_id", + "port_settings_id" + ] + }, + "SwitchPortSettingsIdentity": { + "description": "A switch port settings identity whose id may be used to view additional details.", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "name", + "time_created", + "time_modified" + ] + }, + "SwitchPortSettingsIdentityResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/SwitchPortSettingsIdentity" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "SwitchResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Switch" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "SwitchVlanInterfaceConfig": { + "description": "A switch port VLAN interface configuration for a port settings object.", + "type": "object", + "properties": { + "interface_config_id": { + "description": "The switch interface configuration this VLAN interface configuration belongs to.", + "type": "string", + "format": "uuid" + }, + "vlan_id": { + "description": "The virtual network id for this interface that is used for producing and consuming 802.1Q Ethernet tags. This field has a maximum value of 4095 as 802.1Q tags are twelve bits.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "interface_config_id", + "vlan_id" + ] + }, + "TargetRelease": { + "description": "View of a system software target release", + "type": "object", + "properties": { + "time_requested": { + "description": "Time this was set as the target release", + "type": "string", + "format": "date-time" + }, + "version": { + "description": "The specified release of the rack's system software", + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + } + }, + "required": [ + "time_requested", + "version" + ] + }, + "Timeseries": { + "description": "A timeseries contains a timestamped set of values from one source.\n\nThis includes the typed key-value pairs that uniquely identify it, and the set of timestamps and data values from it.", + "type": "object", + "properties": { + "fields": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/FieldValue" + } + }, + "points": { + "$ref": "#/components/schemas/Points" + } + }, + "required": [ + "fields", + "points" + ] + }, + "TimeseriesDescription": { + "description": "Text descriptions for the target and metric of a timeseries.", + "type": "object", + "properties": { + "metric": { + "type": "string" + }, + "target": { + "type": "string" + } + }, + "required": [ + "metric", + "target" + ] + }, + "TimeseriesName": { + "title": "The name of a timeseries", + "description": "Names are constructed by concatenating the target and metric names with ':'. Target and metric names must be lowercase alphanumeric characters with '_' separating words.", + "type": "string", + "pattern": "^(([a-z]+[a-z0-9]*)(_([a-z0-9]+))*):(([a-z]+[a-z0-9]*)(_([a-z0-9]+))*)$" + }, + "TimeseriesQuery": { + "description": "A timeseries query string, written in the Oximeter query language.", + "type": "object", + "properties": { + "query": { + "description": "A timeseries query string, written in the Oximeter query language.", + "type": "string" + } + }, + "required": [ + "query" + ] + }, + "TimeseriesSchema": { + "description": "The schema for a timeseries.\n\nThis includes the name of the timeseries, as well as the datum type of its metric and the schema for each field.", + "type": "object", + "properties": { + "authz_scope": { + "$ref": "#/components/schemas/AuthzScope" + }, + "created": { + "type": "string", + "format": "date-time" + }, + "datum_type": { + "$ref": "#/components/schemas/DatumType" + }, + "description": { + "$ref": "#/components/schemas/TimeseriesDescription" + }, + "field_schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FieldSchema" + }, + "uniqueItems": true + }, + "timeseries_name": { + "$ref": "#/components/schemas/TimeseriesName" + }, + "units": { + "$ref": "#/components/schemas/Units" + }, + "version": { + "type": "integer", + "format": "uint8", + "minimum": 1 + } + }, + "required": [ + "authz_scope", + "created", + "datum_type", + "description", + "field_schema", + "timeseries_name", + "units", + "version" + ] + }, + "TimeseriesSchemaResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeseriesSchema" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "TufRepo": { + "description": "Metadata about a TUF repository", + "type": "object", + "properties": { + "file_name": { + "description": "The file name of the repository, as reported by the client that uploaded it\n\nThis is intended for debugging. The file name may not match any particular pattern, and even if it does, it may not be accurate since it's just what the client reported.", + "type": "string" + }, + "hash": { + "description": "The hash of the repository", + "type": "string", + "format": "hex string (32 bytes)" + }, + "system_version": { + "description": "The system version for this repository\n\nThe system version is a top-level version number applied to all the software in the repository.", + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + }, + "time_created": { + "description": "Time the repository was uploaded", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "file_name", + "hash", + "system_version", + "time_created" + ] + }, + "TufRepoResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/TufRepo" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "TufRepoUpload": { + "type": "object", + "properties": { + "repo": { + "$ref": "#/components/schemas/TufRepo" + }, + "status": { + "$ref": "#/components/schemas/TufRepoUploadStatus" + } + }, + "required": [ + "repo", + "status" + ] + }, + "TufRepoUploadStatus": { + "description": "Whether the uploaded TUF repo already existed or was new and had to be inserted. Part of `TufRepoUpload`.", + "oneOf": [ + { + "description": "The repository already existed in the database", + "type": "string", + "enum": [ + "already_exists" + ] + }, + { + "description": "The repository did not exist, and was inserted into the database", + "type": "string", + "enum": [ + "inserted" + ] + } + ] + }, + "TxEqConfig": { + "description": "Per-port tx-eq overrides. This can be used to fine-tune the transceiver equalization settings to improve signal integrity.", + "type": "object", + "properties": { + "main": { + "nullable": true, + "description": "Main tap", + "type": "integer", + "format": "int32" + }, + "post1": { + "nullable": true, + "description": "Post-cursor tap1", + "type": "integer", + "format": "int32" + }, + "post2": { + "nullable": true, + "description": "Post-cursor tap2", + "type": "integer", + "format": "int32" + }, + "pre1": { + "nullable": true, + "description": "Pre-cursor tap1", + "type": "integer", + "format": "int32" + }, + "pre2": { + "nullable": true, + "description": "Pre-cursor tap2", + "type": "integer", + "format": "int32" + } + } + }, + "TxEqConfig2": { + "description": "Per-port tx-eq overrides. This can be used to fine-tune the transceiver equalization settings to improve signal integrity.", + "type": "object", + "properties": { + "main": { + "nullable": true, + "description": "Main tap", + "type": "integer", + "format": "int32" + }, + "post1": { + "nullable": true, + "description": "Post-cursor tap1", + "type": "integer", + "format": "int32" + }, + "post2": { + "nullable": true, + "description": "Post-cursor tap2", + "type": "integer", + "format": "int32" + }, + "pre1": { + "nullable": true, + "description": "Pre-cursor tap1", + "type": "integer", + "format": "int32" + }, + "pre2": { + "nullable": true, + "description": "Pre-cursor tap2", + "type": "integer", + "format": "int32" + } + } + }, + "UninitializedSled": { + "description": "A sled that has not been added to an initialized rack yet", + "type": "object", + "properties": { + "baseboard": { + "$ref": "#/components/schemas/Baseboard" + }, + "cubby": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "rack_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "baseboard", + "cubby", + "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", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/UninitializedSled" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "Units": { + "description": "Measurement units for timeseries samples.", + "oneOf": [ + { + "type": "string", + "enum": [ + "count", + "bytes", + "seconds", + "nanoseconds", + "volts", + "amps", + "watts", + "degrees_celsius" + ] + }, + { + "description": "No meaningful units, e.g. a dimensionless quanity.", + "type": "string", + "enum": [ + "none" + ] + }, + { + "description": "Rotations per minute.", + "type": "string", + "enum": [ + "rpm" + ] + } + ] + }, + "UpdateStatus": { + "type": "object", + "properties": { + "components_by_release_version": { + "description": "Count of components running each release version\n\nKeys will be either:\n\n* Semver-like release version strings * \"install dataset\", representing the initial rack software before any updates * \"unknown\", which means there is no TUF repo uploaded that matches the software running on the component)", + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "uint", + "minimum": 0 + } + }, + "suspended": { + "description": "Whether automatic update is suspended due to manual update activity\n\nAfter a manual support procedure that changes the system software, automatic update activity is suspended to avoid undoing the change. To resume automatic update, first upload the TUF repository matching the manually applied update, then set that as the target release.", + "type": "boolean" + }, + "target_release": { + "nullable": true, + "description": "Current target release of the system software\n\nThis may not correspond to the actual system software running at the time of request; it is instead the release that the system should be moving towards as a goal state. The system asynchronously updates software to match this target release.\n\nWill only be null if a target release has never been set. In that case, the system is not automatically attempting to manage software versions.", + "allOf": [ + { + "$ref": "#/components/schemas/TargetRelease" + } + ] + }, + "time_last_step_planned": { + "description": "Time of most recent update planning activity\n\nThis is intended as a rough indicator of the last time something happened in the update planner.", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "components_by_release_version", + "suspended", + "target_release", + "time_last_step_planned" + ] + }, + "UpdatesTrustRoot": { + "description": "Trusted root role used by the update system to verify update repositories.", + "type": "object", + "properties": { + "id": { + "description": "The UUID of this trusted root role.", + "type": "string", + "format": "uuid" + }, + "root_role": { + "description": "The trusted root role itself, a JSON document as described by The Update Framework." + }, + "time_created": { + "description": "Time the trusted root role was added.", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "root_role", + "time_created" + ] + }, + "UpdatesTrustRootResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/UpdatesTrustRoot" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "User": { + "description": "View of a User", + "type": "object", + "properties": { + "display_name": { + "description": "Human-readable name that can identify the user", + "type": "string" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "silo_id": { + "description": "Uuid of the silo to which this user belongs", + "type": "string", + "format": "uuid" + } + }, + "required": [ + "display_name", + "id", + "silo_id" + ] + }, + "UserBuiltin": { + "description": "View of a Built-in User\n\nBuilt-in users are identities internal to the system, used when the control plane performs actions autonomously", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "name", + "time_created", + "time_modified" + ] + }, + "UserBuiltinResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/UserBuiltin" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "UserCreate": { + "description": "Create-time parameters for a `User`", + "type": "object", + "properties": { + "external_id": { + "description": "username used to log in", + "allOf": [ + { + "$ref": "#/components/schemas/UserId" + } + ] + }, + "password": { + "description": "how to set the user's login password", + "allOf": [ + { + "$ref": "#/components/schemas/UserPassword" + } + ] + } + }, + "required": [ + "external_id", + "password" + ] + }, + "UserId": { + "title": "A username for a local-only user", + "description": "Usernames must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Usernames cannot be a UUID, but they may contain a UUID. They can be at most 63 characters long.", + "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-zA-Z0-9-]*[a-zA-Z0-9]+)?$", + "minLength": 1, + "maxLength": 63 + }, + "UserPassword": { + "description": "Parameters for setting a user's password", + "oneOf": [ + { + "description": "Sets the user's password to the provided value", + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": [ + "password" + ] + }, + "value": { + "$ref": "#/components/schemas/Password" + } + }, + "required": [ + "mode", + "value" + ] + }, + { + "description": "Invalidates any current password (disabling password authentication)", + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": [ + "login_disallowed" + ] + } + }, + "required": [ + "mode" + ] + } + ] + }, + "UserResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "UsernamePasswordCredentials": { + "description": "Credentials for local user login", + "type": "object", + "properties": { + "password": { + "$ref": "#/components/schemas/Password" + }, + "username": { + "$ref": "#/components/schemas/UserId" + } + }, + "required": [ + "password", + "username" + ] + }, + "Utilization": { + "description": "View of the current silo's resource utilization and capacity", + "type": "object", + "properties": { + "capacity": { + "description": "The total amount of resources that can be provisioned in this silo Actions that would exceed this limit will fail", + "allOf": [ + { + "$ref": "#/components/schemas/VirtualResourceCounts" + } + ] + }, + "provisioned": { + "description": "Accounts for resources allocated to running instances or storage allocated via disks or snapshots Note that CPU and memory resources associated with a stopped instances are not counted here whereas associated disks will still be counted", + "allOf": [ + { + "$ref": "#/components/schemas/VirtualResourceCounts" + } + ] + } + }, + "required": [ + "capacity", + "provisioned" + ] + }, + "ValueArray": { + "description": "List of data values for one timeseries.\n\nEach element is an option, where `None` represents a missing sample.", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "integer" + ] + }, + "values": { + "type": "array", + "items": { + "nullable": true, + "type": "integer", + "format": "int64" + } + } + }, + "required": [ + "type", + "values" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "double" + ] + }, + "values": { + "type": "array", + "items": { + "nullable": true, + "type": "number", + "format": "double" + } + } + }, + "required": [ + "type", + "values" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "boolean" + ] + }, + "values": { + "type": "array", + "items": { + "nullable": true, + "type": "boolean" + } + } + }, + "required": [ + "type", + "values" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "string" + ] + }, + "values": { + "type": "array", + "items": { + "nullable": true, + "type": "string" + } + } + }, + "required": [ + "type", + "values" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "integer_distribution" + ] + }, + "values": { + "type": "array", + "items": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Distributionint64" + } + ] + } + } + }, + "required": [ + "type", + "values" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "double_distribution" + ] + }, + "values": { + "type": "array", + "items": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Distributiondouble" + } + ] + } + } + }, + "required": [ + "type", + "values" + ] + } + ] + }, + "Values": { + "description": "A single list of values, for one dimension of a timeseries.", + "type": "object", + "properties": { + "metric_type": { + "description": "The type of this metric.", + "allOf": [ + { + "$ref": "#/components/schemas/MetricType" + } + ] + }, + "values": { + "description": "The data values.", + "allOf": [ + { + "$ref": "#/components/schemas/ValueArray" + } + ] + } + }, + "required": [ + "metric_type", + "values" + ] + }, + "VirtualResourceCounts": { + "description": "A collection of resource counts used to describe capacity and utilization", + "type": "object", + "properties": { + "cpus": { + "description": "Number of virtual CPUs", + "type": "integer", + "format": "int64" + }, + "memory": { + "description": "Amount of memory in bytes", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + }, + "storage": { + "description": "Amount of disk storage in bytes", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + } + }, + "required": [ + "cpus", + "memory", + "storage" + ] + }, + "Vni": { + "description": "A Geneve Virtual Network Identifier", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "Vpc": { + "description": "View of a VPC", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "dns_name": { + "description": "The name used for the VPC in DNS.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "ipv6_prefix": { + "description": "The unique local IPv6 address range for subnets in this VPC", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Net" + } + ] + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "project_id": { + "description": "id for the project containing this VPC", + "type": "string", + "format": "uuid" + }, + "system_router_id": { + "description": "id for the system router where subnet default routes are registered", + "type": "string", + "format": "uuid" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "dns_name", + "id", + "ipv6_prefix", + "name", + "project_id", + "system_router_id", + "time_created", + "time_modified" + ] + }, + "VpcCreate": { + "description": "Create-time parameters for a `Vpc`", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "dns_name": { + "$ref": "#/components/schemas/Name" + }, + "ipv6_prefix": { + "nullable": true, + "description": "The IPv6 prefix for this VPC\n\nAll IPv6 subnets created from this VPC must be taken from this range, which should be a Unique Local Address in the range `fd00::/48`. The default VPC Subnet will have the first `/64` range from this prefix.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Net" + } + ] + }, + "name": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "description", + "dns_name", + "name" + ] + }, + "VpcFirewallIcmpFilter": { + "type": "object", + "properties": { + "code": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/IcmpParamRange" + } + ] + }, + "icmp_type": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "icmp_type" + ] + }, + "VpcFirewallRule": { + "description": "A single rule in a VPC firewall", + "type": "object", + "properties": { + "action": { + "description": "Whether traffic matching the rule should be allowed or dropped", + "allOf": [ + { + "$ref": "#/components/schemas/VpcFirewallRuleAction" + } + ] + }, + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "direction": { + "description": "Whether this rule is for incoming or outgoing traffic", + "allOf": [ + { + "$ref": "#/components/schemas/VpcFirewallRuleDirection" + } + ] + }, + "filters": { + "description": "Reductions on the scope of the rule", + "allOf": [ + { + "$ref": "#/components/schemas/VpcFirewallRuleFilter" + } + ] + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "priority": { + "description": "The relative priority of this rule", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "status": { + "description": "Whether this rule is in effect", + "allOf": [ + { + "$ref": "#/components/schemas/VpcFirewallRuleStatus" + } + ] + }, + "targets": { + "description": "Determine the set of instances that the rule applies to", + "type": "array", + "items": { + "$ref": "#/components/schemas/VpcFirewallRuleTarget" + } + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + }, + "vpc_id": { + "description": "The VPC to which this rule belongs", + "type": "string", + "format": "uuid" + } + }, + "required": [ + "action", + "description", + "direction", + "filters", + "id", + "name", + "priority", + "status", + "targets", + "time_created", + "time_modified", + "vpc_id" + ] + }, + "VpcFirewallRuleAction": { + "type": "string", + "enum": [ + "allow", + "deny" + ] + }, + "VpcFirewallRuleDirection": { + "type": "string", + "enum": [ + "inbound", + "outbound" + ] + }, + "VpcFirewallRuleFilter": { + "description": "Filters reduce the scope of a firewall rule. Without filters, the rule applies to all packets to the targets (or from the targets, if it's an outbound rule). With multiple filters, the rule applies only to packets matching ALL filters. The maximum number of each type of filter is 256.", + "type": "object", + "properties": { + "hosts": { + "nullable": true, + "description": "If present, host filters match the \"other end\" of traffic from the target’s perspective: for an inbound rule, they match the source of traffic. For an outbound rule, they match the destination.", + "type": "array", + "items": { + "$ref": "#/components/schemas/VpcFirewallRuleHostFilter" + }, + "maxItems": 256 + }, + "ports": { + "nullable": true, + "description": "If present, the destination ports or port ranges this rule applies to.", + "type": "array", + "items": { + "$ref": "#/components/schemas/L4PortRange" + }, + "maxItems": 256 + }, + "protocols": { + "nullable": true, + "description": "If present, the networking protocols this rule applies to.", + "type": "array", + "items": { + "$ref": "#/components/schemas/VpcFirewallRuleProtocol" + }, + "maxItems": 256 + } + } + }, + "VpcFirewallRuleHostFilter": { + "description": "The `VpcFirewallRuleHostFilter` is used to filter traffic on the basis of its source or destination host.", + "oneOf": [ + { + "description": "The rule applies to traffic from/to all instances in the VPC", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "vpc" + ] + }, + "value": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The rule applies to traffic from/to all instances in the VPC Subnet", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "subnet" + ] + }, + "value": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The rule applies to traffic from/to this specific instance", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "instance" + ] + }, + "value": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The rule applies to traffic from/to a specific IP address", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ip" + ] + }, + "value": { + "type": "string", + "format": "ip" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The rule applies to traffic from/to a specific IP subnet", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ip_net" + ] + }, + "value": { + "$ref": "#/components/schemas/IpNet" + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "VpcFirewallRuleProtocol": { + "description": "The protocols that may be specified in a firewall rule's filter", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "tcp" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "udp" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "icmp" + ] + }, + "value": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/VpcFirewallIcmpFilter" + } + ] + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "VpcFirewallRuleStatus": { + "type": "string", + "enum": [ + "disabled", + "enabled" + ] + }, + "VpcFirewallRuleTarget": { + "description": "A `VpcFirewallRuleTarget` is used to specify the set of instances to which a firewall rule applies. You can target instances directly by name, or specify a VPC, VPC subnet, IP, or IP subnet, which will apply the rule to traffic going to all matching instances. Targets are additive: the rule applies to instances matching ANY target.", + "oneOf": [ + { + "description": "The rule applies to all instances in the VPC", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "vpc" + ] + }, + "value": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The rule applies to all instances in the VPC Subnet", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "subnet" + ] + }, + "value": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The rule applies to this specific instance", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "instance" + ] + }, + "value": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The rule applies to a specific IP address", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ip" + ] + }, + "value": { + "type": "string", + "format": "ip" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The rule applies to a specific IP subnet", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ip_net" + ] + }, + "value": { + "$ref": "#/components/schemas/IpNet" + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "VpcFirewallRuleUpdate": { + "description": "A single rule in a VPC firewall", + "type": "object", + "properties": { + "action": { + "description": "Whether traffic matching the rule should be allowed or dropped", + "allOf": [ + { + "$ref": "#/components/schemas/VpcFirewallRuleAction" + } + ] + }, + "description": { + "description": "Human-readable free-form text about a resource", + "type": "string" + }, + "direction": { + "description": "Whether this rule is for incoming or outgoing traffic", + "allOf": [ + { + "$ref": "#/components/schemas/VpcFirewallRuleDirection" + } + ] + }, + "filters": { + "description": "Reductions on the scope of the rule", + "allOf": [ + { + "$ref": "#/components/schemas/VpcFirewallRuleFilter" + } + ] + }, + "name": { + "description": "Name of the rule, unique to this VPC", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "priority": { + "description": "The relative priority of this rule", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "status": { + "description": "Whether this rule is in effect", + "allOf": [ + { + "$ref": "#/components/schemas/VpcFirewallRuleStatus" + } + ] + }, + "targets": { + "description": "Determine the set of instances that the rule applies to", + "type": "array", + "items": { + "$ref": "#/components/schemas/VpcFirewallRuleTarget" + }, + "maxItems": 256 + } + }, + "required": [ + "action", + "description", + "direction", + "filters", + "name", + "priority", + "status", + "targets" + ] + }, + "VpcFirewallRuleUpdateParams": { + "description": "Updated list of firewall rules. Will replace all existing rules.", + "type": "object", + "properties": { + "rules": { + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/VpcFirewallRuleUpdate" + }, + "maxItems": 1024 + } + } + }, + "VpcFirewallRules": { + "description": "Collection of a Vpc's firewall rules", + "type": "object", + "properties": { + "rules": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VpcFirewallRule" + } + } + }, + "required": [ + "rules" + ] + }, + "VpcResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Vpc" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "VpcRouter": { + "description": "A VPC router defines a series of rules that indicate where traffic should be sent depending on its destination.", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "kind": { + "$ref": "#/components/schemas/VpcRouterKind" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + }, + "vpc_id": { + "description": "The VPC to which the router belongs.", + "type": "string", + "format": "uuid" + } + }, + "required": [ + "description", + "id", + "kind", + "name", + "time_created", + "time_modified", + "vpc_id" + ] + }, + "VpcRouterCreate": { + "description": "Create-time parameters for a `VpcRouter`", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "description", + "name" + ] + }, + "VpcRouterKind": { + "type": "string", + "enum": [ + "system", + "custom" + ] + }, + "VpcRouterResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/VpcRouter" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "VpcRouterUpdate": { + "description": "Updateable properties of a `VpcRouter`", + "type": "object", + "properties": { + "description": { + "nullable": true, + "type": "string" + }, + "name": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + } + }, + "VpcSubnet": { + "description": "A VPC subnet represents a logical grouping for instances that allows network traffic between them, within a IPv4 subnetwork or optionally an IPv6 subnetwork.", + "type": "object", + "properties": { + "custom_router_id": { + "nullable": true, + "description": "ID for an attached custom router.", + "type": "string", + "format": "uuid" + }, + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "ipv4_block": { + "description": "The IPv4 subnet CIDR block.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4Net" + } + ] + }, + "ipv6_block": { + "description": "The IPv6 subnet CIDR block.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Net" + } + ] + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + }, + "vpc_id": { + "description": "The VPC to which the subnet belongs.", + "type": "string", + "format": "uuid" + } + }, + "required": [ + "description", + "id", + "ipv4_block", + "ipv6_block", + "name", + "time_created", + "time_modified", + "vpc_id" + ] + }, + "VpcSubnetCreate": { + "description": "Create-time parameters for a `VpcSubnet`", + "type": "object", + "properties": { + "custom_router": { + "nullable": true, + "description": "An optional router, used to direct packets sent from hosts in this subnet to any destination address.\n\nCustom routers apply in addition to the VPC-wide *system* router, and have higher priority than the system router for an otherwise equal-prefix-length match.", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + }, + "description": { + "type": "string" + }, + "ipv4_block": { + "description": "The IPv4 address range for this subnet.\n\nIt must be allocated from an RFC 1918 private address range, and must not overlap with any other existing subnet in the VPC.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4Net" + } + ] + }, + "ipv6_block": { + "nullable": true, + "description": "The IPv6 address range for this subnet.\n\nIt must be allocated from the RFC 4193 Unique Local Address range, with the prefix equal to the parent VPC's prefix. A random `/64` block will be assigned if one is not provided. It must not overlap with any existing subnet in the VPC.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Net" + } + ] + }, + "name": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "description", + "ipv4_block", + "name" + ] + }, + "VpcSubnetResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/VpcSubnet" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "VpcSubnetUpdate": { + "description": "Updateable properties of a `VpcSubnet`", + "type": "object", + "properties": { + "custom_router": { + "nullable": true, + "description": "An optional router, used to direct packets sent from hosts in this subnet to any destination address.", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + }, + "description": { + "nullable": true, + "type": "string" + }, + "name": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + } + }, + "VpcUpdate": { + "description": "Updateable properties of a `Vpc`", + "type": "object", + "properties": { + "description": { + "nullable": true, + "type": "string" + }, + "dns_name": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "name": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + } + }, + "WebhookCreate": { + "description": "Create-time identity-related parameters", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "endpoint": { + "description": "The URL that webhook notification requests should be sent to", + "type": "string", + "format": "uri" + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "secrets": { + "description": "A non-empty list of secret keys used to sign webhook payloads.", + "type": "array", + "items": { + "type": "string" + } + }, + "subscriptions": { + "description": "A list of webhook event class subscriptions.\n\nIf this list is empty or is not included in the request body, the webhook will not be subscribed to any events.", + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/AlertSubscription" + } + } + }, + "required": [ + "description", + "endpoint", + "name", + "secrets" + ] + }, + "WebhookDeliveryAttempt": { + "description": "An individual delivery attempt for a webhook event.\n\nThis represents a single HTTP request that was sent to the receiver, and its outcome.", + "type": "object", + "properties": { + "attempt": { + "description": "The attempt number.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "response": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/WebhookDeliveryResponse" + } + ] + }, + "result": { + "description": "The outcome of this delivery attempt: either the event was delivered successfully, or the request failed for one of several reasons.", + "allOf": [ + { + "$ref": "#/components/schemas/WebhookDeliveryAttemptResult" + } + ] + }, + "time_sent": { + "description": "The time at which the webhook delivery was attempted.", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "attempt", + "result", + "time_sent" + ] + }, + "WebhookDeliveryAttemptResult": { + "oneOf": [ + { + "description": "The webhook event has been delivered successfully.", + "type": "string", + "enum": [ + "succeeded" + ] + }, + { + "description": "A webhook request was sent to the endpoint, and it returned a HTTP error status code indicating an error.", + "type": "string", + "enum": [ + "failed_http_error" + ] + }, + { + "description": "The webhook request could not be sent to the receiver endpoint.", + "type": "string", + "enum": [ + "failed_unreachable" + ] + }, + { + "description": "A connection to the receiver endpoint was successfully established, but no response was received within the delivery timeout.", + "type": "string", + "enum": [ + "failed_timeout" + ] + } + ] + }, + "WebhookDeliveryResponse": { + "description": "The response received from a webhook receiver endpoint.", + "type": "object", + "properties": { + "duration_ms": { + "description": "The response time of the webhook endpoint, in milliseconds.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "status": { + "description": "The HTTP status code returned from the webhook endpoint.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "duration_ms", + "status" + ] + }, + "WebhookReceiver": { + "description": "The configuration for a webhook alert receiver.", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "endpoint": { + "description": "The URL that webhook notification requests are sent to.", + "type": "string", + "format": "uri" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "secrets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebhookSecret" + } + }, + "subscriptions": { + "description": "The list of alert classes to which this receiver is subscribed.", + "type": "array", + "items": { + "$ref": "#/components/schemas/AlertSubscription" + } + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "endpoint", + "id", + "name", + "secrets", + "subscriptions", + "time_created", + "time_modified" + ] + }, + "WebhookReceiverUpdate": { + "description": "Parameters to update a webhook configuration.", + "type": "object", + "properties": { + "description": { + "nullable": true, + "type": "string" + }, + "endpoint": { + "nullable": true, + "description": "The URL that webhook notification requests should be sent to", + "type": "string", + "format": "uri" + }, + "name": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + } + }, + "WebhookSecret": { + "description": "A view of a shared secret key assigned to a webhook receiver.\n\nOnce a secret is created, the value of the secret is not available in the API, as it must remain secret. Instead, secrets are referenced by their unique IDs assigned when they are created.", + "type": "object", + "properties": { + "id": { + "description": "The public unique ID of the secret.", + "type": "string", + "format": "uuid" + }, + "time_created": { + "description": "The UTC timestamp at which this secret was created.", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "time_created" + ] + }, + "WebhookSecretCreate": { + "type": "object", + "properties": { + "secret": { + "description": "The value of the shared secret key.", + "type": "string" + } + }, + "required": [ + "secret" + ] + }, + "WebhookSecrets": { + "description": "A list of the IDs of secrets associated with a webhook receiver.", + "type": "object", + "properties": { + "secrets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebhookSecret" + } + } + }, + "required": [ + "secrets" + ] + }, + "NameOrIdSortMode": { + "description": "Supported set of sort modes for scanning by name or id", + "oneOf": [ + { + "description": "sort in increasing order of \"name\"", + "type": "string", + "enum": [ + "name_ascending" + ] + }, + { + "description": "sort in decreasing order of \"name\"", + "type": "string", + "enum": [ + "name_descending" + ] + }, + { + "description": "sort in increasing order of \"id\"", + "type": "string", + "enum": [ + "id_ascending" + ] + } + ] + }, + "TimeAndIdSortMode": { + "description": "Supported set of sort modes for scanning by timestamp and ID", + "oneOf": [ + { + "description": "sort in increasing order of timestamp and ID, i.e., earliest first", + "type": "string", + "enum": [ + "time_and_id_ascending" + ] + }, + { + "description": "sort in increasing order of timestamp and ID, i.e., most recent first", + "type": "string", + "enum": [ + "time_and_id_descending" + ] + } + ] + }, + "IdSortMode": { + "description": "Supported set of sort modes for scanning by id only.\n\nCurrently, we only support scanning in ascending order.", + "oneOf": [ + { + "description": "sort in increasing order of \"id\"", + "type": "string", + "enum": [ + "id_ascending" + ] + } + ] + }, + "SystemMetricName": { + "type": "string", + "enum": [ + "virtual_disk_space_provisioned", + "cpus_provisioned", + "ram_provisioned" + ] + }, + "PaginationOrder": { + "description": "The order in which the client wants to page through the requested collection", + "type": "string", + "enum": [ + "ascending", + "descending" + ] + }, + "VersionSortMode": { + "description": "Supported sort modes when scanning by semantic version", + "oneOf": [ + { + "description": "Sort in increasing semantic version order (oldest first)", + "type": "string", + "enum": [ + "version_ascending" + ] + }, + { + "description": "Sort in decreasing semantic version order (newest first)", + "type": "string", + "enum": [ + "version_descending" + ] + } + ] + }, + "NameSortMode": { + "description": "Supported set of sort modes for scanning by name only\n\nCurrently, we only support scanning in ascending order.", + "oneOf": [ + { + "description": "sort in increasing order of \"name\"", + "type": "string", + "enum": [ + "name_ascending" + ] + } + ] + } + }, + "responses": { + "Error": { + "description": "Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "tags": [ + { + "name": "affinity", + "description": "Anti-affinity groups give control over instance placement.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/affinity" + } + }, + { + "name": "console-auth", + "description": "API for console authentication", + "externalDocs": { + "url": "http://docs.oxide.computer/api/console-auth" + } + }, + { + "name": "current-user", + "description": "Information pertaining to the current user.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/current-user" + } + }, + { + "name": "disks", + "description": "Virtual disks are used to store instance-local data which includes the operating system.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/disks" + } + }, + { + "name": "experimental", + "description": "Experimental, unstable interfaces, primarily for use by Oxide personnel", + "externalDocs": { + "url": "http://docs.oxide.computer/api/experimental" + } + }, + { + "name": "floating-ips", + "description": "Floating IPs allow a project to allocate well-known IPs to instances.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/floating-ips" + } + }, + { + "name": "images", + "description": "Images are read-only virtual disks that may be used to boot virtual machines.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/images" + } + }, + { + "name": "instances", + "description": "Virtual machine instances are the basic unit of computation. These operations are used for provisioning, controlling, and destroying instances.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/instances" + } + }, + { + "name": "login", + "description": "Authentication endpoints", + "externalDocs": { + "url": "http://docs.oxide.computer/api/login" + } + }, + { + "name": "metrics", + "description": "Silo-scoped metrics", + "externalDocs": { + "url": "http://docs.oxide.computer/api/metrics" + } + }, + { + "name": "multicast-groups", + "description": "Multicast groups provide efficient one-to-many network communication.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/multicast-groups" + } + }, + { + "name": "policy", + "description": "System-wide IAM policy", + "externalDocs": { + "url": "http://docs.oxide.computer/api/policy" + } + }, + { + "name": "projects", + "description": "Projects are a grouping of associated resources such as instances and disks within a silo for purposes of billing and access control.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/projects" + } + }, + { + "name": "silos", + "description": "Silos represent a logical partition of users and resources.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/silos" + } + }, + { + "name": "snapshots", + "description": "Snapshots of virtual disks at a particular point in time.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/snapshots" + } + }, + { + "name": "system/alerts", + "description": "Alerts deliver notifications for events that occur on the Oxide rack", + "externalDocs": { + "url": "http://docs.oxide.computer/api/alerts" + } + }, + { + "name": "system/audit-log", + "description": "These endpoints relate to audit logs.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/system-audit-log" + } + }, + { + "name": "system/hardware", + "description": "These operations pertain to hardware inventory and management. Racks are the unit of expansion of an Oxide deployment. Racks are in turn composed of sleds, switches, power supplies, and a cabled backplane.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/system-hardware" + } + }, + { + "name": "system/ip-pools", + "description": "IP pools are collections of external IPs that can be assigned to silos. When a pool is linked to a silo, users in that silo can allocate IPs from the pool for their instances.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/system-ip-pools" + } + }, + { + "name": "system/metrics", + "description": "Metrics provide insight into the operation of the Oxide deployment. These include telemetry on hardware and software components that can be used to understand the current state as well as to diagnose issues.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/system-metrics" + } + }, + { + "name": "system/networking", + "description": "This provides rack-level network configuration.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/system-networking" + } + }, + { + "name": "system/probes", + "description": "Probes for testing network connectivity", + "externalDocs": { + "url": "http://docs.oxide.computer/api/probes" + } + }, + { + "name": "system/silos", + "description": "Silos represent a logical partition of users and resources.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/system-silos" + } + }, + { + "name": "system/status", + "description": "Endpoints related to system health", + "externalDocs": { + "url": "http://docs.oxide.computer/api/system-status" + } + }, + { + "name": "system/update", + "description": "Upload and manage system updates", + "externalDocs": { + "url": "http://docs.oxide.computer/api/system-update" + } + }, + { + "name": "tokens", + "description": "API clients use device access tokens for authentication.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/tokens" + } + }, + { + "name": "vpcs", + "description": "Virtual Private Clouds (VPCs) provide isolated network environments for managing and deploying services.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/vpcs" + } + } + ] +} diff --git a/openapi/nexus/nexus-latest.json b/openapi/nexus/nexus-latest.json index 34c8e4a758e..d2c71b392bc 120000 --- a/openapi/nexus/nexus-latest.json +++ b/openapi/nexus/nexus-latest.json @@ -1 +1 @@ -nexus-2026010300.0.0-7599dd.json \ No newline at end of file +nexus-2026011700.0.0-491bb1.json \ No newline at end of file diff --git a/package-manifest.toml b/package-manifest.toml index e236e111407..5c8df384059 100644 --- a/package-manifest.toml +++ b/package-manifest.toml @@ -654,10 +654,10 @@ source.repo = "maghemite" # `tools/maghemite_openapi_version`. Failing to do so will cause a failure when # building `ddm-admin-client` (which will instruct you to update # `tools/maghemite_openapi_version`). -source.commit = "7f78e2b9ab37981e9edcf2e076a3257a032bbb06" +source.commit = "f1ea4650fc95a95bbcd1a64d19a45c40e98286f7" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/maghemite/image//mg-ddm-gz.sha256.txt -source.sha256 = "c962841f5d3a4c9e2d2dcdc9ec002d1978e70463009d31dfd46d7def80b3ecbb" +source.sha256 = "18c0fd1d1dd5a23d229afb2e627ec9671a7158aeb645fc3c949b35a5834628a8" output.type = "tarball" [package.mg-ddm] @@ -670,10 +670,10 @@ source.repo = "maghemite" # `tools/maghemite_openapi_version`. Failing to do so will cause a failure when # building `ddm-admin-client` (which will instruct you to update # `tools/maghemite_openapi_version`). -source.commit = "7f78e2b9ab37981e9edcf2e076a3257a032bbb06" +source.commit = "f1ea4650fc95a95bbcd1a64d19a45c40e98286f7" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/maghemite/image//mg-ddm.sha256.txt -source.sha256 = "c97b4e329b59d467a5c5af39e5d45298b6ce8dafb93831f37d7aa827a897472d" +source.sha256 = "4daa754c389d44b20d53be86fad55b6be408a05728a15edf3a927581ba27bbd2" output.type = "zone" output.intermediate_only = true @@ -685,10 +685,10 @@ source.repo = "maghemite" # `tools/maghemite_openapi_version`. Failing to do so will cause a failure when # building `ddm-admin-client` (which will instruct you to update # `tools/maghemite_openapi_version`). -source.commit = "7f78e2b9ab37981e9edcf2e076a3257a032bbb06" +source.commit = "f1ea4650fc95a95bbcd1a64d19a45c40e98286f7" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/maghemite/image//mgd.sha256.txt -source.sha256 = "2b0813a1e857ec2e995bc07c6d1de13c8321a20ea768cbab3f01f8e494240d61" +source.sha256 = "176f63a305b83455a5a71b0b32405828cf70c04ee2d30e3bfe8243720b2289c2" output.type = "zone" output.intermediate_only = true diff --git a/schema/crdb/bgp-unnumbered-peers/up01.sql b/schema/crdb/bgp-unnumbered-peers/up01.sql new file mode 100644 index 00000000000..1380eecc0cb --- /dev/null +++ b/schema/crdb/bgp-unnumbered-peers/up01.sql @@ -0,0 +1,5 @@ +-- Add an id column to switch_port_settings_bgp_peer_config to serve as the new +-- primary key. This is needed because we're making addr nullable for BGP unnumbered +-- peers, and addr was previously part of the primary key. +ALTER TABLE omicron.public.switch_port_settings_bgp_peer_config + ADD COLUMN IF NOT EXISTS id UUID NOT NULL DEFAULT gen_random_uuid(); diff --git a/schema/crdb/bgp-unnumbered-peers/up02.sql b/schema/crdb/bgp-unnumbered-peers/up02.sql new file mode 100644 index 00000000000..03de0d2e379 --- /dev/null +++ b/schema/crdb/bgp-unnumbered-peers/up02.sql @@ -0,0 +1,6 @@ +-- Drop the existing primary key constraint and add the new primary key using +-- the id column. These must be in the same transaction because CockroachDB +-- doesn't allow dropping a primary key without immediately adding a new one. +ALTER TABLE omicron.public.switch_port_settings_bgp_peer_config + DROP CONSTRAINT IF EXISTS switch_port_settings_bgp_peer_config_pkey, + ADD CONSTRAINT switch_port_settings_bgp_peer_config_pkey PRIMARY KEY (id); diff --git a/schema/crdb/bgp-unnumbered-peers/up03.sql b/schema/crdb/bgp-unnumbered-peers/up03.sql new file mode 100644 index 00000000000..7c5a6f8fa6e --- /dev/null +++ b/schema/crdb/bgp-unnumbered-peers/up03.sql @@ -0,0 +1,9 @@ +-- Drop the NOT NULL constraints on columns that were part of the old primary key. +-- These constraints were implicitly added when the columns were part of the pk, +-- and they persist after dropping the pk. We need to explicitly drop them since +-- these columns are now nullable (port_settings_id and interface_name) or +-- intentionally nullable for unnumbered peers (addr). +ALTER TABLE omicron.public.switch_port_settings_bgp_peer_config + ALTER COLUMN port_settings_id DROP NOT NULL, + ALTER COLUMN interface_name DROP NOT NULL, + ALTER COLUMN addr DROP NOT NULL; diff --git a/schema/crdb/bgp-unnumbered-peers/up04.sql b/schema/crdb/bgp-unnumbered-peers/up04.sql new file mode 100644 index 00000000000..608a7b8f929 --- /dev/null +++ b/schema/crdb/bgp-unnumbered-peers/up04.sql @@ -0,0 +1,5 @@ +-- Add unique constraint for numbered peers (those with addr). +-- This ensures we don't have duplicate peers on the same interface with the same address. +CREATE UNIQUE INDEX IF NOT EXISTS switch_port_settings_bgp_peer_config_numbered_unique + ON omicron.public.switch_port_settings_bgp_peer_config (port_settings_id, interface_name, addr) + WHERE addr IS NOT NULL; diff --git a/schema/crdb/bgp-unnumbered-peers/up05.sql b/schema/crdb/bgp-unnumbered-peers/up05.sql new file mode 100644 index 00000000000..11d26138f3e --- /dev/null +++ b/schema/crdb/bgp-unnumbered-peers/up05.sql @@ -0,0 +1,5 @@ +-- Add unique constraint for unnumbered peers (those without addr). +-- Only one unnumbered peer is allowed per interface. +CREATE UNIQUE INDEX IF NOT EXISTS switch_port_settings_bgp_peer_config_unnumbered_unique + ON omicron.public.switch_port_settings_bgp_peer_config (port_settings_id, interface_name) + WHERE addr IS NULL; diff --git a/schema/crdb/bgp-unnumbered-peers/up06.sql b/schema/crdb/bgp-unnumbered-peers/up06.sql new file mode 100644 index 00000000000..11a95019e79 --- /dev/null +++ b/schema/crdb/bgp-unnumbered-peers/up06.sql @@ -0,0 +1,5 @@ +-- Add an index for looking up BGP peers by port_settings_id. +-- This is needed because the partial indexes (for numbered and unnumbered peers) +-- don't cover all rows when filtering only by port_settings_id. +CREATE INDEX IF NOT EXISTS lookup_sps_bgp_peer_config_by_port_settings_id + ON omicron.public.switch_port_settings_bgp_peer_config (port_settings_id); diff --git a/schema/crdb/dbinit.sql b/schema/crdb/dbinit.sql index 29efbfcfb3b..67d6e721970 100644 --- a/schema/crdb/dbinit.sql +++ b/schema/crdb/dbinit.sql @@ -3420,6 +3420,7 @@ CREATE TABLE IF NOT EXISTS omicron.public.switch_port_settings_route_config ( ); CREATE TABLE IF NOT EXISTS omicron.public.switch_port_settings_bgp_peer_config ( + id UUID NOT NULL DEFAULT gen_random_uuid(), port_settings_id UUID, bgp_config_id UUID NOT NULL, interface_name TEXT, @@ -3439,14 +3440,29 @@ CREATE TABLE IF NOT EXISTS omicron.public.switch_port_settings_bgp_peer_config ( allow_export_list_active BOOLEAN NOT NULL DEFAULT false, vlan_id INT4, - /* TODO https://github.com/oxidecomputer/omicron/issues/3013 */ - PRIMARY KEY (port_settings_id, interface_name, addr) + PRIMARY KEY (id) ); +-- Unique constraint for numbered BGP peers (those with an address) +CREATE UNIQUE INDEX IF NOT EXISTS switch_port_settings_bgp_peer_config_numbered_unique + ON omicron.public.switch_port_settings_bgp_peer_config (port_settings_id, interface_name, addr) + WHERE addr IS NOT NULL; + +-- Unique constraint for unnumbered BGP peers (one per interface) +CREATE UNIQUE INDEX IF NOT EXISTS switch_port_settings_bgp_peer_config_unnumbered_unique + ON omicron.public.switch_port_settings_bgp_peer_config (port_settings_id, interface_name) + WHERE addr IS NULL; + CREATE INDEX IF NOT EXISTS lookup_sps_bgp_peer_config_by_bgp_config_id on omicron.public.switch_port_settings_bgp_peer_config( bgp_config_id ); +-- Index for looking up BGP peers by port_settings_id. +-- This is needed because the partial indexes (for numbered and unnumbered peers) +-- don't cover all rows when filtering only by port_settings_id. +CREATE INDEX IF NOT EXISTS lookup_sps_bgp_peer_config_by_port_settings_id + ON omicron.public.switch_port_settings_bgp_peer_config (port_settings_id); + CREATE TABLE IF NOT EXISTS omicron.public.switch_port_settings_bgp_peer_config_communities ( port_settings_id UUID NOT NULL, interface_name TEXT NOT NULL, @@ -7655,7 +7671,7 @@ INSERT INTO omicron.public.db_metadata ( version, target_version ) VALUES - (TRUE, NOW(), NOW(), '217.0.0', NULL) + (TRUE, NOW(), NOW(), '218.0.0', NULL) ON CONFLICT DO NOTHING; COMMIT; diff --git a/sled-agent/src/bootstrap/early_networking.rs b/sled-agent/src/bootstrap/early_networking.rs index 286ccde4fc2..1af0483ca8f 100644 --- a/sled-agent/src/bootstrap/early_networking.rs +++ b/sled-agent/src/bootstrap/early_networking.rs @@ -26,6 +26,7 @@ use mg_admin_client::types::{ }; use mg_admin_client::types::{ BgpPeerConfig as MgBgpPeerConfig, Ipv6UnicastConfig, + UnnumberedBgpPeerConfig as MgUnnumberedBgpPeerConfig, }; use omicron_common::OMICRON_DPD_TAG; use omicron_common::address::DENDRITE_PORT; @@ -496,6 +497,96 @@ impl<'a> EarlyNetworkSetup<'a> { let mut config: Option = None; let mut bgp_peer_configs = HashMap::>::new(); + let mut bgp_unnumbered_peer_configs = + HashMap::>::new(); + + // Helper function to build IPv4 unicast import/export policies + let build_ipv4_unicast = + |peer: &omicron_common::api::internal::shared::BgpPeerConfig| { + Ipv4UnicastConfig { + nexthop: None, + import_policy: match &peer.allowed_import { + ImportExportPolicy::NoFiltering => { + MgImportExportPolicy4::NoFiltering + } + ImportExportPolicy::Allow(list) => { + MgImportExportPolicy4::Allow( + list.iter() + .filter_map(|x| match x { + IpNet::V4(p) => Some(Prefix4 { + length: p.width(), + value: p.addr(), + }), + IpNet::V6(_) => None, + }) + .collect(), + ) + } + }, + export_policy: match &peer.allowed_export { + ImportExportPolicy::NoFiltering => { + MgImportExportPolicy4::NoFiltering + } + ImportExportPolicy::Allow(list) => { + MgImportExportPolicy4::Allow( + list.iter() + .filter_map(|x| match x { + IpNet::V4(p) => Some(Prefix4 { + length: p.width(), + value: p.addr(), + }), + IpNet::V6(_) => None, + }) + .collect(), + ) + } + }, + } + }; + + // Helper function to build IPv6 unicast import/export policies + let build_ipv6_unicast = + |peer: &omicron_common::api::internal::shared::BgpPeerConfig| { + Ipv6UnicastConfig { + nexthop: None, + import_policy: match &peer.allowed_import { + ImportExportPolicy::NoFiltering => { + MgImportExportPolicy6::NoFiltering + } + ImportExportPolicy::Allow(list) => { + MgImportExportPolicy6::Allow( + list.iter() + .filter_map(|x| match x { + IpNet::V6(p) => Some(Prefix6 { + length: p.width(), + value: p.addr(), + }), + IpNet::V4(_) => None, + }) + .collect(), + ) + } + }, + export_policy: match &peer.allowed_export { + ImportExportPolicy::NoFiltering => { + MgImportExportPolicy6::NoFiltering + } + ImportExportPolicy::Allow(list) => { + MgImportExportPolicy6::Allow( + list.iter() + .filter_map(|x| match x { + IpNet::V6(p) => Some(Prefix6 { + length: p.width(), + value: p.addr(), + }), + IpNet::V4(_) => None, + }) + .collect(), + ) + } + }, + } + }; // Iterate through ports and apply BGP config. for port in &our_ports { @@ -530,129 +621,98 @@ impl<'a> EarlyNetworkSetup<'a> { } } - let bpc = MgBgpPeerConfig { - name: format!("{}", peer.addr), - host: format!("{}:179", peer.addr), - hold_time: peer.hold_time.unwrap_or(6), - idle_hold_time: peer.idle_hold_time.unwrap_or(3), - delay_open: peer.delay_open.unwrap_or(0), - connect_retry: peer.connect_retry.unwrap_or(3), - keepalive: peer.keepalive.unwrap_or(2), - resolution: BGP_SESSION_RESOLUTION, - passive: false, - remote_asn: peer.remote_asn, - min_ttl: peer.min_ttl, - md5_auth_key: peer.md5_auth_key.clone(), - multi_exit_discriminator: peer.multi_exit_discriminator, - communities: peer.communities.clone(), - local_pref: peer.local_pref, - enforce_first_as: peer.enforce_first_as, - ipv4_unicast: Some(Ipv4UnicastConfig { - nexthop: None, - import_policy: match &peer.allowed_import { - ImportExportPolicy::NoFiltering => { - MgImportExportPolicy4::NoFiltering - } - ImportExportPolicy::Allow(list) => { - MgImportExportPolicy4::Allow( - list.clone() - .iter() - .filter_map(|x| match x { - IpNet::V4(p) => Some(Prefix4 { - length: p.width(), - value: p.addr(), - }), - IpNet::V6(_) => None, - }) - .collect(), - ) - } - }, - export_policy: match &peer.allowed_export { - ImportExportPolicy::NoFiltering => { - MgImportExportPolicy4::NoFiltering - } - ImportExportPolicy::Allow(list) => { - MgImportExportPolicy4::Allow( - list.clone() - .iter() - .filter_map(|x| match x { - IpNet::V4(p) => Some(Prefix4 { - length: p.width(), - value: p.addr(), - }), - IpNet::V6(_) => None, - }) - .collect(), - ) - } - }, - }), - ipv6_unicast: Some(Ipv6UnicastConfig { - nexthop: None, - import_policy: match &peer.allowed_import { - ImportExportPolicy::NoFiltering => { - MgImportExportPolicy6::NoFiltering - } - ImportExportPolicy::Allow(list) => { - MgImportExportPolicy6::Allow( - list.clone() - .iter() - .filter_map(|x| match x { - IpNet::V6(p) => Some(Prefix6 { - length: p.width(), - value: p.addr(), - }), - IpNet::V4(_) => None, - }) - .collect(), - ) - } - }, - export_policy: match &peer.allowed_export { - ImportExportPolicy::NoFiltering => { - MgImportExportPolicy6::NoFiltering - } - ImportExportPolicy::Allow(list) => { - MgImportExportPolicy6::Allow( - list.clone() - .iter() - .filter_map(|x| match x { - IpNet::V6(p) => Some(Prefix6 { - length: p.width(), - value: p.addr(), - }), - IpNet::V4(_) => None, - }) - .collect(), - ) - } - }, - }), - vlan_id: peer.vlan_id, - connect_retry_jitter: Some(JitterRange { - max: 1.0, - min: 0.75, - }), - deterministic_collision_resolution: false, - idle_hold_jitter: None, - }; - match bgp_peer_configs.get_mut(&port.port) { - Some(peers) => { - peers.push(bpc); + // Determine if this is a numbered or unnumbered peer based on + // whether an address is specified (unspecified = unnumbered) + if !peer.addr.is_unspecified() { + let addr = peer.addr; + // Numbered peer - identified by address + let bpc = MgBgpPeerConfig { + name: format!("{}", addr), + host: format!("{}:179", addr), + hold_time: peer.hold_time.unwrap_or(6), + idle_hold_time: peer.idle_hold_time.unwrap_or(3), + delay_open: peer.delay_open.unwrap_or(0), + connect_retry: peer.connect_retry.unwrap_or(3), + keepalive: peer.keepalive.unwrap_or(2), + resolution: BGP_SESSION_RESOLUTION, + passive: false, + remote_asn: peer.remote_asn, + min_ttl: peer.min_ttl, + md5_auth_key: peer.md5_auth_key.clone(), + multi_exit_discriminator: peer.multi_exit_discriminator, + communities: peer.communities.clone(), + local_pref: peer.local_pref, + enforce_first_as: peer.enforce_first_as, + ipv4_unicast: Some(build_ipv4_unicast(peer)), + ipv6_unicast: Some(build_ipv6_unicast(peer)), + vlan_id: peer.vlan_id, + connect_retry_jitter: Some(JitterRange { + max: 1.0, + min: 0.75, + }), + deterministic_collision_resolution: false, + idle_hold_jitter: None, + }; + match bgp_peer_configs.get_mut(&port.port) { + Some(peers) => { + peers.push(bpc); + } + None => { + bgp_peer_configs + .insert(port.port.clone(), vec![bpc]); + } } - None => { - bgp_peer_configs.insert(port.port.clone(), vec![bpc]); + } else { + // Unnumbered peer - identified by interface + let bpc = MgUnnumberedBgpPeerConfig { + name: format!("unnumbered-{}", port.port), + interface: port.port.clone(), + hold_time: peer.hold_time.unwrap_or(6), + idle_hold_time: peer.idle_hold_time.unwrap_or(3), + delay_open: peer.delay_open.unwrap_or(0), + connect_retry: peer.connect_retry.unwrap_or(3), + keepalive: peer.keepalive.unwrap_or(2), + resolution: BGP_SESSION_RESOLUTION, + passive: false, + remote_asn: peer.remote_asn, + min_ttl: peer.min_ttl, + md5_auth_key: peer.md5_auth_key.clone(), + multi_exit_discriminator: peer.multi_exit_discriminator, + communities: peer.communities.clone(), + local_pref: peer.local_pref, + enforce_first_as: peer.enforce_first_as, + ipv4_unicast: Some(build_ipv4_unicast(peer)), + ipv6_unicast: Some(build_ipv6_unicast(peer)), + vlan_id: peer.vlan_id, + connect_retry_jitter: Some(JitterRange { + max: 1.0, + min: 0.75, + }), + deterministic_collision_resolution: false, + idle_hold_jitter: None, + router_lifetime: 1800, // Default to 30 minutes + }; + match bgp_unnumbered_peer_configs.get_mut(&port.port) { + Some(peers) => { + peers.push(bpc); + } + None => { + bgp_unnumbered_peer_configs + .insert(port.port.clone(), vec![bpc]); + } } } } } - if !bgp_peer_configs.is_empty() { + let has_bgp_config = !bgp_peer_configs.is_empty() + || !bgp_unnumbered_peer_configs.is_empty(); + if has_bgp_config { if let Some(config) = &config { let request = ApplyRequest { asn: config.asn, peers: bgp_peer_configs, + unnumbered_peers: bgp_unnumbered_peer_configs, shaper: config.shaper.as_ref().map(|x| ShaperSource { code: x.clone(), asn: config.asn, diff --git a/tools/maghemite_ddm_openapi_version b/tools/maghemite_ddm_openapi_version index c66212c3baf..758b4005ebf 100644 --- a/tools/maghemite_ddm_openapi_version +++ b/tools/maghemite_ddm_openapi_version @@ -1 +1 @@ -COMMIT="7f78e2b9ab37981e9edcf2e076a3257a032bbb06" +COMMIT="f1ea4650fc95a95bbcd1a64d19a45c40e98286f7" diff --git a/tools/maghemite_mg_openapi_version b/tools/maghemite_mg_openapi_version index c66212c3baf..758b4005ebf 100644 --- a/tools/maghemite_mg_openapi_version +++ b/tools/maghemite_mg_openapi_version @@ -1 +1 @@ -COMMIT="7f78e2b9ab37981e9edcf2e076a3257a032bbb06" +COMMIT="f1ea4650fc95a95bbcd1a64d19a45c40e98286f7" diff --git a/tools/maghemite_mgd_checksums b/tools/maghemite_mgd_checksums index 431f5300f81..1b04067a2d1 100644 --- a/tools/maghemite_mgd_checksums +++ b/tools/maghemite_mgd_checksums @@ -1,2 +1,2 @@ -CIDL_SHA256="2b0813a1e857ec2e995bc07c6d1de13c8321a20ea768cbab3f01f8e494240d61" -MGD_LINUX_SHA256="813a2e09247ed0e51c9f53caabb53fbe07301979caf8f84bae47310167657d1a" \ No newline at end of file +CIDL_SHA256="176f63a305b83455a5a71b0b32405828cf70c04ee2d30e3bfe8243720b2289c2" +MGD_LINUX_SHA256="734c8c690f36d7872e066cd1d74e788d8dee8552f9453ba917d93a7eb666b971" \ No newline at end of file From 78c2f6a08fd3f30e6a5930f0444439ef75d1ca21 Mon Sep 17 00:00:00 2001 From: Ryan Goodfellow Date: Mon, 19 Jan 2026 06:33:51 +0000 Subject: [PATCH 04/20] bump maghemite --- Cargo.lock | 6 +++--- Cargo.toml | 6 +++--- package-manifest.toml | 10 +++++----- tools/maghemite_ddm_openapi_version | 2 +- tools/maghemite_mg_openapi_version | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 12a0b0a515a..9e9b25a54d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2481,7 +2481,7 @@ dependencies = [ [[package]] name = "ddm-admin-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=f1ea4650fc95a95bbcd1a64d19a45c40e98286f7#f1ea4650fc95a95bbcd1a64d19a45c40e98286f7" +source = "git+https://github.com/oxidecomputer/maghemite?rev=90110ca7b6d4c074fef651f8ca71bf3f5589a45e#90110ca7b6d4c074fef651f8ca71bf3f5589a45e" dependencies = [ "oxnet", "progenitor 0.11.2", @@ -6363,7 +6363,7 @@ dependencies = [ [[package]] name = "mg-admin-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=f1ea4650fc95a95bbcd1a64d19a45c40e98286f7#f1ea4650fc95a95bbcd1a64d19a45c40e98286f7" +source = "git+https://github.com/oxidecomputer/maghemite?rev=90110ca7b6d4c074fef651f8ca71bf3f5589a45e#90110ca7b6d4c074fef651f8ca71bf3f5589a45e" dependencies = [ "chrono", "colored 3.0.0", @@ -11441,7 +11441,7 @@ dependencies = [ [[package]] name = "rdb-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=f1ea4650fc95a95bbcd1a64d19a45c40e98286f7#f1ea4650fc95a95bbcd1a64d19a45c40e98286f7" +source = "git+https://github.com/oxidecomputer/maghemite?rev=90110ca7b6d4c074fef651f8ca71bf3f5589a45e#90110ca7b6d4c074fef651f8ca71bf3f5589a45e" dependencies = [ "oxnet", "schemars 0.8.22", diff --git a/Cargo.toml b/Cargo.toml index cf431a1b9a5..cc5082f09ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -566,8 +566,8 @@ ntp-admin-api = { path = "ntp-admin/api" } ntp-admin-client = { path = "clients/ntp-admin-client" } ntp-admin-types = { path = "ntp-admin/types" } ntp-admin-types-versions = { path = "ntp-admin/types/versions" } -mg-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "f1ea4650fc95a95bbcd1a64d19a45c40e98286f7" } -ddm-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "f1ea4650fc95a95bbcd1a64d19a45c40e98286f7" } +mg-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "90110ca7b6d4c074fef651f8ca71bf3f5589a45e" } +ddm-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "90110ca7b6d4c074fef651f8ca71bf3f5589a45e" } multimap = "0.10.1" nexus-auth = { path = "nexus/auth" } nexus-background-task-interface = { path = "nexus/background-task-interface" } @@ -697,7 +697,7 @@ rats-corim = { git = "https://github.com/oxidecomputer/rats-corim.git", rev = "f raw-cpuid = { git = "https://github.com/oxidecomputer/rust-cpuid.git", rev = "a4cf01df76f35430ff5d39dc2fe470bcb953503b" } rayon = "1.10" rcgen = "0.12.1" -rdb-types = { git = "https://github.com/oxidecomputer/maghemite", rev = "f1ea4650fc95a95bbcd1a64d19a45c40e98286f7" } +rdb-types = { git = "https://github.com/oxidecomputer/maghemite", rev = "90110ca7b6d4c074fef651f8ca71bf3f5589a45e" } reconfigurator-cli = { path = "dev-tools/reconfigurator-cli" } reedline = "0.40.0" ref-cast = "1.0" diff --git a/package-manifest.toml b/package-manifest.toml index 5c8df384059..561d6a0db0d 100644 --- a/package-manifest.toml +++ b/package-manifest.toml @@ -654,10 +654,10 @@ source.repo = "maghemite" # `tools/maghemite_openapi_version`. Failing to do so will cause a failure when # building `ddm-admin-client` (which will instruct you to update # `tools/maghemite_openapi_version`). -source.commit = "f1ea4650fc95a95bbcd1a64d19a45c40e98286f7" +source.commit = "90110ca7b6d4c074fef651f8ca71bf3f5589a45e" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/maghemite/image//mg-ddm-gz.sha256.txt -source.sha256 = "18c0fd1d1dd5a23d229afb2e627ec9671a7158aeb645fc3c949b35a5834628a8" +source.sha256 = "834fe0c901568b54b55f8940fc3cf6685c225addd19bb175cc70f1843525958b" output.type = "tarball" [package.mg-ddm] @@ -670,10 +670,10 @@ source.repo = "maghemite" # `tools/maghemite_openapi_version`. Failing to do so will cause a failure when # building `ddm-admin-client` (which will instruct you to update # `tools/maghemite_openapi_version`). -source.commit = "f1ea4650fc95a95bbcd1a64d19a45c40e98286f7" +source.commit = "90110ca7b6d4c074fef651f8ca71bf3f5589a45e" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/maghemite/image//mg-ddm.sha256.txt -source.sha256 = "4daa754c389d44b20d53be86fad55b6be408a05728a15edf3a927581ba27bbd2" +source.sha256 = "12cedd2c1107372f8b5a955be4c9c124fbc90a3194e701d938c654d301da02db" output.type = "zone" output.intermediate_only = true @@ -685,7 +685,7 @@ source.repo = "maghemite" # `tools/maghemite_openapi_version`. Failing to do so will cause a failure when # building `ddm-admin-client` (which will instruct you to update # `tools/maghemite_openapi_version`). -source.commit = "f1ea4650fc95a95bbcd1a64d19a45c40e98286f7" +source.commit = "90110ca7b6d4c074fef651f8ca71bf3f5589a45e" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/maghemite/image//mgd.sha256.txt source.sha256 = "176f63a305b83455a5a71b0b32405828cf70c04ee2d30e3bfe8243720b2289c2" diff --git a/tools/maghemite_ddm_openapi_version b/tools/maghemite_ddm_openapi_version index 758b4005ebf..ebdfe895cac 100644 --- a/tools/maghemite_ddm_openapi_version +++ b/tools/maghemite_ddm_openapi_version @@ -1 +1 @@ -COMMIT="f1ea4650fc95a95bbcd1a64d19a45c40e98286f7" +COMMIT="90110ca7b6d4c074fef651f8ca71bf3f5589a45e" diff --git a/tools/maghemite_mg_openapi_version b/tools/maghemite_mg_openapi_version index 758b4005ebf..ebdfe895cac 100644 --- a/tools/maghemite_mg_openapi_version +++ b/tools/maghemite_mg_openapi_version @@ -1 +1 @@ -COMMIT="f1ea4650fc95a95bbcd1a64d19a45c40e98286f7" +COMMIT="90110ca7b6d4c074fef651f8ca71bf3f5589a45e" From 27b97aeb1fba476c572c22844b34846cd6667382 Mon Sep 17 00:00:00 2001 From: Ryan Goodfellow Date: Mon, 19 Jan 2026 17:14:53 +0000 Subject: [PATCH 05/20] bump dendrite --- Cargo.lock | 18 ++++++++++-------- Cargo.toml | 2 +- package-manifest.toml | 12 ++++++------ tools/dendrite_stub_checksums | 6 +++--- tools/dendrite_version | 2 +- workspace-hack/Cargo.toml | 12 ++++++++---- 6 files changed, 29 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9e9b25a54d8..ea6274e470c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1773,7 +1773,7 @@ dependencies = [ [[package]] name = "common" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/dendrite?rev=21fbf39f63b25567b25aeadf0d9a73a01c842512#21fbf39f63b25567b25aeadf0d9a73a01c842512" +source = "git+https://github.com/oxidecomputer/dendrite?rev=0d884859047afe70864f79c93fb450f1996f09e3#0d884859047afe70864f79c93fb450f1996f09e3" dependencies = [ "anyhow", "chrono", @@ -3007,11 +3007,11 @@ dependencies = [ [[package]] name = "dpd-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/dendrite?rev=21fbf39f63b25567b25aeadf0d9a73a01c842512#21fbf39f63b25567b25aeadf0d9a73a01c842512" +source = "git+https://github.com/oxidecomputer/dendrite?rev=0d884859047afe70864f79c93fb450f1996f09e3#0d884859047afe70864f79c93fb450f1996f09e3" dependencies = [ "async-trait", "chrono", - "common 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=21fbf39f63b25567b25aeadf0d9a73a01c842512)", + "common 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=0d884859047afe70864f79c93fb450f1996f09e3)", "crc8", "futures", "http", @@ -7457,7 +7457,7 @@ dependencies = [ "crucible-agent-client", "dns-server", "dns-service-client", - "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=21fbf39f63b25567b25aeadf0d9a73a01c842512)", + "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=0d884859047afe70864f79c93fb450f1996f09e3)", "dropshot", "futures", "gateway-messages", @@ -8345,7 +8345,7 @@ dependencies = [ "display-error-chain", "dns-server", "dns-service-client", - "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=21fbf39f63b25567b25aeadf0d9a73a01c842512)", + "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=0d884859047afe70864f79c93fb450f1996f09e3)", "dropshot", "ereport-types", "expectorate", @@ -8828,7 +8828,7 @@ dependencies = [ "display-error-chain", "dns-server", "dns-service-client", - "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=21fbf39f63b25567b25aeadf0d9a73a01c842512)", + "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=0d884859047afe70864f79c93fb450f1996f09e3)", "dropshot", "expectorate", "flate2", @@ -9063,6 +9063,8 @@ dependencies = [ "ipnet", "ipnetwork", "itertools 0.10.5", + "itertools 0.12.1", + "itertools 0.13.0", "lalrpop-util", "lazy_static", "libc", @@ -16261,7 +16263,7 @@ name = "wicket-common" version = "0.1.0" dependencies = [ "anyhow", - "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=21fbf39f63b25567b25aeadf0d9a73a01c842512)", + "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=0d884859047afe70864f79c93fb450f1996f09e3)", "dropshot", "gateway-client", "gateway-types", @@ -16321,7 +16323,7 @@ dependencies = [ "clap", "debug-ignore", "display-error-chain", - "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=21fbf39f63b25567b25aeadf0d9a73a01c842512)", + "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=0d884859047afe70864f79c93fb450f1996f09e3)", "dropshot", "either", "expectorate", diff --git a/Cargo.toml b/Cargo.toml index cc5082f09ca..6da003c3f68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -462,7 +462,7 @@ digest = "0.10.7" dns-server = { path = "dns-server" } dns-server-api = { path = "dns-server-api" } dns-service-client = { path = "clients/dns-service-client" } -dpd-client = { git = "https://github.com/oxidecomputer/dendrite", rev = "21fbf39f63b25567b25aeadf0d9a73a01c842512" } +dpd-client = { git = "https://github.com/oxidecomputer/dendrite", rev = "0d884859047afe70864f79c93fb450f1996f09e3" } dropshot = { version = "0.16.6", features = [ "usdt-probes" ] } dropshot-api-manager = "0.2.4" dropshot-api-manager-types = "0.2.4" diff --git a/package-manifest.toml b/package-manifest.toml index 561d6a0db0d..b7a9f0f9b6d 100644 --- a/package-manifest.toml +++ b/package-manifest.toml @@ -736,8 +736,8 @@ only_for_targets.image = "standard" # the other `source.*` keys. source.type = "prebuilt" source.repo = "dendrite" -source.commit = "21fbf39f63b25567b25aeadf0d9a73a01c842512" -source.sha256 = "5fdf1716837b30cad74815d853339694233bdd57ed660890d6dc3dec5b9faa5d" +source.commit = "0d884859047afe70864f79c93fb450f1996f09e3" +source.sha256 = "fc1eb50d2f92d612567c13a784c43124f0ca4d39fe2d9a00738b962c7c72d0cf" output.type = "zone" output.intermediate_only = true @@ -763,8 +763,8 @@ only_for_targets.image = "standard" # the other `source.*` keys. source.type = "prebuilt" source.repo = "dendrite" -source.commit = "21fbf39f63b25567b25aeadf0d9a73a01c842512" -source.sha256 = "4c2169695baa344cf031b7445246a32debaa53fc8a0bb8ad0e5ca51d2a6d1acb" +source.commit = "0d884859047afe70864f79c93fb450f1996f09e3" +source.sha256 = "fc1b90912facef3f463e96a9953a63f4114bfe5c2cf7d247695a07a61aaeac09" output.type = "zone" output.intermediate_only = true @@ -783,8 +783,8 @@ only_for_targets.image = "standard" # the other `source.*` keys. source.type = "prebuilt" source.repo = "dendrite" -source.commit = "21fbf39f63b25567b25aeadf0d9a73a01c842512" -source.sha256 = "2ba8711482b4f96ada44ee5b1ac5543a693d5d7b62ae502357d019d95677e55b" +source.commit = "0d884859047afe70864f79c93fb450f1996f09e3" +source.sha256 = "459bdbbdfba4098dccd136912cdb5c3ef16ad57b8f6e1e11558aed353df904a4" output.type = "zone" output.intermediate_only = true diff --git a/tools/dendrite_stub_checksums b/tools/dendrite_stub_checksums index d49c44a5ce3..5e38b9780b6 100644 --- a/tools/dendrite_stub_checksums +++ b/tools/dendrite_stub_checksums @@ -1,3 +1,3 @@ -CIDL_SHA256_ILLUMOS="5fdf1716837b30cad74815d853339694233bdd57ed660890d6dc3dec5b9faa5d" -CIDL_SHA256_LINUX_DPD="b04cd77fb06b88be84d7dd7200af25876cd17227f4eadce92c4d50694d7e78f5" -CIDL_SHA256_LINUX_SWADM="173e694c14d812bb7200f63496dc4f4189ce571a41286707cb106ea33144568c" +CIDL_SHA256_ILLUMOS="fc1eb50d2f92d612567c13a784c43124f0ca4d39fe2d9a00738b962c7c72d0cf" +CIDL_SHA256_LINUX_DPD="86db6e0421c47d94cf753b4396c5f2a27def765216de4458d817cf7980c40b91" +CIDL_SHA256_LINUX_SWADM="59fa406d5530b7e35a812d82f5a88c389936b6166ff5575b6d799a9d014ed414" diff --git a/tools/dendrite_version b/tools/dendrite_version index d5ee005e7ca..0353e295983 100644 --- a/tools/dendrite_version +++ b/tools/dendrite_version @@ -1 +1 @@ -COMMIT="21fbf39f63b25567b25aeadf0d9a73a01c842512" +COMMIT="0d884859047afe70864f79c93fb450f1996f09e3" diff --git a/workspace-hack/Cargo.toml b/workspace-hack/Cargo.toml index 70b6ad8ec8d..cf55eeb2d6c 100644 --- a/workspace-hack/Cargo.toml +++ b/workspace-hack/Cargo.toml @@ -77,6 +77,7 @@ indexmap = { version = "2.12.1", features = ["serde"] } inout = { version = "0.1.3", default-features = false, features = ["std"] } ipnet = { version = "2.11.0", features = ["serde"] } ipnetwork = { version = "0.21.1", features = ["schemars", "serde"] } +itertools-594e8ee84c453af0 = { package = "itertools", version = "0.13.0" } lalrpop-util = { version = "0.19.12" } lazy_static = { version = "1.5.0", default-features = false, features = ["spin_no_std"] } libc = { version = "0.2.174", features = ["extra_traits"] } @@ -121,7 +122,7 @@ scopeguard = { version = "1.2.0" } semver = { version = "1.0.27", features = ["serde"] } serde = { version = "1.0.228", features = ["alloc", "derive", "rc"] } serde_core = { version = "1.0.228", features = ["alloc", "rc"] } -serde_json = { version = "1.0.145", features = ["alloc", "raw_value", "unbounded_depth"] } +serde_json = { version = "1.0.149", features = ["alloc", "raw_value", "unbounded_depth"] } serde_with = { version = "3.14.0", features = ["hex", "schemars_0_8"] } sha1 = { version = "0.10.6", features = ["oid"] } sha2 = { version = "0.10.9", features = ["oid"] } @@ -218,6 +219,7 @@ indexmap = { version = "2.12.1", features = ["serde"] } inout = { version = "0.1.3", default-features = false, features = ["std"] } ipnet = { version = "2.11.0", features = ["serde"] } ipnetwork = { version = "0.21.1", features = ["schemars", "serde"] } +itertools-594e8ee84c453af0 = { package = "itertools", version = "0.13.0" } lalrpop-util = { version = "0.19.12" } lazy_static = { version = "1.5.0", default-features = false, features = ["spin_no_std"] } libc = { version = "0.2.174", features = ["extra_traits"] } @@ -262,7 +264,7 @@ scopeguard = { version = "1.2.0" } semver = { version = "1.0.27", features = ["serde"] } serde = { version = "1.0.228", features = ["alloc", "derive", "rc"] } serde_core = { version = "1.0.228", features = ["alloc", "rc"] } -serde_json = { version = "1.0.145", features = ["alloc", "raw_value", "unbounded_depth"] } +serde_json = { version = "1.0.149", features = ["alloc", "raw_value", "unbounded_depth"] } serde_with = { version = "3.14.0", features = ["hex", "schemars_0_8"] } serde_with_macros = { version = "3.14.0", default-features = false, features = ["schemars_0_8"] } sha1 = { version = "0.10.6", features = ["oid"] } @@ -385,7 +387,8 @@ dof-9fbad63c4bcf4a8f = { package = "dof", version = "0.4.0", default-features = getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3.4", default-features = false, features = ["std"] } hyper-rustls = { version = "0.27.7", features = ["http2", "ring", "webpki-tokio"] } hyper-util = { version = "0.1.19", features = ["full"] } -itertools = { version = "0.10.5" } +itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12.1" } +itertools-93f6ce9d446188ac = { package = "itertools", version = "0.10.5" } miniz_oxide = { version = "0.8.5", default-features = false, features = ["with-alloc"] } mio = { version = "1.0.2", features = ["net", "os-ext"] } nom = { version = "7.1.3" } @@ -404,7 +407,8 @@ dof-9fbad63c4bcf4a8f = { package = "dof", version = "0.4.0", default-features = getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3.4", default-features = false, features = ["std"] } hyper-rustls = { version = "0.27.7", features = ["http2", "ring", "webpki-tokio"] } hyper-util = { version = "0.1.19", features = ["full"] } -itertools = { version = "0.10.5" } +itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12.1" } +itertools-93f6ce9d446188ac = { package = "itertools", version = "0.10.5" } miniz_oxide = { version = "0.8.5", default-features = false, features = ["with-alloc"] } mio = { version = "1.0.2", features = ["net", "os-ext"] } nom = { version = "7.1.3" } From 390c2314611c66d053166be81acdd314abaa4960 Mon Sep 17 00:00:00 2001 From: Ryan Goodfellow Date: Mon, 19 Jan 2026 19:13:57 +0000 Subject: [PATCH 06/20] bump softnpu --- dev-tools/xtask/src/virtual_hardware.rs | 4 ++-- tools/softnpu_version | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dev-tools/xtask/src/virtual_hardware.rs b/dev-tools/xtask/src/virtual_hardware.rs index aca8b979db5..34e2edc6128 100644 --- a/dev-tools/xtask/src/virtual_hardware.rs +++ b/dev-tools/xtask/src/virtual_hardware.rs @@ -117,8 +117,8 @@ const ZPOOL: &'static str = "/usr/sbin/zpool"; const ZONEADM: &'static str = "/usr/sbin/zoneadm"; const SIDECAR_LITE_COMMIT: &'static str = - "a95b7a9f78c08125f4e34106f5c885c7e9f2e8d5"; -const SOFTNPU_COMMIT: &'static str = "3203c51cf4473d30991b522062ac0df2e045c2f2"; + "89f345799859d42fb1ab3c8c00485fd29d478abc"; +const SOFTNPU_COMMIT: &'static str = "e4c3130538b192988737161f4a4dc97272f02b9c"; const PXA_MAC_DEFAULT: &'static str = "a8:e1:de:01:70:1d"; const PXA_WARNING: &'static str = r#" You have not set up the proxy-ARP environment variables diff --git a/tools/softnpu_version b/tools/softnpu_version index 03f74d88656..0fc9248f4f1 100644 --- a/tools/softnpu_version +++ b/tools/softnpu_version @@ -1,2 +1,2 @@ -COMMIT="3203c51cf4473d30991b522062ac0df2e045c2f2" -SHA2="36095c7f9d613b9208415aeb67335836a25f72eed2f7a41931ba7d91ddb00568" +COMMIT="e4c3130538b192988737161f4a4dc97272f02b9c" +SHA2="5776bfcd80b685080c383b19b04b46ba9b985de18f722f1523f456d7ce59d0c5" From 5a1a1a46d317f0fec9142d0d3afe35370d958641 Mon Sep 17 00:00:00 2001 From: Ryan Goodfellow Date: Mon, 19 Jan 2026 14:13:40 -0800 Subject: [PATCH 07/20] more bumps --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 2 +- package-manifest.toml | 12 ++++++------ tools/dendrite_stub_checksums | 6 +++--- tools/dendrite_version | 2 +- tools/opte_version | 2 +- tools/opte_version_override | 2 +- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ea6274e470c..b44091cc465 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1773,7 +1773,7 @@ dependencies = [ [[package]] name = "common" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/dendrite?rev=0d884859047afe70864f79c93fb450f1996f09e3#0d884859047afe70864f79c93fb450f1996f09e3" +source = "git+https://github.com/oxidecomputer/dendrite?rev=4647860001629c3a648816616e3104180d490f50#4647860001629c3a648816616e3104180d490f50" dependencies = [ "anyhow", "chrono", @@ -3007,11 +3007,11 @@ dependencies = [ [[package]] name = "dpd-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/dendrite?rev=0d884859047afe70864f79c93fb450f1996f09e3#0d884859047afe70864f79c93fb450f1996f09e3" +source = "git+https://github.com/oxidecomputer/dendrite?rev=4647860001629c3a648816616e3104180d490f50#4647860001629c3a648816616e3104180d490f50" dependencies = [ "async-trait", "chrono", - "common 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=0d884859047afe70864f79c93fb450f1996f09e3)", + "common 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=4647860001629c3a648816616e3104180d490f50)", "crc8", "futures", "http", @@ -7457,7 +7457,7 @@ dependencies = [ "crucible-agent-client", "dns-server", "dns-service-client", - "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=0d884859047afe70864f79c93fb450f1996f09e3)", + "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=4647860001629c3a648816616e3104180d490f50)", "dropshot", "futures", "gateway-messages", @@ -8345,7 +8345,7 @@ dependencies = [ "display-error-chain", "dns-server", "dns-service-client", - "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=0d884859047afe70864f79c93fb450f1996f09e3)", + "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=4647860001629c3a648816616e3104180d490f50)", "dropshot", "ereport-types", "expectorate", @@ -8828,7 +8828,7 @@ dependencies = [ "display-error-chain", "dns-server", "dns-service-client", - "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=0d884859047afe70864f79c93fb450f1996f09e3)", + "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=4647860001629c3a648816616e3104180d490f50)", "dropshot", "expectorate", "flate2", @@ -16263,7 +16263,7 @@ name = "wicket-common" version = "0.1.0" dependencies = [ "anyhow", - "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=0d884859047afe70864f79c93fb450f1996f09e3)", + "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=4647860001629c3a648816616e3104180d490f50)", "dropshot", "gateway-client", "gateway-types", @@ -16323,7 +16323,7 @@ dependencies = [ "clap", "debug-ignore", "display-error-chain", - "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=0d884859047afe70864f79c93fb450f1996f09e3)", + "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=4647860001629c3a648816616e3104180d490f50)", "dropshot", "either", "expectorate", diff --git a/Cargo.toml b/Cargo.toml index 6da003c3f68..f13ab76d705 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -462,7 +462,7 @@ digest = "0.10.7" dns-server = { path = "dns-server" } dns-server-api = { path = "dns-server-api" } dns-service-client = { path = "clients/dns-service-client" } -dpd-client = { git = "https://github.com/oxidecomputer/dendrite", rev = "0d884859047afe70864f79c93fb450f1996f09e3" } +dpd-client = { git = "https://github.com/oxidecomputer/dendrite", rev = "4647860001629c3a648816616e3104180d490f50" } dropshot = { version = "0.16.6", features = [ "usdt-probes" ] } dropshot-api-manager = "0.2.4" dropshot-api-manager-types = "0.2.4" diff --git a/package-manifest.toml b/package-manifest.toml index b7a9f0f9b6d..08e8e2cc116 100644 --- a/package-manifest.toml +++ b/package-manifest.toml @@ -736,8 +736,8 @@ only_for_targets.image = "standard" # the other `source.*` keys. source.type = "prebuilt" source.repo = "dendrite" -source.commit = "0d884859047afe70864f79c93fb450f1996f09e3" -source.sha256 = "fc1eb50d2f92d612567c13a784c43124f0ca4d39fe2d9a00738b962c7c72d0cf" +source.commit = "4647860001629c3a648816616e3104180d490f50" +source.sha256 = "72983d76004dcc50c80b1a4c1114218b86e6153917c55f349f5df7f2e3d7e4ac" output.type = "zone" output.intermediate_only = true @@ -763,8 +763,8 @@ only_for_targets.image = "standard" # the other `source.*` keys. source.type = "prebuilt" source.repo = "dendrite" -source.commit = "0d884859047afe70864f79c93fb450f1996f09e3" -source.sha256 = "fc1b90912facef3f463e96a9953a63f4114bfe5c2cf7d247695a07a61aaeac09" +source.commit = "4647860001629c3a648816616e3104180d490f50" +source.sha256 = "8c47d819f12e84c818053c5f7d82783624ef74579dac9aae4dc56a73fbe46f44" output.type = "zone" output.intermediate_only = true @@ -783,8 +783,8 @@ only_for_targets.image = "standard" # the other `source.*` keys. source.type = "prebuilt" source.repo = "dendrite" -source.commit = "0d884859047afe70864f79c93fb450f1996f09e3" -source.sha256 = "459bdbbdfba4098dccd136912cdb5c3ef16ad57b8f6e1e11558aed353df904a4" +source.commit = "4647860001629c3a648816616e3104180d490f50" +source.sha256 = "2439bb52f890c18543f291561bb8c6ad09009b5b490dc635f053dcc57628bcae" output.type = "zone" output.intermediate_only = true diff --git a/tools/dendrite_stub_checksums b/tools/dendrite_stub_checksums index 5e38b9780b6..60e57428627 100644 --- a/tools/dendrite_stub_checksums +++ b/tools/dendrite_stub_checksums @@ -1,3 +1,3 @@ -CIDL_SHA256_ILLUMOS="fc1eb50d2f92d612567c13a784c43124f0ca4d39fe2d9a00738b962c7c72d0cf" -CIDL_SHA256_LINUX_DPD="86db6e0421c47d94cf753b4396c5f2a27def765216de4458d817cf7980c40b91" -CIDL_SHA256_LINUX_SWADM="59fa406d5530b7e35a812d82f5a88c389936b6166ff5575b6d799a9d014ed414" +CIDL_SHA256_ILLUMOS="72983d76004dcc50c80b1a4c1114218b86e6153917c55f349f5df7f2e3d7e4ac" +CIDL_SHA256_LINUX_DPD="a6dd16a2e45125303cbcea95ea76c389a039edc35f91303b89a3aef3413d9802" +CIDL_SHA256_LINUX_SWADM="5ac6bb5fe5379da30292b0a3be9dbc119c7d8aa904cf3a6b943e4cf3c1117f52" diff --git a/tools/dendrite_version b/tools/dendrite_version index 0353e295983..708ad1b5ebf 100644 --- a/tools/dendrite_version +++ b/tools/dendrite_version @@ -1 +1 @@ -COMMIT="0d884859047afe70864f79c93fb450f1996f09e3" +COMMIT="4647860001629c3a648816616e3104180d490f50" diff --git a/tools/opte_version b/tools/opte_version index b508f3a5405..8fb249b5ac8 100644 --- a/tools/opte_version +++ b/tools/opte_version @@ -1 +1 @@ -0.37.424 +0.38.452 diff --git a/tools/opte_version_override b/tools/opte_version_override index cf3b7e676eb..8d57f7ae9f4 100644 --- a/tools/opte_version_override +++ b/tools/opte_version_override @@ -2,4 +2,4 @@ # only set this if you want to override the version of opte/xde installed by the # install_opte.sh script -OPTE_COMMIT="a1ed0960673b6ca2e6b68835537f53cc86110a77" +OPTE_COMMIT="" From 7e04eff4e789c17349e344b5d61117b5a00893cd Mon Sep 17 00:00:00 2001 From: Ryan Goodfellow Date: Mon, 19 Jan 2026 16:44:26 -0800 Subject: [PATCH 08/20] comment out v6 unicast for the moment ..... --- .../tasks/sync_switch_configuration.rs | 16 ++++++++-------- sled-agent/src/bootstrap/early_networking.rs | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/nexus/src/app/background/tasks/sync_switch_configuration.rs b/nexus/src/app/background/tasks/sync_switch_configuration.rs index e2d76291537..1d4e6ada3a9 100644 --- a/nexus/src/app/background/tasks/sync_switch_configuration.rs +++ b/nexus/src/app/background/tasks/sync_switch_configuration.rs @@ -32,8 +32,8 @@ use mg_admin_client::types::{ BgpPeerConfig, CheckerSource, DeleteStaticRoute4Request, DeleteStaticRoute6Request, ImportExportPolicy4 as MgImportExportPolicy4, ImportExportPolicy6 as MgImportExportPolicy6, Ipv4UnicastConfig, - Ipv6UnicastConfig, JitterRange, ShaperSource, StaticRoute4, - StaticRoute4List, StaticRoute6, StaticRoute6List, UnnumberedBgpPeerConfig, + JitterRange, ShaperSource, StaticRoute4, StaticRoute4List, StaticRoute6, + StaticRoute6List, UnnumberedBgpPeerConfig, }; use nexus_db_queries::{ context::OpContext, @@ -735,7 +735,7 @@ impl BackgroundTask for SwitchPortSettingsManager { None => MgImportExportPolicy4::NoFiltering, }; - let import_policy6 = match &allow_import { + let _import_policy6 = match &allow_import { Some(list) => { MgImportExportPolicy6::Allow(list .clone() @@ -803,7 +803,7 @@ impl BackgroundTask for SwitchPortSettingsManager { None => MgImportExportPolicy4::NoFiltering, }; - let export_policy6 = match &allow_export { + let _export_policy6 = match &allow_export { Some(list) => { MgImportExportPolicy6::Allow(list .clone() @@ -848,11 +848,11 @@ impl BackgroundTask for SwitchPortSettingsManager { import_policy: import_policy4, export_policy: export_policy4, }), - ipv6_unicast: Some(Ipv6UnicastConfig{ + ipv6_unicast: None, /* TODO Some(Ipv6UnicastConfig{ nexthop: None, import_policy: import_policy6, export_policy: export_policy6, - }), + }),*/ vlan_id: peer.vlan_id.map(|x| x.0), //TODO plumb these out to the external API connect_retry_jitter: Some(JitterRange { @@ -898,11 +898,11 @@ impl BackgroundTask for SwitchPortSettingsManager { import_policy: MgImportExportPolicy4::NoFiltering, export_policy: MgImportExportPolicy4::NoFiltering, }), - ipv6_unicast: Some(Ipv6UnicastConfig{ + ipv6_unicast: None, /*TODO Some(Ipv6UnicastConfig{ nexthop: None, import_policy: MgImportExportPolicy6::NoFiltering, export_policy: MgImportExportPolicy6::NoFiltering, - }), + }),*/ vlan_id: peer.vlan_id.map(|x| x.0), connect_retry_jitter: Some(JitterRange { max: 1.0, diff --git a/sled-agent/src/bootstrap/early_networking.rs b/sled-agent/src/bootstrap/early_networking.rs index 1af0483ca8f..7213f6fbed8 100644 --- a/sled-agent/src/bootstrap/early_networking.rs +++ b/sled-agent/src/bootstrap/early_networking.rs @@ -545,7 +545,7 @@ impl<'a> EarlyNetworkSetup<'a> { }; // Helper function to build IPv6 unicast import/export policies - let build_ipv6_unicast = + let _build_ipv6_unicast = |peer: &omicron_common::api::internal::shared::BgpPeerConfig| { Ipv6UnicastConfig { nexthop: None, @@ -644,7 +644,7 @@ impl<'a> EarlyNetworkSetup<'a> { local_pref: peer.local_pref, enforce_first_as: peer.enforce_first_as, ipv4_unicast: Some(build_ipv4_unicast(peer)), - ipv6_unicast: Some(build_ipv6_unicast(peer)), + ipv6_unicast: None, // TODO Some(build_ipv6_unicast(peer)), vlan_id: peer.vlan_id, connect_retry_jitter: Some(JitterRange { max: 1.0, @@ -682,7 +682,7 @@ impl<'a> EarlyNetworkSetup<'a> { local_pref: peer.local_pref, enforce_first_as: peer.enforce_first_as, ipv4_unicast: Some(build_ipv4_unicast(peer)), - ipv6_unicast: Some(build_ipv6_unicast(peer)), + ipv6_unicast: None, // TODO Some(build_ipv6_unicast(peer)), vlan_id: peer.vlan_id, connect_retry_jitter: Some(JitterRange { max: 1.0, From 25a161d6d770e7cf802493a34007e7090eef0d9f Mon Sep 17 00:00:00 2001 From: Ryan Goodfellow Date: Tue, 20 Jan 2026 07:07:24 +0000 Subject: [PATCH 09/20] first swing at bootstrap agent lockstep api --- Cargo.lock | 35 + Cargo.toml | 6 + .../Cargo.toml | 22 + .../src/lib.rs | 72 + dev-tools/dropshot-apis/Cargo.toml | 1 + dev-tools/dropshot-apis/src/main.rs | 17 + openapi/bootstrap-agent-lockstep.json | 1542 +++++++++++++++++ sled-agent/Cargo.toml | 1 + sled-agent/bootstrap-agent-api/src/lib.rs | 6 + .../bootstrap-agent-lockstep-api/Cargo.toml | 18 + .../bootstrap-agent-lockstep-api/src/lib.rs | 191 ++ sled-agent/src/bootstrap/config.rs | 1 + sled-agent/src/bootstrap/http_entrypoints.rs | 58 +- .../bootstrap/http_entrypoints_lockstep.rs | 168 ++ sled-agent/src/bootstrap/mod.rs | 1 + sled-agent/src/bootstrap/rack_ops.rs | 1 + sled-agent/src/bootstrap/server.rs | 48 +- wicketd/Cargo.toml | 1 + wicketd/src/context.rs | 9 + wicketd/src/http_entrypoints.rs | 38 +- wicketd/src/rss_config.rs | 53 +- 21 files changed, 2213 insertions(+), 76 deletions(-) create mode 100644 clients/bootstrap-agent-lockstep-client/Cargo.toml create mode 100644 clients/bootstrap-agent-lockstep-client/src/lib.rs create mode 100644 openapi/bootstrap-agent-lockstep.json create mode 100644 sled-agent/bootstrap-agent-lockstep-api/Cargo.toml create mode 100644 sled-agent/bootstrap-agent-lockstep-api/src/lib.rs create mode 100644 sled-agent/src/bootstrap/http_entrypoints_lockstep.rs diff --git a/Cargo.lock b/Cargo.lock index b44091cc465..f199f2cdfbe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1027,6 +1027,38 @@ dependencies = [ "uuid", ] +[[package]] +name = "bootstrap-agent-lockstep-api" +version = "0.1.0" +dependencies = [ + "dropshot", + "omicron-common", + "omicron-passwords", + "omicron-uuid-kinds", + "omicron-workspace-hack", + "schemars 0.8.22", + "serde", + "sled-hardware-types", +] + +[[package]] +name = "bootstrap-agent-lockstep-client" +version = "0.1.0" +dependencies = [ + "omicron-common", + "omicron-uuid-kinds", + "omicron-workspace-hack", + "oxnet", + "progenitor 0.10.0", + "regress", + "reqwest", + "schemars 0.8.22", + "serde", + "serde_json", + "sled-hardware-types", + "slog", +] + [[package]] name = "bstr" version = "1.10.0" @@ -8181,6 +8213,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bootstrap-agent-api", + "bootstrap-agent-lockstep-api", "camino", "clap", "clickhouse-admin-api", @@ -8814,6 +8847,7 @@ dependencies = [ "bootstore", "bootstrap-agent-api", "bootstrap-agent-client", + "bootstrap-agent-lockstep-api", "bytes", "camino", "camino-tempfile", @@ -16316,6 +16350,7 @@ dependencies = [ "async-trait", "base64 0.22.1", "bootstrap-agent-client", + "bootstrap-agent-lockstep-client", "buf-list", "bytes", "camino", diff --git a/Cargo.toml b/Cargo.toml index f13ab76d705..072128322f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "clickhouse-admin/test-utils", "clickhouse-admin/types/versions", "clients/bootstrap-agent-client", + "clients/bootstrap-agent-lockstep-client", "clients/clickhouse-admin-keeper-client", "clients/clickhouse-admin-server-client", "clients/clickhouse-admin-single-client", @@ -134,6 +135,7 @@ members = [ "sled-agent", "sled-agent/api", "sled-agent/bootstrap-agent-api", + "sled-agent/bootstrap-agent-lockstep-api", "sled-agent/config-reconciler", "sled-agent/health-monitor", "sled-agent/repo-depot-api", @@ -178,6 +180,7 @@ default-members = [ "clickhouse-admin/types/versions", "clickhouse-admin/test-utils", "clients/bootstrap-agent-client", + "clients/bootstrap-agent-lockstep-client", "clients/clickhouse-admin-keeper-client", "clients/clickhouse-admin-server-client", "clients/clickhouse-admin-single-client", @@ -306,6 +309,7 @@ default-members = [ "sled-agent", "sled-agent/api", "sled-agent/bootstrap-agent-api", + "sled-agent/bootstrap-agent-lockstep-api", "sled-agent/config-reconciler", "sled-agent/health-monitor", "sled-agent/repo-depot-api", @@ -402,7 +406,9 @@ bcs = "0.1.6" bincode = "1.3.3" bootstore = { path = "bootstore" } bootstrap-agent-api = { path = "sled-agent/bootstrap-agent-api" } +bootstrap-agent-lockstep-api = { path = "sled-agent/bootstrap-agent-lockstep-api" } bootstrap-agent-client = { path = "clients/bootstrap-agent-client" } +bootstrap-agent-lockstep-client = { path = "clients/bootstrap-agent-lockstep-client" } buf-list = { version = "1.0.3", features = ["tokio1"] } byteorder = "1.5.0" bytes = "1.10.1" diff --git a/clients/bootstrap-agent-lockstep-client/Cargo.toml b/clients/bootstrap-agent-lockstep-client/Cargo.toml new file mode 100644 index 00000000000..84fa9465902 --- /dev/null +++ b/clients/bootstrap-agent-lockstep-client/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "bootstrap-agent-lockstep-client" +version = "0.1.0" +edition.workspace = true +license = "MPL-2.0" + +[lints] +workspace = true + +[dependencies] +omicron-common.workspace = true +omicron-uuid-kinds.workspace = true +omicron-workspace-hack.workspace = true +oxnet.workspace = true +progenitor.workspace = true +regress.workspace = true +reqwest.workspace = true +schemars.workspace = true +serde.workspace = true +serde_json.workspace = true +sled-hardware-types.workspace = true +slog.workspace = true diff --git a/clients/bootstrap-agent-lockstep-client/src/lib.rs b/clients/bootstrap-agent-lockstep-client/src/lib.rs new file mode 100644 index 00000000000..1c99ea47b06 --- /dev/null +++ b/clients/bootstrap-agent-lockstep-client/src/lib.rs @@ -0,0 +1,72 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Interface for making API requests to the Bootstrap Agent Lockstep API + +progenitor::generate_api!( + spec = "../../openapi/bootstrap-agent-lockstep.json", + interface = Positional, + inner_type = slog::Logger, + pre_hook = (|log: &slog::Logger, request: &reqwest::Request| { + slog::debug!(log, "client request"; + "method" => %request.method(), + "uri" => %request.url(), + "body" => ?&request.body(), + ); + }), + post_hook = (|log: &slog::Logger, result: &Result<_, _>| { + slog::debug!(log, "client response"; "result" => ?result); + }), + derives = [schemars::JsonSchema], + crates = { + "omicron-uuid-kinds" = "*", + "oxnet" = "0.1.0", + }, + replace = { + AllowedSourceIps = omicron_common::api::external::AllowedSourceIps, + ImportExportPolicy = omicron_common::api::external::ImportExportPolicy, + }, +); + +impl omicron_common::api::external::ClientError for types::Error { + fn message(&self) -> String { + self.message.clone() + } +} + +impl From for sled_hardware_types::Baseboard { + fn from(value: types::Baseboard) -> Self { + match value { + types::Baseboard::Gimlet { identifier, model, revision } => { + sled_hardware_types::Baseboard::new_gimlet( + identifier, model, revision, + ) + } + types::Baseboard::Unknown => { + sled_hardware_types::Baseboard::unknown() + } + types::Baseboard::Pc { identifier, model } => { + sled_hardware_types::Baseboard::new_pc(identifier, model) + } + } + } +} + +impl From for types::Baseboard { + fn from(value: sled_hardware_types::Baseboard) -> Self { + match value { + sled_hardware_types::Baseboard::Gimlet { + identifier, + model, + revision, + } => types::Baseboard::Gimlet { identifier, model, revision }, + sled_hardware_types::Baseboard::Unknown => { + types::Baseboard::Unknown + } + sled_hardware_types::Baseboard::Pc { identifier, model } => { + types::Baseboard::Pc { identifier, model } + } + } + } +} diff --git a/dev-tools/dropshot-apis/Cargo.toml b/dev-tools/dropshot-apis/Cargo.toml index 1b19bab5adb..3418a2b7f08 100644 --- a/dev-tools/dropshot-apis/Cargo.toml +++ b/dev-tools/dropshot-apis/Cargo.toml @@ -6,6 +6,7 @@ edition.workspace = true [dependencies] anyhow.workspace = true bootstrap-agent-api.workspace = true +bootstrap-agent-lockstep-api.workspace = true camino.workspace = true clap.workspace = true clickhouse-admin-api.workspace = true diff --git a/dev-tools/dropshot-apis/src/main.rs b/dev-tools/dropshot-apis/src/main.rs index e70d1c023c4..304b049b42b 100644 --- a/dev-tools/dropshot-apis/src/main.rs +++ b/dev-tools/dropshot-apis/src/main.rs @@ -6,6 +6,7 @@ use std::process::ExitCode; use anyhow::{Context, anyhow}; use bootstrap_agent_api::*; +use bootstrap_agent_lockstep_api::*; use camino::Utf8PathBuf; use clap::Parser; use clickhouse_admin_api::*; @@ -66,6 +67,22 @@ fn all_apis() -> anyhow::Result { ident: "bootstrap-agent", extra_validation: None, }, + ManagedApiConfig { + title: "Bootstrap Agent Lockstep API", + versions: Versions::new_lockstep(semver::Version::new(0, 0, 1)), + metadata: ManagedApiMetadata { + description: Some( + "Lockstep API for rack initialization and reset", + ), + contact_url: Some("https://oxide.computer"), + contact_email: Some("api@oxide.computer"), + extra: to_value(ApiBoundary::Internal), + }, + api_description: + bootstrap_agent_lockstep_api_mod::stub_api_description, + ident: "bootstrap-agent-lockstep", + extra_validation: None, + }, ManagedApiConfig { title: "ClickHouse Cluster Admin Keeper API", versions: Versions::new_versioned( diff --git a/openapi/bootstrap-agent-lockstep.json b/openapi/bootstrap-agent-lockstep.json new file mode 100644 index 00000000000..c39c7a85bb6 --- /dev/null +++ b/openapi/bootstrap-agent-lockstep.json @@ -0,0 +1,1542 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Bootstrap Agent Lockstep API", + "description": "Lockstep API for rack initialization and reset", + "contact": { + "url": "https://oxide.computer", + "email": "api@oxide.computer" + }, + "version": "0.0.1" + }, + "paths": { + "/rack-initialize": { + "get": { + "summary": "Get the current status of rack initialization or reset.", + "operationId": "rack_initialization_status", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RackOperationStatus" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "summary": "Initialize the rack with the provided configuration.", + "operationId": "rack_initialize", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RackInitializeRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RackInitUuid" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Reset the rack to an unconfigured state.", + "operationId": "rack_reset", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RackResetUuid" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + } + }, + "components": { + "schemas": { + "AllowedSourceIps": { + "description": "Description of source IPs allowed to reach rack services.", + "oneOf": [ + { + "description": "Allow traffic from any external IP address.", + "type": "object", + "properties": { + "allow": { + "type": "string", + "enum": [ + "any" + ] + } + }, + "required": [ + "allow" + ] + }, + { + "description": "Restrict access to a specific set of source IP addresses or subnets.\n\nAll others are prevented from reaching rack services.", + "type": "object", + "properties": { + "allow": { + "type": "string", + "enum": [ + "list" + ] + }, + "ips": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IpNet" + } + } + }, + "required": [ + "allow", + "ips" + ] + } + ] + }, + "Baseboard": { + "description": "Describes properties that should uniquely identify a Gimlet.", + "oneOf": [ + { + "type": "object", + "properties": { + "identifier": { + "type": "string" + }, + "model": { + "type": "string" + }, + "revision": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "gimlet" + ] + } + }, + "required": [ + "identifier", + "model", + "revision", + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "unknown" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "identifier": { + "type": "string" + }, + "model": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "pc" + ] + } + }, + "required": [ + "identifier", + "model", + "type" + ] + } + ] + }, + "BfdMode": { + "description": "BFD connection mode.", + "type": "string", + "enum": [ + "single_hop", + "multi_hop" + ] + }, + "BfdPeerConfig": { + "type": "object", + "properties": { + "detection_threshold": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "local": { + "nullable": true, + "type": "string", + "format": "ip" + }, + "mode": { + "$ref": "#/components/schemas/BfdMode" + }, + "remote": { + "type": "string", + "format": "ip" + }, + "required_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "switch": { + "$ref": "#/components/schemas/SwitchLocation" + } + }, + "required": [ + "detection_threshold", + "mode", + "remote", + "required_rx", + "switch" + ] + }, + "BgpConfig": { + "type": "object", + "properties": { + "asn": { + "description": "The autonomous system number for the BGP configuration.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "checker": { + "nullable": true, + "description": "Checker to apply to incoming messages.", + "default": null, + "type": "string" + }, + "originate": { + "description": "The set of prefixes for the BGP router to originate.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv4Net" + } + }, + "shaper": { + "nullable": true, + "description": "Shaper to apply to outgoing messages.", + "default": null, + "type": "string" + } + }, + "required": [ + "asn", + "originate" + ] + }, + "BgpPeerConfig": { + "type": "object", + "properties": { + "addr": { + "description": "Address of the peer. Use `Ipv4Addr::UNSPECIFIED` to indicate an unnumbered BGP session established over the interface specified by `port`.", + "type": "string", + "format": "ipv4" + }, + "allowed_export": { + "description": "Define export policy for a peer.", + "default": { + "type": "no_filtering" + }, + "allOf": [ + { + "$ref": "#/components/schemas/ImportExportPolicy" + } + ] + }, + "allowed_import": { + "description": "Define import policy for a peer.", + "default": { + "type": "no_filtering" + }, + "allOf": [ + { + "$ref": "#/components/schemas/ImportExportPolicy" + } + ] + }, + "asn": { + "description": "The autonomous system number of the router the peer belongs to.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "communities": { + "description": "Include the provided communities in updates sent to the peer.", + "default": [], + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "connect_retry": { + "nullable": true, + "description": "The interval in seconds between peer connection retry attempts.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "delay_open": { + "nullable": true, + "description": "How long to delay sending open messages to a peer. In seconds.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "enforce_first_as": { + "description": "Enforce that the first AS in paths received from this peer is the peer's AS.", + "default": false, + "type": "boolean" + }, + "hold_time": { + "nullable": true, + "description": "How long to keep a session alive without a keepalive in seconds. Defaults to 6.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "idle_hold_time": { + "nullable": true, + "description": "How long to keep a peer in idle after a state machine reset in seconds.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "keepalive": { + "nullable": true, + "description": "The interval to send keepalive messages at.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "local_pref": { + "nullable": true, + "description": "Apply a local preference to routes received from this peer.", + "default": null, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "md5_auth_key": { + "nullable": true, + "description": "Use the given key for TCP-MD5 authentication with the peer.", + "default": null, + "type": "string" + }, + "min_ttl": { + "nullable": true, + "description": "Require messages from a peer have a minimum IP time to live field.", + "default": null, + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "multi_exit_discriminator": { + "nullable": true, + "description": "Apply the provided multi-exit discriminator (MED) updates sent to the peer.", + "default": null, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "port": { + "description": "Switch port the peer is reachable on.", + "type": "string" + }, + "remote_asn": { + "nullable": true, + "description": "Require that a peer has a specified ASN.", + "default": null, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "vlan_id": { + "nullable": true, + "description": "Associate a VLAN ID with a BGP peer session.", + "default": null, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "addr", + "asn", + "port" + ] + }, + "BootstrapAddressDiscovery": { + "description": "Describes how bootstrap addresses should be collected during RSS.", + "oneOf": [ + { + "description": "Ignore all bootstrap addresses except our own.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "only_ours" + ] + } + }, + "required": [ + "type" + ] + }, + { + "description": "Ignore all bootstrap addresses except the following.", + "type": "object", + "properties": { + "addrs": { + "type": "array", + "items": { + "type": "string", + "format": "ipv6" + }, + "uniqueItems": true + }, + "type": { + "type": "string", + "enum": [ + "only_these" + ] + } + }, + "required": [ + "addrs", + "type" + ] + } + ] + }, + "Certificate": { + "type": "object", + "properties": { + "cert": { + "type": "string" + }, + "key": { + "type": "string" + } + }, + "required": [ + "cert", + "key" + ] + }, + "Error": { + "description": "Error information from a response.", + "type": "object", + "properties": { + "error_code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "request_id": { + "type": "string" + } + }, + "required": [ + "message", + "request_id" + ] + }, + "ImportExportPolicy": { + "description": "Define policy relating to the import and export of prefixes from a BGP peer.", + "oneOf": [ + { + "description": "Do not perform any filtering.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "no_filtering" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "allow" + ] + }, + "value": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IpNet" + } + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "IpNet": { + "x-rust-type": { + "crate": "oxnet", + "path": "oxnet::IpNet", + "version": "0.1.0" + }, + "oneOf": [ + { + "title": "v4", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4Net" + } + ] + }, + { + "title": "v6", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Net" + } + ] + } + ] + }, + "IpRange": { + "oneOf": [ + { + "title": "v4", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4Range" + } + ] + }, + { + "title": "v6", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Range" + } + ] + } + ] + }, + "Ipv4Net": { + "example": "192.168.1.0/24", + "title": "An IPv4 subnet", + "description": "An IPv4 subnet, including prefix and prefix length", + "x-rust-type": { + "crate": "oxnet", + "path": "oxnet::Ipv4Net", + "version": "0.1.0" + }, + "type": "string", + "pattern": "^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])/([0-9]|1[0-9]|2[0-9]|3[0-2])$" + }, + "Ipv4Range": { + "description": "A non-decreasing IPv4 address range, inclusive of both ends.\n\nThe first address must be less than or equal to the last address.", + "type": "object", + "properties": { + "first": { + "type": "string", + "format": "ipv4" + }, + "last": { + "type": "string", + "format": "ipv4" + } + }, + "required": [ + "first", + "last" + ] + }, + "Ipv6Net": { + "example": "fd12:3456::/64", + "title": "An IPv6 subnet", + "description": "An IPv6 subnet, including prefix and subnet mask", + "x-rust-type": { + "crate": "oxnet", + "path": "oxnet::Ipv6Net", + "version": "0.1.0" + }, + "type": "string", + "pattern": "^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$" + }, + "Ipv6Range": { + "description": "A non-decreasing IPv6 address range, inclusive of both ends.\n\nThe first address must be less than or equal to the last address.", + "type": "object", + "properties": { + "first": { + "type": "string", + "format": "ipv6" + }, + "last": { + "type": "string", + "format": "ipv6" + } + }, + "required": [ + "first", + "last" + ] + }, + "LldpAdminStatus": { + "description": "To what extent should this port participate in LLDP", + "type": "string", + "enum": [ + "enabled", + "disabled", + "rx_only", + "tx_only" + ] + }, + "LldpPortConfig": { + "description": "Per-port LLDP configuration settings. Only the \"status\" setting is mandatory. All other fields have natural defaults or may be inherited from the switch.", + "type": "object", + "properties": { + "chassis_id": { + "nullable": true, + "description": "Chassis ID to advertise. If this is set, it will be advertised as a LocallyAssigned ID type. If this is not set, it will be inherited from the switch-level settings.", + "type": "string" + }, + "management_addrs": { + "nullable": true, + "description": "Management IP addresses to advertise. If this is not set, it will be inherited from the switch-level settings.", + "type": "array", + "items": { + "type": "string", + "format": "ip" + } + }, + "port_description": { + "nullable": true, + "description": "Port description to advertise. If this is not set, no description will be advertised.", + "type": "string" + }, + "port_id": { + "nullable": true, + "description": "Port ID to advertise. If this is set, it will be advertised as a LocallyAssigned ID type. If this is not set, it will be set to the port name. e.g., qsfp0/0.", + "type": "string" + }, + "status": { + "description": "To what extent should this port participate in LLDP", + "allOf": [ + { + "$ref": "#/components/schemas/LldpAdminStatus" + } + ] + }, + "system_description": { + "nullable": true, + "description": "System description to advertise. If this is not set, it will be inherited from the switch-level settings.", + "type": "string" + }, + "system_name": { + "nullable": true, + "description": "System name to advertise. If this is not set, it will be inherited from the switch-level settings.", + "type": "string" + } + }, + "required": [ + "status" + ] + }, + "Name": { + "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, but they may contain a UUID. They can be at most 63 characters long.", + "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-zA-Z0-9-]*[a-zA-Z0-9]+)?$", + "minLength": 1, + "maxLength": 63 + }, + "NewPasswordHash": { + "title": "A password hash in PHC string format", + "description": "Password hashes must be in PHC (Password Hashing Competition) string format. Passwords must be hashed with Argon2id. Password hashes may be rejected if the parameters appear not to be secure enough.", + "type": "string" + }, + "PortConfigV2": { + "type": "object", + "properties": { + "addresses": { + "description": "This port's addresses and optional vlan IDs", + "type": "array", + "items": { + "$ref": "#/components/schemas/UplinkAddressConfig" + } + }, + "autoneg": { + "description": "Whether or not to set autonegotiation", + "default": false, + "type": "boolean" + }, + "bgp_peers": { + "description": "BGP peers on this port", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpPeerConfig" + } + }, + "lldp": { + "nullable": true, + "description": "LLDP configuration for this port", + "allOf": [ + { + "$ref": "#/components/schemas/LldpPortConfig" + } + ] + }, + "port": { + "description": "Nmae of the port this config applies to.", + "type": "string" + }, + "routes": { + "description": "The set of routes associated with this port.", + "type": "array", + "items": { + "$ref": "#/components/schemas/RouteConfig" + } + }, + "switch": { + "description": "Switch the port belongs to.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchLocation" + } + ] + }, + "tx_eq": { + "nullable": true, + "description": "TX-EQ configuration for this port", + "allOf": [ + { + "$ref": "#/components/schemas/TxEqConfig" + } + ] + }, + "uplink_port_fec": { + "nullable": true, + "description": "Port forward error correction type.", + "allOf": [ + { + "$ref": "#/components/schemas/PortFec" + } + ] + }, + "uplink_port_speed": { + "description": "Port speed.", + "allOf": [ + { + "$ref": "#/components/schemas/PortSpeed" + } + ] + } + }, + "required": [ + "addresses", + "bgp_peers", + "port", + "routes", + "switch", + "uplink_port_speed" + ] + }, + "PortFec": { + "description": "Switchport FEC options", + "type": "string", + "enum": [ + "firecode", + "none", + "rs" + ] + }, + "PortSpeed": { + "description": "Switchport Speed options", + "type": "string", + "enum": [ + "speed0_g", + "speed1_g", + "speed10_g", + "speed25_g", + "speed40_g", + "speed50_g", + "speed100_g", + "speed200_g", + "speed400_g" + ] + }, + "RackInitUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::RackInitUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "RackInitializeRequest": { + "description": "Rack initialization request.", + "type": "object", + "properties": { + "allowed_source_ips": { + "description": "IPs or subnets allowed to make requests to user-facing services.", + "default": { + "allow": "any" + }, + "allOf": [ + { + "$ref": "#/components/schemas/AllowedSourceIps" + } + ] + }, + "bootstrap_discovery": { + "description": "Describes how bootstrap addresses should be collected during RSS.", + "allOf": [ + { + "$ref": "#/components/schemas/BootstrapAddressDiscovery" + } + ] + }, + "dns_servers": { + "description": "The external DNS server addresses.", + "type": "array", + "items": { + "type": "string", + "format": "ip" + } + }, + "external_certificates": { + "description": "Initial TLS certificates for the external API.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Certificate" + } + }, + "external_dns_ips": { + "description": "Service IP addresses on which we run external DNS servers.\n\nEach address must be present in `internal_services_ip_pool_ranges`.", + "type": "array", + "items": { + "type": "string", + "format": "ip" + } + }, + "external_dns_zone_name": { + "description": "DNS name for the DNS zone delegated to the rack for external DNS.", + "type": "string" + }, + "internal_services_ip_pool_ranges": { + "description": "Ranges of the service IP pool which may be used for internal services.", + "type": "array", + "items": { + "$ref": "#/components/schemas/IpRange" + } + }, + "ntp_servers": { + "description": "The external NTP server addresses.", + "type": "array", + "items": { + "type": "string" + } + }, + "rack_network_config": { + "description": "Initial rack network configuration.", + "allOf": [ + { + "$ref": "#/components/schemas/RackNetworkConfigV2" + } + ] + }, + "recovery_silo": { + "description": "Configuration of the Recovery Silo (the initial Silo).", + "allOf": [ + { + "$ref": "#/components/schemas/RecoverySiloConfig" + } + ] + }, + "trust_quorum_peers": { + "nullable": true, + "description": "The set of peer_ids required to initialize trust quorum.\n\nThe value is `None` if we are not using trust quorum.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Baseboard" + } + } + }, + "required": [ + "bootstrap_discovery", + "dns_servers", + "external_certificates", + "external_dns_ips", + "external_dns_zone_name", + "internal_services_ip_pool_ranges", + "ntp_servers", + "rack_network_config", + "recovery_silo" + ] + }, + "RackNetworkConfigV2": { + "description": "Initial network configuration", + "type": "object", + "properties": { + "bfd": { + "description": "BFD configuration for connecting the rack to external networks", + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/BfdPeerConfig" + } + }, + "bgp": { + "description": "BGP configurations for connecting the rack to external networks", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpConfig" + } + }, + "infra_ip_first": { + "description": "First ip address to be used for configuring network infrastructure", + "type": "string", + "format": "ipv4" + }, + "infra_ip_last": { + "description": "Last ip address to be used for configuring network infrastructure", + "type": "string", + "format": "ipv4" + }, + "ports": { + "description": "Uplinks for connecting the rack to external networks", + "type": "array", + "items": { + "$ref": "#/components/schemas/PortConfigV2" + } + }, + "rack_subnet": { + "$ref": "#/components/schemas/Ipv6Net" + } + }, + "required": [ + "bgp", + "infra_ip_first", + "infra_ip_last", + "ports", + "rack_subnet" + ] + }, + "RackOperationStatus": { + "description": "Current status of any rack-level operation being performed by this bootstrap agent.", + "oneOf": [ + { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/RackInitUuid" + }, + "status": { + "type": "string", + "enum": [ + "initializing" + ] + }, + "step": { + "$ref": "#/components/schemas/RssStep" + } + }, + "required": [ + "id", + "status", + "step" + ] + }, + { + "description": "`id` will be none if the rack was already initialized on startup.", + "type": "object", + "properties": { + "id": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/RackInitUuid" + } + ] + }, + "status": { + "type": "string", + "enum": [ + "initialized" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/RackInitUuid" + }, + "message": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "initialization_failed" + ] + } + }, + "required": [ + "id", + "message", + "status" + ] + }, + { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/RackInitUuid" + }, + "status": { + "type": "string", + "enum": [ + "initialization_panicked" + ] + } + }, + "required": [ + "id", + "status" + ] + }, + { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/RackResetUuid" + }, + "status": { + "type": "string", + "enum": [ + "resetting" + ] + } + }, + "required": [ + "id", + "status" + ] + }, + { + "description": "`reset_id` will be None if the rack is in an uninitialized-on-startup, or Some if it is in an uninitialized state due to a reset operation completing.", + "type": "object", + "properties": { + "reset_id": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/RackResetUuid" + } + ] + }, + "status": { + "type": "string", + "enum": [ + "uninitialized" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/RackResetUuid" + }, + "message": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "reset_failed" + ] + } + }, + "required": [ + "id", + "message", + "status" + ] + }, + { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/RackResetUuid" + }, + "status": { + "type": "string", + "enum": [ + "reset_panicked" + ] + } + }, + "required": [ + "id", + "status" + ] + } + ] + }, + "RackResetUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::RackResetUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "RecoverySiloConfig": { + "description": "Configuration for the recovery silo created during rack setup.", + "type": "object", + "properties": { + "silo_name": { + "$ref": "#/components/schemas/Name" + }, + "user_name": { + "$ref": "#/components/schemas/UserId" + }, + "user_password_hash": { + "$ref": "#/components/schemas/NewPasswordHash" + } + }, + "required": [ + "silo_name", + "user_name", + "user_password_hash" + ] + }, + "RouteConfig": { + "type": "object", + "properties": { + "destination": { + "description": "The destination of the route.", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + }, + "nexthop": { + "description": "The nexthop/gateway address.", + "type": "string", + "format": "ip" + }, + "rib_priority": { + "nullable": true, + "description": "The RIB priority (i.e. Admin Distance) associated with this route.", + "default": null, + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "vlan_id": { + "nullable": true, + "description": "The VLAN id associated with this route.", + "default": null, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "destination", + "nexthop" + ] + }, + "RssStep": { + "description": "Steps we go through during initial rack setup.", + "oneOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "requested" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "starting" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "load_existing_plan" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "create_sled_plan" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "init_trust_quorum" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "network_config_update" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "sled_init" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "init_dns" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "configure_dns" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "init_ntp" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "wait_for_time_sync" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "wait_for_database" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "cluster_init" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "zones_init" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "nexus_handoff" + ] + } + }, + "required": [ + "status" + ] + } + ] + }, + "SwitchLocation": { + "description": "Identifies switch physical location", + "oneOf": [ + { + "description": "Switch in upper slot", + "type": "string", + "enum": [ + "switch0" + ] + }, + { + "description": "Switch in lower slot", + "type": "string", + "enum": [ + "switch1" + ] + } + ] + }, + "TxEqConfig": { + "description": "Per-port tx-eq overrides. This can be used to fine-tune the transceiver equalization settings to improve signal integrity.", + "type": "object", + "properties": { + "main": { + "nullable": true, + "description": "Main tap", + "type": "integer", + "format": "int32" + }, + "post1": { + "nullable": true, + "description": "Post-cursor tap1", + "type": "integer", + "format": "int32" + }, + "post2": { + "nullable": true, + "description": "Post-cursor tap2", + "type": "integer", + "format": "int32" + }, + "pre1": { + "nullable": true, + "description": "Pre-cursor tap1", + "type": "integer", + "format": "int32" + }, + "pre2": { + "nullable": true, + "description": "Pre-cursor tap2", + "type": "integer", + "format": "int32" + } + } + }, + "UplinkAddressConfig": { + "type": "object", + "properties": { + "address": { + "$ref": "#/components/schemas/IpNet" + }, + "vlan_id": { + "nullable": true, + "description": "The VLAN id (if any) associated with this address.", + "default": null, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "address" + ] + }, + "UserId": { + "title": "A username for a local-only user", + "description": "Usernames must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Usernames cannot be a UUID, but they may contain a UUID. They can be at most 63 characters long.", + "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-zA-Z0-9-]*[a-zA-Z0-9]+)?$", + "minLength": 1, + "maxLength": 63 + } + }, + "responses": { + "Error": { + "description": "Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } +} diff --git a/sled-agent/Cargo.toml b/sled-agent/Cargo.toml index 09c81f36c1a..8c93bc47536 100644 --- a/sled-agent/Cargo.toml +++ b/sled-agent/Cargo.toml @@ -16,6 +16,7 @@ base64.workspace = true bootstore.workspace = true bootstrap-agent-api.workspace = true bootstrap-agent-client.workspace = true +bootstrap-agent-lockstep-api.workspace = true bytes.workspace = true camino.workspace = true camino-tempfile.workspace = true diff --git a/sled-agent/bootstrap-agent-api/src/lib.rs b/sled-agent/bootstrap-agent-api/src/lib.rs index 845408b87ed..12f9dae7c56 100644 --- a/sled-agent/bootstrap-agent-api/src/lib.rs +++ b/sled-agent/bootstrap-agent-api/src/lib.rs @@ -53,6 +53,8 @@ pub trait BootstrapAgentApi { ) -> Result>, HttpError>; /// Get the current status of rack initialization or reset. + /// + /// Deprecated. Do not use. Moved to bootstrap agent lockstep API. #[endpoint { method = GET, path = "/rack-initialize", @@ -62,6 +64,8 @@ pub trait BootstrapAgentApi { ) -> Result, HttpError>; /// Initialize the rack with the provided configuration. + /// + /// Deprecated. Do not use. Moved to bootstrap agent lockstep API. #[endpoint { method = POST, path = "/rack-initialize", @@ -72,6 +76,8 @@ pub trait BootstrapAgentApi { ) -> Result, HttpError>; /// Reset the rack to an unconfigured state. + /// + /// Deprecated. Do not use. Moved to bootstrap agent lockstep API. #[endpoint { method = DELETE, path = "/rack-initialize", diff --git a/sled-agent/bootstrap-agent-lockstep-api/Cargo.toml b/sled-agent/bootstrap-agent-lockstep-api/Cargo.toml new file mode 100644 index 00000000000..0d83e2ad484 --- /dev/null +++ b/sled-agent/bootstrap-agent-lockstep-api/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "bootstrap-agent-lockstep-api" +version = "0.1.0" +edition.workspace = true +license = "MPL-2.0" + +[lints] +workspace = true + +[dependencies] +dropshot.workspace = true +omicron-common.workspace = true +omicron-passwords.workspace = true +omicron-uuid-kinds.workspace = true +omicron-workspace-hack.workspace = true +schemars.workspace = true +serde.workspace = true +sled-hardware-types.workspace = true diff --git a/sled-agent/bootstrap-agent-lockstep-api/src/lib.rs b/sled-agent/bootstrap-agent-lockstep-api/src/lib.rs new file mode 100644 index 00000000000..2deda9562ac --- /dev/null +++ b/sled-agent/bootstrap-agent-lockstep-api/src/lib.rs @@ -0,0 +1,191 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Lockstep API for bootstrap agent rack initialization. +//! +//! This API handles rack initialization and reset operations. It is a lockstep +//! API as we do not expect rack initialization functions to be called during +//! and upgrade. Furthermore when rack initialization functions are called +//! it's expected that software components are on the same version. + +use std::collections::BTreeSet; +use std::net::{IpAddr, Ipv6Addr}; + +use dropshot::{HttpError, HttpResponseOk, RequestContext, TypedBody}; +use omicron_common::address::IpRange; +use omicron_common::api::external::{AllowedSourceIps, Name, UserId}; +use omicron_common::api::internal::nexus::Certificate; +use omicron_common::api::internal::shared::RackNetworkConfig; +use omicron_uuid_kinds::{RackInitUuid, RackResetUuid}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use sled_hardware_types::Baseboard; + +/// Configuration for the recovery silo created during rack setup. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct RecoverySiloConfig { + pub silo_name: Name, + pub user_name: UserId, + pub user_password_hash: omicron_passwords::NewPasswordHash, +} + +/// Describes how bootstrap addresses should be collected during RSS. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case", tag = "type")] +pub enum BootstrapAddressDiscovery { + /// Ignore all bootstrap addresses except our own. + OnlyOurs, + /// Ignore all bootstrap addresses except the following. + OnlyThese { addrs: BTreeSet }, +} + +/// Rack initialization request. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] +pub struct RackInitializeRequest { + /// The set of peer_ids required to initialize trust quorum. + /// + /// The value is `None` if we are not using trust quorum. + pub trust_quorum_peers: Option>, + + /// Describes how bootstrap addresses should be collected during RSS. + pub bootstrap_discovery: BootstrapAddressDiscovery, + + /// The external NTP server addresses. + pub ntp_servers: Vec, + + /// The external DNS server addresses. + pub dns_servers: Vec, + + /// Ranges of the service IP pool which may be used for internal services. + pub internal_services_ip_pool_ranges: Vec, + + /// Service IP addresses on which we run external DNS servers. + /// + /// Each address must be present in `internal_services_ip_pool_ranges`. + pub external_dns_ips: Vec, + + /// DNS name for the DNS zone delegated to the rack for external DNS. + pub external_dns_zone_name: String, + + /// Initial TLS certificates for the external API. + pub external_certificates: Vec, + + /// Configuration of the Recovery Silo (the initial Silo). + pub recovery_silo: RecoverySiloConfig, + + /// Initial rack network configuration. + pub rack_network_config: RackNetworkConfig, + + /// IPs or subnets allowed to make requests to user-facing services. + #[serde(default = "default_allowed_source_ips")] + pub allowed_source_ips: AllowedSourceIps, +} + +const fn default_allowed_source_ips() -> AllowedSourceIps { + AllowedSourceIps::Any +} + +/// Current status of any rack-level operation being performed by this bootstrap +/// agent. +#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] +#[serde(tag = "status", rename_all = "snake_case")] +pub enum RackOperationStatus { + Initializing { + id: RackInitUuid, + step: RssStep, + }, + /// `id` will be none if the rack was already initialized on startup. + Initialized { + id: Option, + }, + InitializationFailed { + id: RackInitUuid, + message: String, + }, + InitializationPanicked { + id: RackInitUuid, + }, + Resetting { + id: RackResetUuid, + }, + /// `reset_id` will be None if the rack is in an uninitialized-on-startup, + /// or Some if it is in an uninitialized state due to a reset operation + /// completing. + Uninitialized { + reset_id: Option, + }, + ResetFailed { + id: RackResetUuid, + message: String, + }, + ResetPanicked { + id: RackResetUuid, + }, +} + +/// Steps we go through during initial rack setup. +#[derive( + Copy, + Clone, + Debug, + Deserialize, + Eq, + Hash, + JsonSchema, + Ord, + PartialEq, + PartialOrd, + Serialize, +)] +#[serde(tag = "status", rename_all = "snake_case")] +pub enum RssStep { + Requested, + Starting, + LoadExistingPlan, + CreateSledPlan, + InitTrustQuorum, + NetworkConfigUpdate, + SledInit, + InitDns, + ConfigureDns, + InitNtp, + WaitForTimeSync, + WaitForDatabase, + ClusterInit, + ZonesInit, + NexusHandoff, +} + +#[dropshot::api_description] +pub trait BootstrapAgentLockstepApi { + type Context; + + /// Get the current status of rack initialization or reset. + #[endpoint { + method = GET, + path = "/rack-initialize", + }] + async fn rack_initialization_status( + rqctx: RequestContext, + ) -> Result, HttpError>; + + /// Initialize the rack with the provided configuration. + #[endpoint { + method = POST, + path = "/rack-initialize", + }] + async fn rack_initialize( + rqctx: RequestContext, + body: TypedBody, + ) -> Result, HttpError>; + + /// Reset the rack to an unconfigured state. + #[endpoint { + method = DELETE, + path = "/rack-initialize", + }] + async fn rack_reset( + rqctx: RequestContext, + ) -> Result, HttpError>; +} diff --git a/sled-agent/src/bootstrap/config.rs b/sled-agent/src/bootstrap/config.rs index 3b6b5e3e443..cba8d58d1f7 100644 --- a/sled-agent/src/bootstrap/config.rs +++ b/sled-agent/src/bootstrap/config.rs @@ -5,6 +5,7 @@ //! Interfaces for working with bootstrap agent configuration pub const BOOTSTRAP_AGENT_HTTP_PORT: u16 = 80; +pub const BOOTSTRAP_AGENT_LOCKSTEP_PORT: u16 = 8080; pub const BOOTSTRAP_AGENT_RACK_INIT_PORT: u16 = 12346; pub const BOOTSTORE_PORT: u16 = 12347; pub const TRUST_QUORUM_PORT: u16 = 12349; diff --git a/sled-agent/src/bootstrap/http_entrypoints.rs b/sled-agent/src/bootstrap/http_entrypoints.rs index 5e6f41b0737..fe20bffdd68 100644 --- a/sled-agent/src/bootstrap/http_entrypoints.rs +++ b/sled-agent/src/bootstrap/http_entrypoints.rs @@ -16,6 +16,7 @@ use bootstore::schemes::v0 as bootstore; use bootstrap_agent_api::BootstrapAgentApi; use bootstrap_agent_api::Component; use bootstrap_agent_api::bootstrap_agent_api_mod; +use dropshot::ClientErrorStatusCode; use dropshot::{ ApiDescription, HttpError, HttpResponseOk, HttpResponseUpdatedNoContent, RequestContext, TypedBody, @@ -36,6 +37,7 @@ use std::net::Ipv6Addr; use tokio::sync::mpsc::error::TrySendError; use tokio::sync::{mpsc, oneshot}; +#[derive(Clone)] pub(crate) struct BootstrapServerContext { pub(crate) base_log: Logger, pub(crate) global_zone_bootstrap_ip: Ipv6Addr, @@ -99,43 +101,43 @@ impl BootstrapAgentApi for BootstrapAgentImpl { } async fn rack_initialization_status( - rqctx: RequestContext, + _rqctx: RequestContext, ) -> Result, HttpError> { - let ctx = rqctx.context(); - let status = ctx.rss_access.operation_status(); - Ok(HttpResponseOk(status)) + // This endpoint has moved to the lockstep API on port 8080. + Err(HttpError::for_client_error( + None, + ClientErrorStatusCode::GONE, + "This endpoint has been permanently moved to the bootstrap agent \ + lockstep API on port 8080" + .to_string(), + )) } async fn rack_initialize( - rqctx: RequestContext, - body: TypedBody, + _rqctx: RequestContext, + _body: TypedBody, ) -> Result, HttpError> { - // Note that if we are performing rack initialization in - // response to an external request, we assume we are not - // skipping timesync. - const SKIP_TIMESYNC: bool = false; - let ctx = rqctx.context(); - let request = - RackInitializeRequestParams::new(body.into_inner(), SKIP_TIMESYNC); - let id = ctx - .start_rack_initialize(request) - .map_err(|err| HttpError::for_bad_request(None, err.to_string()))?; - Ok(HttpResponseOk(id)) + // This endpoint has moved to the lockstep API on port 8080. + Err(HttpError::for_client_error( + None, + ClientErrorStatusCode::GONE, + "This endpoint has been permanently moved to the bootstrap agent \ + lockstep API on port 8080" + .to_string(), + )) } async fn rack_reset( - rqctx: RequestContext, + _rqctx: RequestContext, ) -> Result, HttpError> { - let ctx = rqctx.context(); - let id = ctx - .rss_access - .start_reset( - &ctx.base_log, - ctx.sprockets.clone(), - ctx.global_zone_bootstrap_ip, - ) - .map_err(|err| HttpError::for_bad_request(None, err.to_string()))?; - Ok(HttpResponseOk(id)) + // This endpoint has moved to the lockstep API on port 8080. + Err(HttpError::for_client_error( + None, + ClientErrorStatusCode::GONE, + "This endpoint has been permanently moved to the bootstrap agent \ + lockstep API on port 8080" + .to_string(), + )) } async fn sled_reset( diff --git a/sled-agent/src/bootstrap/http_entrypoints_lockstep.rs b/sled-agent/src/bootstrap/http_entrypoints_lockstep.rs new file mode 100644 index 00000000000..a0ba7e9f3a7 --- /dev/null +++ b/sled-agent/src/bootstrap/http_entrypoints_lockstep.rs @@ -0,0 +1,168 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! HTTP entrypoint functions for the bootstrap agent's lockstep API. +//! +//! This API handles rack initialization and reset operations. It is a lockstep +//! API, meaning the client and server are always deployed together and only +//! need to support a single version. + +use super::http_entrypoints::BootstrapServerContext; +use bootstrap_agent_lockstep_api::BootstrapAgentLockstepApi; +use bootstrap_agent_lockstep_api::bootstrap_agent_lockstep_api_mod; +use bootstrap_agent_lockstep_api::{ + RackInitializeRequest as LockstepRackInitializeRequest, + RackOperationStatus as LockstepRackOperationStatus, + RssStep as LockstepRssStep, +}; +use dropshot::{ + ApiDescription, HttpError, HttpResponseOk, RequestContext, TypedBody, +}; +use omicron_uuid_kinds::RackInitUuid; +use omicron_uuid_kinds::RackResetUuid; +use sled_agent_types::rack_init::{ + BootstrapAddressDiscovery, RackInitializeRequest, + RackInitializeRequestParams, RecoverySiloConfig, +}; +use sled_agent_types::rack_ops::{RackOperationStatus, RssStep}; + +/// Returns a description of the bootstrap agent lockstep API +pub(crate) fn api() -> ApiDescription { + bootstrap_agent_lockstep_api_mod::api_description::< + BootstrapAgentLockstepImpl, + >() + .expect("registered entrypoints successfully") +} + +enum BootstrapAgentLockstepImpl {} + +/// Convert from the lockstep API's RackInitializeRequest to the internal type. +fn convert_rack_initialize_request( + lockstep: LockstepRackInitializeRequest, +) -> RackInitializeRequest { + RackInitializeRequest { + trust_quorum_peers: lockstep.trust_quorum_peers, + bootstrap_discovery: match lockstep.bootstrap_discovery { + bootstrap_agent_lockstep_api::BootstrapAddressDiscovery::OnlyOurs => { + BootstrapAddressDiscovery::OnlyOurs + } + bootstrap_agent_lockstep_api::BootstrapAddressDiscovery::OnlyThese { + addrs, + } => BootstrapAddressDiscovery::OnlyThese { addrs }, + }, + ntp_servers: lockstep.ntp_servers, + dns_servers: lockstep.dns_servers, + internal_services_ip_pool_ranges: lockstep.internal_services_ip_pool_ranges, + external_dns_ips: lockstep.external_dns_ips, + external_dns_zone_name: lockstep.external_dns_zone_name, + external_certificates: lockstep.external_certificates, + recovery_silo: RecoverySiloConfig { + silo_name: lockstep.recovery_silo.silo_name, + user_name: lockstep.recovery_silo.user_name, + user_password_hash: lockstep.recovery_silo.user_password_hash, + }, + rack_network_config: lockstep.rack_network_config, + allowed_source_ips: lockstep.allowed_source_ips, + } +} + +/// Convert from the internal RackOperationStatus to the lockstep API's type. +fn convert_rack_operation_status( + status: RackOperationStatus, +) -> LockstepRackOperationStatus { + match status { + RackOperationStatus::Initializing { id, step } => { + LockstepRackOperationStatus::Initializing { + id, + step: convert_rss_step(step), + } + } + RackOperationStatus::Initialized { id } => { + LockstepRackOperationStatus::Initialized { id } + } + RackOperationStatus::InitializationFailed { id, message } => { + LockstepRackOperationStatus::InitializationFailed { id, message } + } + RackOperationStatus::InitializationPanicked { id } => { + LockstepRackOperationStatus::InitializationPanicked { id } + } + RackOperationStatus::Resetting { id } => { + LockstepRackOperationStatus::Resetting { id } + } + RackOperationStatus::Uninitialized { reset_id } => { + LockstepRackOperationStatus::Uninitialized { reset_id } + } + RackOperationStatus::ResetFailed { id, message } => { + LockstepRackOperationStatus::ResetFailed { id, message } + } + RackOperationStatus::ResetPanicked { id } => { + LockstepRackOperationStatus::ResetPanicked { id } + } + } +} + +fn convert_rss_step(step: RssStep) -> LockstepRssStep { + match step { + RssStep::Requested => LockstepRssStep::Requested, + RssStep::Starting => LockstepRssStep::Starting, + RssStep::LoadExistingPlan => LockstepRssStep::LoadExistingPlan, + RssStep::CreateSledPlan => LockstepRssStep::CreateSledPlan, + RssStep::InitTrustQuorum => LockstepRssStep::InitTrustQuorum, + RssStep::NetworkConfigUpdate => LockstepRssStep::NetworkConfigUpdate, + RssStep::SledInit => LockstepRssStep::SledInit, + RssStep::InitDns => LockstepRssStep::InitDns, + RssStep::ConfigureDns => LockstepRssStep::ConfigureDns, + RssStep::InitNtp => LockstepRssStep::InitNtp, + RssStep::WaitForTimeSync => LockstepRssStep::WaitForTimeSync, + RssStep::WaitForDatabase => LockstepRssStep::WaitForDatabase, + RssStep::ClusterInit => LockstepRssStep::ClusterInit, + RssStep::ZonesInit => LockstepRssStep::ZonesInit, + RssStep::NexusHandoff => LockstepRssStep::NexusHandoff, + } +} + +impl BootstrapAgentLockstepApi for BootstrapAgentLockstepImpl { + type Context = BootstrapServerContext; + + async fn rack_initialization_status( + rqctx: RequestContext, + ) -> Result, HttpError> { + let ctx = rqctx.context(); + let status = ctx.rss_access.operation_status(); + Ok(HttpResponseOk(convert_rack_operation_status(status))) + } + + async fn rack_initialize( + rqctx: RequestContext, + body: TypedBody, + ) -> Result, HttpError> { + // Note that if we are performing rack initialization in + // response to an external request, we assume we are not + // skipping timesync. + const SKIP_TIMESYNC: bool = false; + let ctx = rqctx.context(); + let lockstep_request = body.into_inner(); + let request = convert_rack_initialize_request(lockstep_request); + let params = RackInitializeRequestParams::new(request, SKIP_TIMESYNC); + let id = ctx + .start_rack_initialize(params) + .map_err(|err| HttpError::for_bad_request(None, err.to_string()))?; + Ok(HttpResponseOk(id)) + } + + async fn rack_reset( + rqctx: RequestContext, + ) -> Result, HttpError> { + let ctx = rqctx.context(); + let id = ctx + .rss_access + .start_reset( + &ctx.base_log, + ctx.sprockets.clone(), + ctx.global_zone_bootstrap_ip, + ) + .map_err(|err| HttpError::for_bad_request(None, err.to_string()))?; + Ok(HttpResponseOk(id)) + } +} diff --git a/sled-agent/src/bootstrap/mod.rs b/sled-agent/src/bootstrap/mod.rs index b4b77981f5b..b26c7b78277 100644 --- a/sled-agent/src/bootstrap/mod.rs +++ b/sled-agent/src/bootstrap/mod.rs @@ -9,6 +9,7 @@ pub mod client; pub mod config; pub mod early_networking; mod http_entrypoints; +mod http_entrypoints_lockstep; mod maghemite; pub(crate) mod params; mod pre_server; diff --git a/sled-agent/src/bootstrap/rack_ops.rs b/sled-agent/src/bootstrap/rack_ops.rs index b90acb5faa0..b7ff7198258 100644 --- a/sled-agent/src/bootstrap/rack_ops.rs +++ b/sled-agent/src/bootstrap/rack_ops.rs @@ -43,6 +43,7 @@ pub enum RssAccessError { AlreadyReset, } +#[derive(Clone)] pub(crate) struct RssAccess { // Note: The `Mutex` here is a std mutex, not a tokio mutex, and thus not // subject to async cancellation issues (and also cannot be held across an diff --git a/sled-agent/src/bootstrap/server.rs b/sled-agent/src/bootstrap/server.rs index be4cd24bf0e..2174041cd69 100644 --- a/sled-agent/src/bootstrap/server.rs +++ b/sled-agent/src/bootstrap/server.rs @@ -7,7 +7,9 @@ use super::BootstrapError; use super::RssAccessError; use super::config::BOOTSTRAP_AGENT_HTTP_PORT; +use super::config::BOOTSTRAP_AGENT_LOCKSTEP_PORT; use super::http_entrypoints; +use super::http_entrypoints_lockstep; use super::views::SledAgentResponse; use crate::bootstrap::config::BOOTSTRAP_AGENT_RACK_INIT_PORT; use crate::bootstrap::http_entrypoints::BootstrapServerContext; @@ -165,6 +167,11 @@ pub enum StartError { pub struct Server { inner_task: JoinHandle<()>, bootstrap_http_server: HttpServer, + // The lockstep server shares the same context as bootstrap_http_server. + // We hold this handle to keep the server running; the actual rack + // initialization calls go through bootstrap_http_server.app_private(). + #[allow(dead_code)] + lockstep_http_server: HttpServer, } impl Server { @@ -217,7 +224,12 @@ impl Server { sprockets: config.sprockets.clone(), trust_quorum_handle: long_running_task_handles.trust_quorum.clone(), }; - let bootstrap_http_server = start_dropshot_server(bootstrap_context)?; + let bootstrap_http_server = + start_dropshot_server(bootstrap_context.clone())?; + + // Start the lockstep dropshot server for rack initialization. + let lockstep_http_server = + start_lockstep_dropshot_server(bootstrap_context)?; // Create a channel for proxying sled-initialization requests that land // in the sprockets server to our bootstrap agent `Inner` task. @@ -286,7 +298,7 @@ impl Server { }; let inner_task = tokio::spawn(inner.run()); - Ok(Self { inner_task, bootstrap_http_server }) + Ok(Self { inner_task, bootstrap_http_server, lockstep_http_server }) } pub fn start_rack_initialize( @@ -469,6 +481,38 @@ fn start_dropshot_server( Ok(http_server) } +// Clippy doesn't like `StartError` due to +// https://github.com/oxidecomputer/usdt/issues/133; remove this once that issue +// is addressed. +#[allow(clippy::result_large_err)] +fn start_lockstep_dropshot_server( + context: BootstrapServerContext, +) -> Result, StartError> { + let mut dropshot_config = dropshot::ConfigDropshot::default(); + dropshot_config.default_request_body_max_bytes = 1024 * 1024; + dropshot_config.bind_address = SocketAddr::V6(SocketAddrV6::new( + context.global_zone_bootstrap_ip, + BOOTSTRAP_AGENT_LOCKSTEP_PORT, + 0, + 0, + )); + let dropshot_log = context + .base_log + .new(o!("component" => "dropshot (BootstrapAgentLockstep)")); + let http_server = dropshot::ServerBuilder::new( + http_entrypoints_lockstep::api(), + context, + dropshot_log, + ) + .config(dropshot_config) + .start() + .map_err(|error| { + StartError::InitBootstrapDropshotServer(error.to_string()) + })?; + + Ok(http_server) +} + struct MissingM2Paths(&'static str); impl From for StartError { diff --git a/wicketd/Cargo.toml b/wicketd/Cargo.toml index 9f091568fa9..7215c80b325 100644 --- a/wicketd/Cargo.toml +++ b/wicketd/Cargo.toml @@ -55,6 +55,7 @@ transceiver-controller.workspace = true uuid.workspace = true bootstrap-agent-client.workspace = true +bootstrap-agent-lockstep-client.workspace = true omicron-ddm-admin-client.workspace = true gateway-client.workspace = true installinator-api.workspace = true diff --git a/wicketd/src/context.rs b/wicketd/src/context.rs index 83e6a04f96a..f6375f296ac 100644 --- a/wicketd/src/context.rs +++ b/wicketd/src/context.rs @@ -53,6 +53,15 @@ impl ServerContext { Ok(SocketAddrV6::new(ip, BOOTSTRAP_AGENT_HTTP_PORT, 0, 0)) } + pub(crate) fn bootstrap_agent_lockstep_addr(&self) -> Result { + // Port on which the bootstrap agent lockstep API dropshot server within + // sled-agent is listening. + const BOOTSTRAP_AGENT_LOCKSTEP_PORT: u16 = 8080; + + let ip = self.bootstrap_agent_ip()?; + Ok(SocketAddrV6::new(ip, BOOTSTRAP_AGENT_LOCKSTEP_PORT, 0, 0)) + } + fn bootstrap_agent_ip(&self) -> Result { let mut any_bootstrap_peer = None; for (baseboard, ip) in self.bootstrap_peers.sleds() { diff --git a/wicketd/src/http_entrypoints.rs b/wicketd/src/http_entrypoints.rs index e5f17ac1f1c..7595cbac735 100644 --- a/wicketd/src/http_entrypoints.rs +++ b/wicketd/src/http_entrypoints.rs @@ -261,9 +261,10 @@ impl WicketdApi for WicketdApiImpl { let ctx = rqctx.context(); let log = &rqctx.log; - let sled_agent_addr = ctx.bootstrap_agent_addr().map_err(|err| { - HttpError::for_bad_request(None, format!("{err:#}")) - })?; + let lockstep_addr = + ctx.bootstrap_agent_lockstep_addr().map_err(|err| { + HttpError::for_bad_request(None, format!("{err:#}")) + })?; let request = { let mut config = ctx.rss_config.lock().unwrap(); @@ -275,18 +276,18 @@ impl WicketdApi for WicketdApiImpl { slog::info!( ctx.log, "Sending RSS initialize request to {}", - sled_agent_addr + lockstep_addr ); - let client = bootstrap_agent_client::Client::new( - &format!("http://{}", sled_agent_addr), - ctx.log.new(slog::o!("component" => "bootstrap client")), + let client = bootstrap_agent_lockstep_client::Client::new( + &format!("http://{}", lockstep_addr), + ctx.log.new(slog::o!("component" => "bootstrap lockstep client")), ); let init_id = client .rack_initialize(&request) .await .map_err(|err| { - use bootstrap_agent_client::Error as BaError; + use bootstrap_agent_lockstep_client::Error as BaError; match err { BaError::CommunicationError(err) => { let message = @@ -316,25 +317,22 @@ impl WicketdApi for WicketdApiImpl { ) -> Result, HttpError> { let ctx = rqctx.context(); - let sled_agent_addr = ctx.bootstrap_agent_addr().map_err(|err| { - HttpError::for_bad_request(None, format!("{err:#}")) - })?; + let lockstep_addr = + ctx.bootstrap_agent_lockstep_addr().map_err(|err| { + HttpError::for_bad_request(None, format!("{err:#}")) + })?; - slog::info!( - ctx.log, - "Sending RSS reset request to {}", - sled_agent_addr - ); - let client = bootstrap_agent_client::Client::new( - &format!("http://{}", sled_agent_addr), - ctx.log.new(slog::o!("component" => "bootstrap client")), + slog::info!(ctx.log, "Sending RSS reset request to {}", lockstep_addr); + let client = bootstrap_agent_lockstep_client::Client::new( + &format!("http://{}", lockstep_addr), + ctx.log.new(slog::o!("component" => "bootstrap lockstep client")), ); let reset_id = client .rack_reset() .await .map_err(|err| { - use bootstrap_agent_client::Error as BaError; + use bootstrap_agent_lockstep_client::Error as BaError; match err { BaError::CommunicationError(err) => { let message = diff --git a/wicketd/src/rss_config.rs b/wicketd/src/rss_config.rs index 0223a565dfa..8f6a781d1d8 100644 --- a/wicketd/src/rss_config.rs +++ b/wicketd/src/rss_config.rs @@ -9,13 +9,13 @@ use anyhow::Context; use anyhow::Result; use anyhow::anyhow; use anyhow::bail; -use bootstrap_agent_client::types::BootstrapAddressDiscovery; -use bootstrap_agent_client::types::Certificate; -use bootstrap_agent_client::types::Name; -use bootstrap_agent_client::types::PortConfigV2 as BaPortConfigV2; -use bootstrap_agent_client::types::RackInitializeRequest; -use bootstrap_agent_client::types::RecoverySiloConfig; -use bootstrap_agent_client::types::UserId; +use bootstrap_agent_lockstep_client::types::BootstrapAddressDiscovery; +use bootstrap_agent_lockstep_client::types::Certificate; +use bootstrap_agent_lockstep_client::types::Name; +use bootstrap_agent_lockstep_client::types::PortConfigV2 as BaPortConfigV2; +use bootstrap_agent_lockstep_client::types::RackInitializeRequest; +use bootstrap_agent_lockstep_client::types::RecoverySiloConfig; +use bootstrap_agent_lockstep_client::types::UserId; use display_error_chain::DisplayErrorChain; use omicron_certificates::CertificateError; use omicron_common::address; @@ -252,7 +252,7 @@ impl CurrentRssConfig { // https://github.com/oxidecomputer/omicron/issues/3690 const TRUST_QUORUM_MIN_SIZE: usize = 3; let trust_quorum_peers: Option< - Vec, + Vec, > = if self.bootstrap_sleds.len() >= TRUST_QUORUM_MIN_SIZE { Some( self.bootstrap_sleds @@ -270,16 +270,17 @@ impl CurrentRssConfig { }; // Convert between internal and progenitor types. - let user_password_hash = bootstrap_agent_client::types::NewPasswordHash( - recovery_silo_password_hash.to_string(), - ); + let user_password_hash = + bootstrap_agent_lockstep_client::types::NewPasswordHash( + recovery_silo_password_hash.to_string(), + ); let internal_services_ip_pool_ranges = self .internal_services_ip_pool_ranges .iter() .map(|pool| { - use bootstrap_agent_client::types::IpRange; - use bootstrap_agent_client::types::Ipv4Range; - use bootstrap_agent_client::types::Ipv6Range; + use bootstrap_agent_lockstep_client::types::IpRange; + use bootstrap_agent_lockstep_client::types::Ipv4Range; + use bootstrap_agent_lockstep_client::types::Ipv6Range; match pool { address::IpRange::V4(range) => IpRange::V4(Ipv4Range { first: range.first, @@ -619,8 +620,8 @@ pub(crate) enum BgpAuthKeyError { fn validate_rack_network_config( config: &UserSpecifiedRackNetworkConfig, bgp_auth_keys: &BTreeMap>, -) -> Result { - use bootstrap_agent_client::types::BgpConfig as BaBgpConfig; +) -> Result { + use bootstrap_agent_lockstep_client::types::BgpConfig as BaBgpConfig; // Ensure that there is at least one uplink if !config.has_any_uplinks() { @@ -661,7 +662,7 @@ fn validate_rack_network_config( // TODO Add more client side checks on `rack_network_config` contents? - Ok(bootstrap_agent_client::types::RackNetworkConfigV2 { + Ok(bootstrap_agent_lockstep_client::types::RackNetworkConfigV2 { rack_subnet: RACK_SUBNET.net(), infra_ip_first: config.infra_ip_first, infra_ip_last: config.infra_ip_last, @@ -695,15 +696,15 @@ fn build_port_config( config: &UserSpecifiedPortConfig, bgp_auth_keys: &BTreeMap>, ) -> BaPortConfigV2 { - use bootstrap_agent_client::types::BgpPeerConfig as BaBgpPeerConfig; - use bootstrap_agent_client::types::LldpAdminStatus as BaLldpAdminStatus; - use bootstrap_agent_client::types::LldpPortConfig as BaLldpPortConfig; - use bootstrap_agent_client::types::PortFec as BaPortFec; - use bootstrap_agent_client::types::PortSpeed as BaPortSpeed; - use bootstrap_agent_client::types::RouteConfig as BaRouteConfig; - use bootstrap_agent_client::types::SwitchLocation as BaSwitchLocation; - use bootstrap_agent_client::types::TxEqConfig as BaTxEqConfig; - use bootstrap_agent_client::types::UplinkAddressConfig as BaUplinkAddressConfig; + use bootstrap_agent_lockstep_client::types::BgpPeerConfig as BaBgpPeerConfig; + use bootstrap_agent_lockstep_client::types::LldpAdminStatus as BaLldpAdminStatus; + use bootstrap_agent_lockstep_client::types::LldpPortConfig as BaLldpPortConfig; + use bootstrap_agent_lockstep_client::types::PortFec as BaPortFec; + use bootstrap_agent_lockstep_client::types::PortSpeed as BaPortSpeed; + use bootstrap_agent_lockstep_client::types::RouteConfig as BaRouteConfig; + use bootstrap_agent_lockstep_client::types::SwitchLocation as BaSwitchLocation; + use bootstrap_agent_lockstep_client::types::TxEqConfig as BaTxEqConfig; + use bootstrap_agent_lockstep_client::types::UplinkAddressConfig as BaUplinkAddressConfig; use omicron_common::api::internal::shared::LldpAdminStatus; use omicron_common::api::internal::shared::PortFec; use omicron_common::api::internal::shared::PortSpeed; From 34955d29cf15388ee4dbf336706a888cb7101a68 Mon Sep 17 00:00:00 2001 From: Ryan Goodfellow Date: Tue, 20 Jan 2026 16:52:20 +0000 Subject: [PATCH 10/20] various fixes --- dev-tools/ls-apis/api-manifest.toml | 10 ++++++++++ dev-tools/ls-apis/tests/api_check.out | 1 + dev-tools/ls-apis/tests/api_dependencies.out | 3 +++ 3 files changed, 14 insertions(+) diff --git a/dev-tools/ls-apis/api-manifest.toml b/dev-tools/ls-apis/api-manifest.toml index 204bd11ca24..9f30c138a06 100644 --- a/dev-tools/ls-apis/api-manifest.toml +++ b/dev-tools/ls-apis/api-manifest.toml @@ -176,6 +176,16 @@ server_package_name = "bootstrap-agent-api" versioned_how = "client" versioned_how_reason = "depends on itself (i.e., instances call each other)" +[[apis]] +client_package_name = "bootstrap-agent-lockstep-client" +label = "Bootstrap Agent Lockstep API" +server_package_name = "bootstrap-agent-lockstep-api" +versioned_how = "server" +notes = """ +Lockstep API for rack initialization. Only used by wicketd, which is in the \ +same deployment unit (Host OS) as the sled-agent that exposes this API. +""" + [[apis]] client_package_name = "clickhouse-admin-keeper-client" label = "Clickhouse Cluster Admin for Keepers" diff --git a/dev-tools/ls-apis/tests/api_check.out b/dev-tools/ls-apis/tests/api_check.out index cb06720164f..107676ad50c 100644 --- a/dev-tools/ls-apis/tests/api_check.out +++ b/dev-tools/ls-apis/tests/api_check.out @@ -2,6 +2,7 @@ Server-managed APIs: + Bootstrap Agent Lockstep API (bootstrap-agent-lockstep-client, exposed by omicron-sled-agent) Clickhouse Cluster Admin for Keepers (clickhouse-admin-keeper-client, exposed by omicron-clickhouse-admin) Clickhouse Cluster Admin for Servers (clickhouse-admin-server-client, exposed by omicron-clickhouse-admin) Clickhouse Single-Node Cluster Admin (clickhouse-admin-single-client, exposed by omicron-clickhouse-admin) diff --git a/dev-tools/ls-apis/tests/api_dependencies.out b/dev-tools/ls-apis/tests/api_dependencies.out index d48fe9fcfcf..4c9d26402c3 100644 --- a/dev-tools/ls-apis/tests/api_dependencies.out +++ b/dev-tools/ls-apis/tests/api_dependencies.out @@ -2,6 +2,9 @@ Bootstrap Agent (client: bootstrap-agent-client) consumed by: omicron-sled-agent (omicron/sled-agent) via 1 path consumed by: wicketd (omicron/wicketd) via 2 paths +Bootstrap Agent Lockstep API (client: bootstrap-agent-lockstep-client) + consumed by: wicketd (omicron/wicketd) via 1 path + Clickhouse Cluster Admin for Keepers (client: clickhouse-admin-keeper-client) consumed by: omicron-nexus (omicron/nexus) via 3 paths From 8f1917688b87060a99cb09ff7d91ee2b2933db76 Mon Sep 17 00:00:00 2001 From: Ryan Goodfellow Date: Wed, 21 Jan 2026 06:26:54 +0000 Subject: [PATCH 11/20] remove rack-init APIs from client-side versioned bootstrap agent api --- Cargo.lock | 2 +- clients/bootstrap-agent-client/src/lib.rs | 2 +- .../bootstrap-agent-2.0.0-632b71.json | 208 ++++++++++++++++++ .../bootstrap-agent-latest.json | 2 +- openapi/wicketd.json | 2 +- sled-agent/bootstrap-agent-api/src/lib.rs | 10 +- wicketd-api/Cargo.toml | 2 +- wicketd-api/src/lib.rs | 2 +- wicketd/src/context.rs | 9 - wicketd/src/http_entrypoints.rs | 17 +- 10 files changed, 231 insertions(+), 25 deletions(-) create mode 100644 openapi/bootstrap-agent/bootstrap-agent-2.0.0-632b71.json diff --git a/Cargo.lock b/Cargo.lock index f199f2cdfbe..c9eff48878e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16435,7 +16435,7 @@ dependencies = [ name = "wicketd-api" version = "0.1.0" dependencies = [ - "bootstrap-agent-client", + "bootstrap-agent-lockstep-client", "dropshot", "gateway-client", "omicron-common", diff --git a/clients/bootstrap-agent-client/src/lib.rs b/clients/bootstrap-agent-client/src/lib.rs index 232bd08742e..7b863beec5f 100644 --- a/clients/bootstrap-agent-client/src/lib.rs +++ b/clients/bootstrap-agent-client/src/lib.rs @@ -5,7 +5,7 @@ //! Interface for making API requests to a Bootstrap Agent progenitor::generate_api!( - spec = "../../openapi/bootstrap-agent/bootstrap-agent-1.0.0-127591.json", + spec = "../../openapi/bootstrap-agent/bootstrap-agent-2.0.0-632b71.json", interface = Positional, inner_type = slog::Logger, pre_hook = (|log: &slog::Logger, request: &reqwest::Request| { diff --git a/openapi/bootstrap-agent/bootstrap-agent-2.0.0-632b71.json b/openapi/bootstrap-agent/bootstrap-agent-2.0.0-632b71.json new file mode 100644 index 00000000000..c70391ef6a1 --- /dev/null +++ b/openapi/bootstrap-agent/bootstrap-agent-2.0.0-632b71.json @@ -0,0 +1,208 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Bootstrap Agent API", + "description": "Per-sled API for setup and teardown", + "contact": { + "url": "https://oxide.computer", + "email": "api@oxide.computer" + }, + "version": "2.0.0" + }, + "paths": { + "/baseboard": { + "get": { + "summary": "Return the baseboard identity of this sled.", + "operationId": "baseboard_get", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Baseboard" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/components": { + "get": { + "summary": "Provide a list of components known to the bootstrap agent.", + "description": "This API is intended to allow early boot services (such as Wicket) to query the underlying component versions installed on a sled.", + "operationId": "components_get", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_Component", + "type": "array", + "items": { + "$ref": "#/components/schemas/Component" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/sled-initialize": { + "delete": { + "summary": "Reset this particular sled to an unconfigured state.", + "operationId": "sled_reset", + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + } + }, + "components": { + "schemas": { + "ArtifactVersion": { + "description": "An artifact version.\n\nThis is a freeform identifier with some basic validation. It may be the serialized form of a semver version, or a custom identifier that uses the same character set as a semver, plus `_`.\n\nThe exact pattern accepted is `^[a-zA-Z0-9._+-]{1,63}$`.\n\n# Ord implementation\n\n`ArtifactVersion`s are not intended to be sorted, just compared for equality. `ArtifactVersion` implements `Ord` only for storage within sorted collections.", + "type": "string", + "pattern": "^[a-zA-Z0-9._+-]{1,63}$" + }, + "Baseboard": { + "description": "Describes properties that should uniquely identify a Gimlet.", + "oneOf": [ + { + "type": "object", + "properties": { + "identifier": { + "type": "string" + }, + "model": { + "type": "string" + }, + "revision": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "gimlet" + ] + } + }, + "required": [ + "identifier", + "model", + "revision", + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "unknown" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "identifier": { + "type": "string" + }, + "model": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "pc" + ] + } + }, + "required": [ + "identifier", + "model", + "type" + ] + } + ] + }, + "Component": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "version": { + "$ref": "#/components/schemas/ArtifactVersion" + } + }, + "required": [ + "name", + "version" + ] + }, + "Error": { + "description": "Error information from a response.", + "type": "object", + "properties": { + "error_code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "request_id": { + "type": "string" + } + }, + "required": [ + "message", + "request_id" + ] + } + }, + "responses": { + "Error": { + "description": "Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } +} diff --git a/openapi/bootstrap-agent/bootstrap-agent-latest.json b/openapi/bootstrap-agent/bootstrap-agent-latest.json index 6355e3b64f8..4301a6dd71b 120000 --- a/openapi/bootstrap-agent/bootstrap-agent-latest.json +++ b/openapi/bootstrap-agent/bootstrap-agent-latest.json @@ -1 +1 @@ -bootstrap-agent-1.0.0-127591.json \ No newline at end of file +bootstrap-agent-2.0.0-632b71.json \ No newline at end of file diff --git a/openapi/wicketd.json b/openapi/wicketd.json index c0b41c90d8d..752eb45af65 100644 --- a/openapi/wicketd.json +++ b/openapi/wicketd.json @@ -3879,7 +3879,7 @@ ] }, "RssStep": { - "description": "Steps we go through during initial rack setup. Keep this list in order that they happen.\n\n
JSON schema\n\n```json { \"description\": \"Steps we go through during initial rack setup. Keep this list in order that they happen.\", \"oneOf\": [ { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"requested\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"starting\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"load_existing_plan\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"create_sled_plan\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"init_trust_quorum\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"network_config_update\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"sled_init\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"init_dns\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"configure_dns\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"init_ntp\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"wait_for_time_sync\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"wait_for_database\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"cluster_init\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"zones_init\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"nexus_handoff\" ] } } } ] } ```
", + "description": "Steps we go through during initial rack setup.\n\n
JSON schema\n\n```json { \"description\": \"Steps we go through during initial rack setup.\", \"oneOf\": [ { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"requested\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"starting\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"load_existing_plan\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"create_sled_plan\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"init_trust_quorum\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"network_config_update\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"sled_init\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"init_dns\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"configure_dns\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"init_ntp\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"wait_for_time_sync\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"wait_for_database\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"cluster_init\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"zones_init\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"nexus_handoff\" ] } } } ] } ```
", "oneOf": [ { "type": "object", diff --git a/sled-agent/bootstrap-agent-api/src/lib.rs b/sled-agent/bootstrap-agent-api/src/lib.rs index 12f9dae7c56..de966027041 100644 --- a/sled-agent/bootstrap-agent-api/src/lib.rs +++ b/sled-agent/bootstrap-agent-api/src/lib.rs @@ -23,7 +23,10 @@ use tufaceous_artifact::ArtifactVersion; api_versions!([ // Do not create new versions of this client-side versioned API. - // https://github.com/oxidecomputer/omicron/issues/9290 + // See https://github.com/oxidecomputer/omicron/issues/9290 + + // Remove rack initialization endpoints moved to bootstrap-agent-lockstep-api. + (2, REMOVE_RACK_INIT_ENDPOINTS), (1, INITIAL), ]); @@ -53,11 +56,12 @@ pub trait BootstrapAgentApi { ) -> Result>, HttpError>; /// Get the current status of rack initialization or reset. - /// + /// /// Deprecated. Do not use. Moved to bootstrap agent lockstep API. #[endpoint { method = GET, path = "/rack-initialize", + versions = VERSION_INITIAL..VERSION_REMOVE_RACK_INIT_ENDPOINTS, }] async fn rack_initialization_status( rqctx: RequestContext, @@ -69,6 +73,7 @@ pub trait BootstrapAgentApi { #[endpoint { method = POST, path = "/rack-initialize", + versions = VERSION_INITIAL..VERSION_REMOVE_RACK_INIT_ENDPOINTS, }] async fn rack_initialize( rqctx: RequestContext, @@ -81,6 +86,7 @@ pub trait BootstrapAgentApi { #[endpoint { method = DELETE, path = "/rack-initialize", + versions = VERSION_INITIAL..VERSION_REMOVE_RACK_INIT_ENDPOINTS, }] async fn rack_reset( rqctx: RequestContext, diff --git a/wicketd-api/Cargo.toml b/wicketd-api/Cargo.toml index c247300ccea..474b19b562d 100644 --- a/wicketd-api/Cargo.toml +++ b/wicketd-api/Cargo.toml @@ -7,7 +7,7 @@ edition.workspace = true workspace = true [dependencies] -bootstrap-agent-client.workspace = true +bootstrap-agent-lockstep-client.workspace = true dropshot.workspace = true gateway-client.workspace = true omicron-common.workspace = true diff --git a/wicketd-api/src/lib.rs b/wicketd-api/src/lib.rs index 467922aad86..addbf09aa31 100644 --- a/wicketd-api/src/lib.rs +++ b/wicketd-api/src/lib.rs @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use bootstrap_agent_client::types::RackOperationStatus; +use bootstrap_agent_lockstep_client::types::RackOperationStatus; use dropshot::HttpError; use dropshot::HttpResponseOk; use dropshot::HttpResponseUpdatedNoContent; diff --git a/wicketd/src/context.rs b/wicketd/src/context.rs index f6375f296ac..66ff7a6cb98 100644 --- a/wicketd/src/context.rs +++ b/wicketd/src/context.rs @@ -44,15 +44,6 @@ pub struct ServerContext { } impl ServerContext { - pub(crate) fn bootstrap_agent_addr(&self) -> Result { - // Port on which the bootstrap agent dropshot server within sled-agent - // is listening. - const BOOTSTRAP_AGENT_HTTP_PORT: u16 = 80; - - let ip = self.bootstrap_agent_ip()?; - Ok(SocketAddrV6::new(ip, BOOTSTRAP_AGENT_HTTP_PORT, 0, 0)) - } - pub(crate) fn bootstrap_agent_lockstep_addr(&self) -> Result { // Port on which the bootstrap agent lockstep API dropshot server within // sled-agent is listening. diff --git a/wicketd/src/http_entrypoints.rs b/wicketd/src/http_entrypoints.rs index 7595cbac735..b54b70c3aa7 100644 --- a/wicketd/src/http_entrypoints.rs +++ b/wicketd/src/http_entrypoints.rs @@ -13,7 +13,7 @@ use crate::mgs::MgsHandle; use crate::mgs::ShutdownInProgress; use crate::transceivers::GetTransceiversResponse; use crate::transceivers::Handle as TransceiverHandle; -use bootstrap_agent_client::types::RackOperationStatus; +use bootstrap_agent_lockstep_client::types::RackOperationStatus; use dropshot::ApiDescription; use dropshot::HttpError; use dropshot::HttpResponseOk; @@ -217,20 +217,21 @@ impl WicketdApi for WicketdApiImpl { ) -> Result, HttpError> { let ctx = rqctx.context(); - let sled_agent_addr = ctx.bootstrap_agent_addr().map_err(|err| { - HttpError::for_bad_request(None, format!("{err:#}")) - })?; + let lockstep_addr = + ctx.bootstrap_agent_lockstep_addr().map_err(|err| { + HttpError::for_bad_request(None, format!("{err:#}")) + })?; - let client = bootstrap_agent_client::Client::new( - &format!("http://{}", sled_agent_addr), - ctx.log.new(slog::o!("component" => "bootstrap client")), + let client = bootstrap_agent_lockstep_client::Client::new( + &format!("http://{}", lockstep_addr), + ctx.log.new(slog::o!("component" => "bootstrap lockstep client")), ); let op_status = client .rack_initialization_status() .await .map_err(|err| { - use bootstrap_agent_client::Error as BaError; + use bootstrap_agent_lockstep_client::Error as BaError; match err { BaError::CommunicationError(err) => { let message = From 745d1b78fee13e04211b801f47a0495ad0b8968b Mon Sep 17 00:00:00 2001 From: Ryan Goodfellow Date: Wed, 21 Jan 2026 07:34:51 +0000 Subject: [PATCH 12/20] bring rack network config into current versioning scheme --- .../src/api/internal/shared/external_ip/v1.rs | 1 + common/src/api/internal/shared/mod.rs | 538 +----------------- .../src/api/internal/shared/rack_init/mod.rs | 29 + .../src/api/internal/shared/rack_init/v1.rs | 520 +++++++++++++++++ .../tests/integration_tests/early_network.rs | 4 +- .../versions/src/initial/early_networking.rs | 46 +- 6 files changed, 599 insertions(+), 539 deletions(-) create mode 100644 common/src/api/internal/shared/rack_init/mod.rs create mode 100644 common/src/api/internal/shared/rack_init/v1.rs diff --git a/common/src/api/internal/shared/external_ip/v1.rs b/common/src/api/internal/shared/external_ip/v1.rs index 7a3300e7810..a9f55bc5756 100644 --- a/common/src/api/internal/shared/external_ip/v1.rs +++ b/common/src/api/internal/shared/external_ip/v1.rs @@ -1,3 +1,4 @@ +// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. diff --git a/common/src/api/internal/shared/mod.rs b/common/src/api/internal/shared/mod.rs index 25c5edd48f7..faff5e2ac7b 100644 --- a/common/src/api/internal/shared/mod.rs +++ b/common/src/api/internal/shared/mod.rs @@ -6,20 +6,20 @@ use super::nexus::HostIdentifier; use crate::{ - api::external::{self, BfdMode, ImportExportPolicy, Name, Vni}, + api::external::{self, Vni}, disk::DatasetName, zpool_name::ZpoolName, }; use daft::Diffable; use omicron_uuid_kinds::DatasetUuid; use omicron_uuid_kinds::ExternalZpoolUuid; -use oxnet::{IpNet, Ipv4Net, Ipv6Net}; +use oxnet::IpNet; use schemars::JsonSchema; use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use std::{ collections::{HashMap, HashSet}, fmt, - net::{IpAddr, Ipv4Addr, Ipv6Addr}, + net::{IpAddr, Ipv6Addr}, str::FromStr, }; use strum::EnumCount; @@ -27,6 +27,7 @@ use uuid::Uuid; pub mod external_ip; pub mod network_interface; +pub mod rack_init; // Re-export latest version of all NIC-related types. pub use network_interface::NetworkInterfaceKind; @@ -43,517 +44,26 @@ pub use external_ip::SourceNatConfigGeneric; pub use external_ip::SourceNatConfigV4; pub use external_ip::SourceNatConfigV6; -// We alias [`PortConfig`] to the current version of the protocol, so -// that we can convert between versions as necessary. -pub type PortConfig = PortConfigV2; - -// We alias [`RackNetworkConfig`] to the current version of the protocol, so -// that we can convert between versions as necessary. -pub type RackNetworkConfig = RackNetworkConfigV2; - -/// Initial network configuration -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] -pub struct RackNetworkConfigV2 { - pub rack_subnet: Ipv6Net, - // TODO: #3591 Consider making infra-ip ranges implicit for uplinks - /// First ip address to be used for configuring network infrastructure - pub infra_ip_first: Ipv4Addr, - /// Last ip address to be used for configuring network infrastructure - pub infra_ip_last: Ipv4Addr, - /// Uplinks for connecting the rack to external networks - pub ports: Vec, - /// BGP configurations for connecting the rack to external networks - pub bgp: Vec, - /// BFD configuration for connecting the rack to external networks - #[serde(default)] - pub bfd: Vec, -} - -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] -pub struct BgpConfig { - /// The autonomous system number for the BGP configuration. - pub asn: u32, - /// The set of prefixes for the BGP router to originate. - pub originate: Vec, - - /// Shaper to apply to outgoing messages. - #[serde(default)] - pub shaper: Option, - - /// Checker to apply to incoming messages. - #[serde(default)] - pub checker: Option, -} - -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] -pub struct BgpPeerConfig { - /// The autonomous system number of the router the peer belongs to. - pub asn: u32, - /// Switch port the peer is reachable on. - pub port: String, - /// Address of the peer. Use `Ipv4Addr::UNSPECIFIED` to indicate an - /// unnumbered BGP session established over the interface specified by - /// `port`. - pub addr: Ipv4Addr, - /// How long to keep a session alive without a keepalive in seconds. - /// Defaults to 6. - pub hold_time: Option, - /// How long to keep a peer in idle after a state machine reset in seconds. - pub idle_hold_time: Option, - /// How long to delay sending open messages to a peer. In seconds. - pub delay_open: Option, - /// The interval in seconds between peer connection retry attempts. - pub connect_retry: Option, - /// The interval to send keepalive messages at. - pub keepalive: Option, - /// Require that a peer has a specified ASN. - #[serde(default)] - pub remote_asn: Option, - /// Require messages from a peer have a minimum IP time to live field. - #[serde(default)] - pub min_ttl: Option, - /// Use the given key for TCP-MD5 authentication with the peer. - #[serde(default)] - pub md5_auth_key: Option, - /// Apply the provided multi-exit discriminator (MED) updates sent to the peer. - #[serde(default)] - pub multi_exit_discriminator: Option, - /// Include the provided communities in updates sent to the peer. - #[serde(default)] - pub communities: Vec, - /// Apply a local preference to routes received from this peer. - #[serde(default)] - pub local_pref: Option, - /// Enforce that the first AS in paths received from this peer is the peer's AS. - #[serde(default)] - pub enforce_first_as: bool, - /// Define import policy for a peer. - #[serde(default)] - pub allowed_import: ImportExportPolicy, - /// Define export policy for a peer. - #[serde(default)] - pub allowed_export: ImportExportPolicy, - /// Associate a VLAN ID with a BGP peer session. - #[serde(default)] - pub vlan_id: Option, -} - -impl BgpPeerConfig { - /// The default hold time for a BGP peer in seconds. - pub const DEFAULT_HOLD_TIME: u64 = 6; - - /// The default idle hold time for a BGP peer in seconds. - pub const DEFAULT_IDLE_HOLD_TIME: u64 = 3; - - /// The default delay open time for a BGP peer in seconds. - pub const DEFAULT_DELAY_OPEN: u64 = 0; - - /// The default connect retry time for a BGP peer in seconds. - pub const DEFAULT_CONNECT_RETRY: u64 = 3; - - /// The default keepalive time for a BGP peer in seconds. - pub const DEFAULT_KEEPALIVE: u64 = 2; - - pub fn hold_time(&self) -> u64 { - self.hold_time.unwrap_or(Self::DEFAULT_HOLD_TIME) - } - - pub fn idle_hold_time(&self) -> u64 { - self.idle_hold_time.unwrap_or(Self::DEFAULT_IDLE_HOLD_TIME) - } - - pub fn delay_open(&self) -> u64 { - self.delay_open.unwrap_or(Self::DEFAULT_DELAY_OPEN) - } - - pub fn connect_retry(&self) -> u64 { - self.connect_retry.unwrap_or(Self::DEFAULT_CONNECT_RETRY) - } - - pub fn keepalive(&self) -> u64 { - self.keepalive.unwrap_or(Self::DEFAULT_KEEPALIVE) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] -pub struct BfdPeerConfig { - pub local: Option, - pub remote: IpAddr, - pub detection_threshold: u8, - pub required_rx: u64, - pub mode: BfdMode, - pub switch: SwitchLocation, -} - -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] -pub struct RouteConfig { - /// The destination of the route. - pub destination: IpNet, - /// The nexthop/gateway address. - pub nexthop: IpAddr, - /// The VLAN id associated with this route. - #[serde(default)] - pub vlan_id: Option, - /// The RIB priority (i.e. Admin Distance) associated with this route. - #[serde(default)] - pub rib_priority: Option, -} - -#[derive( - Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Hash, -)] -pub struct UplinkAddressConfig { - pub address: IpNet, - /// The VLAN id (if any) associated with this address. - #[serde(default)] - pub vlan_id: Option, -} - -impl UplinkAddressConfig { - pub fn addr(&self) -> IpAddr { - self.address.addr() - } -} - -impl std::fmt::Display for UplinkAddressConfig { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.vlan_id { - None => write!(f, "{}", self.address), - Some(v) => write!(f, "{};{}", self.address, v), - } - } -} - -#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] -pub struct UplinkAddressConfigError(String); - -impl std::fmt::Display for UplinkAddressConfigError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "parse switch location error: {}", self.0) - } -} - -/// Convert a string into an UplinkAddressConfig. -/// 192.168.1.1/24 => UplinkAddressConfig { 192.168.1.1/24, None } -/// 192.168.1.1/24;200 => UplinkAddressConfig { 192.168.1.1/24, Some(200) } -impl FromStr for UplinkAddressConfig { - type Err = UplinkAddressConfigError; - - fn from_str(s: &str) -> Result { - let fields: Vec<&str> = s.split(';').collect(); - let (address, vlan_id) = match fields.len() { - 1 => Ok((fields[0], None)), - 2 => Ok((fields[0], Some(fields[1]))), - _ => Err(UplinkAddressConfigError(format!( - "not a valid uplink address: {s}" - ))), - }?; - let address = address.parse().map_err(|_| { - UplinkAddressConfigError(format!( - "not a valid ip address: {address}" - )) - })?; - let vlan_id = match vlan_id { - None => Ok(None), - Some(v) => match v.parse() { - Err(_) => Err(format!("invalid vlan id: {v}")), - Ok(vlan_id) if vlan_id > 1 && vlan_id < 4096 => { - Ok(Some(vlan_id)) - } - Ok(vlan_id) => Err(format!("vlan id out of range: {vlan_id}")), - }, - } - .map_err(|e| UplinkAddressConfigError(e))?; - Ok(UplinkAddressConfig { address, vlan_id }) - } -} - -#[derive( - Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, -)] -#[serde(rename_all = "snake_case")] -/// To what extent should this port participate in LLDP -pub enum LldpAdminStatus { - #[default] - Enabled, - Disabled, - RxOnly, - TxOnly, -} - -impl fmt::Display for LldpAdminStatus { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - LldpAdminStatus::Enabled => write!(f, "enabled"), - LldpAdminStatus::Disabled => write!(f, "disabled"), - LldpAdminStatus::RxOnly => write!(f, "rx_only"), - LldpAdminStatus::TxOnly => write!(f, "tx_only"), - } - } -} - -#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] -pub struct ParseLldpAdminStatusError(String); - -impl std::fmt::Display for ParseLldpAdminStatusError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "LLDP admin status error: {}", self.0) - } -} - -impl FromStr for LldpAdminStatus { - type Err = ParseLldpAdminStatusError; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "enabled" => Ok(Self::Enabled), - "disabled" => Ok(Self::Disabled), - "rxonly" | "rx_only" => Ok(Self::RxOnly), - "txonly" | "tx_only" => Ok(Self::TxOnly), - _ => Err(ParseLldpAdminStatusError(format!( - "not a valid admin status: {s}" - ))), - } - } -} - -/// Per-port LLDP configuration settings. Only the "status" setting is -/// mandatory. All other fields have natural defaults or may be inherited from -/// the switch. -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] -pub struct LldpPortConfig { - /// To what extent should this port participate in LLDP - pub status: LldpAdminStatus, - /// Chassis ID to advertise. If this is set, it will be advertised as a - /// LocallyAssigned ID type. If this is not set, it will be - /// inherited from the switch-level settings. - pub chassis_id: Option, - /// Port ID to advertise. If this is set, it will be advertised as a - /// LocallyAssigned ID type. If this is not set, it will be set to - /// the port name. e.g., qsfp0/0. - pub port_id: Option, - /// Port description to advertise. If this is not set, no - /// description will be advertised. - pub port_description: Option, - /// System name to advertise. If this is not set, it will be - /// inherited from the switch-level settings. - pub system_name: Option, - /// System description to advertise. If this is not set, it will be - /// inherited from the switch-level settings. - pub system_description: Option, - /// Management IP addresses to advertise. If this is not set, it will be - /// inherited from the switch-level settings. - pub management_addrs: Option>, -} - -/// Per-port tx-eq overrides. This can be used to fine-tune the transceiver -/// equalization settings to improve signal integrity. -#[derive( - Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema, -)] -pub struct TxEqConfig { - /// Pre-cursor tap1 - pub pre1: Option, - /// Pre-cursor tap2 - pub pre2: Option, - /// Main tap - pub main: Option, - /// Post-cursor tap2 - pub post2: Option, - /// Post-cursor tap1 - pub post1: Option, -} - -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] -pub struct PortConfigV2 { - /// The set of routes associated with this port. - pub routes: Vec, - /// This port's addresses and optional vlan IDs - pub addresses: Vec, - /// Switch the port belongs to. - pub switch: SwitchLocation, - /// Nmae of the port this config applies to. - pub port: String, - /// Port speed. - pub uplink_port_speed: PortSpeed, - /// Port forward error correction type. - pub uplink_port_fec: Option, - /// BGP peers on this port - pub bgp_peers: Vec, - /// Whether or not to set autonegotiation - #[serde(default)] - pub autoneg: bool, - /// LLDP configuration for this port - pub lldp: Option, - /// TX-EQ configuration for this port - pub tx_eq: Option, -} - -/// A set of switch uplinks. -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -pub struct SwitchPorts { - pub uplinks: Vec, -} - -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] -pub struct HostPortConfig { - /// Switchport to use for external connectivity - pub port: String, - - /// IP Address and prefix (e.g., `192.168.0.1/16`) to apply to switchport - /// (must be in infra_ip pool). May also include an optional VLAN ID. - pub addrs: Vec, - - pub lldp: Option, - pub tx_eq: Option, -} - -impl From for HostPortConfig { - fn from(x: PortConfigV2) -> Self { - Self { - port: x.port, - addrs: x.addresses, - lldp: x.lldp.clone(), - tx_eq: x.tx_eq, - } - } -} - -/// Identifies switch physical location -#[derive( - Clone, - Copy, - Debug, - Deserialize, - Serialize, - PartialEq, - JsonSchema, - Hash, - Eq, - PartialOrd, - Ord, -)] -#[serde(rename_all = "snake_case")] -pub enum SwitchLocation { - /// Switch in upper slot - Switch0, - /// Switch in lower slot - Switch1, -} - -impl SwitchLocation { - /// Return the location of the other switch, not ourself. - pub const fn other(&self) -> Self { - match self { - SwitchLocation::Switch0 => SwitchLocation::Switch1, - SwitchLocation::Switch1 => SwitchLocation::Switch0, - } - } -} - -impl fmt::Display for SwitchLocation { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - SwitchLocation::Switch0 => write!(f, "switch0"), - SwitchLocation::Switch1 => write!(f, "switch1"), - } - } -} - -#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] -pub struct ParseSwitchLocationError(String); - -impl std::fmt::Display for ParseSwitchLocationError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "parse switch location error: {}", self.0) - } -} - -impl FromStr for SwitchLocation { - type Err = ParseSwitchLocationError; - - fn from_str(s: &str) -> Result { - match s { - "switch0" => Ok(Self::Switch0), - "switch1" => Ok(Self::Switch1), - _ => Err(ParseSwitchLocationError(format!( - "not a valid location: {s}" - ))), - } - } -} - -#[derive(Debug, Clone, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum ExternalPortDiscovery { - // Automatically discover ports via Dendrite - Auto(HashMap), - // Static configuration pairing switches with a collection of ports - Static(HashMap>), -} - -/// Switchport Speed options -#[derive( - Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Hash, -)] -#[serde(rename_all = "snake_case")] -pub enum PortSpeed { - #[serde(alias = "0G")] - Speed0G, - #[serde(alias = "1G")] - Speed1G, - #[serde(alias = "10G")] - Speed10G, - #[serde(alias = "25G")] - Speed25G, - #[serde(alias = "40G")] - Speed40G, - #[serde(alias = "50G")] - Speed50G, - #[serde(alias = "100G")] - Speed100G, - #[serde(alias = "200G")] - Speed200G, - #[serde(alias = "400G")] - Speed400G, -} - -impl fmt::Display for PortSpeed { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - PortSpeed::Speed0G => write!(f, "0G"), - PortSpeed::Speed1G => write!(f, "1G"), - PortSpeed::Speed10G => write!(f, "10G"), - PortSpeed::Speed25G => write!(f, "25G"), - PortSpeed::Speed40G => write!(f, "40G"), - PortSpeed::Speed50G => write!(f, "50G"), - PortSpeed::Speed100G => write!(f, "100G"), - PortSpeed::Speed200G => write!(f, "200G"), - PortSpeed::Speed400G => write!(f, "400G"), - } - } -} - -/// Switchport FEC options -#[derive( - Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Hash, -)] -#[serde(rename_all = "snake_case")] -pub enum PortFec { - Firecode, - None, - Rs, -} - -impl fmt::Display for PortFec { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - PortFec::Firecode => write!(f, "Firecode R-FEC"), - PortFec::None => write!(f, "None"), - PortFec::Rs => write!(f, "RS-FEC"), - } - } -} +// Re-export latest version of rack_init types. +pub use rack_init::BfdPeerConfig; +pub use rack_init::BgpConfig; +pub use rack_init::BgpPeerConfig; +pub use rack_init::ExternalPortDiscovery; +pub use rack_init::HostPortConfig; +pub use rack_init::LldpAdminStatus; +pub use rack_init::LldpPortConfig; +pub use rack_init::ParseLldpAdminStatusError; +pub use rack_init::ParseSwitchLocationError; +pub use rack_init::PortConfig; +pub use rack_init::PortFec; +pub use rack_init::PortSpeed; +pub use rack_init::RackNetworkConfig; +pub use rack_init::RouteConfig; +pub use rack_init::SwitchLocation; +pub use rack_init::SwitchPorts; +pub use rack_init::TxEqConfig; +pub use rack_init::UplinkAddressConfig; +pub use rack_init::UplinkAddressConfigError; /// Description of source IPs allowed to reach rack services. #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] diff --git a/common/src/api/internal/shared/rack_init/mod.rs b/common/src/api/internal/shared/rack_init/mod.rs new file mode 100644 index 00000000000..2c3196348e4 --- /dev/null +++ b/common/src/api/internal/shared/rack_init/mod.rs @@ -0,0 +1,29 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Rack init types. + +pub mod v1; + +// Re-export latest version types for convenience. +// Note: New versions of these types will be added to the top-level module. +pub use v1::BfdPeerConfig; +pub use v1::BgpConfig; +pub use v1::BgpPeerConfig; +pub use v1::ExternalPortDiscovery; +pub use v1::HostPortConfig; +pub use v1::LldpAdminStatus; +pub use v1::LldpPortConfig; +pub use v1::ParseLldpAdminStatusError; +pub use v1::ParseSwitchLocationError; +pub use v1::PortConfig; +pub use v1::PortFec; +pub use v1::PortSpeed; +pub use v1::RackNetworkConfig; +pub use v1::RouteConfig; +pub use v1::SwitchLocation; +pub use v1::SwitchPorts; +pub use v1::TxEqConfig; +pub use v1::UplinkAddressConfig; +pub use v1::UplinkAddressConfigError; diff --git a/common/src/api/internal/shared/rack_init/v1.rs b/common/src/api/internal/shared/rack_init/v1.rs new file mode 100644 index 00000000000..d8e3ff8bb63 --- /dev/null +++ b/common/src/api/internal/shared/rack_init/v1.rs @@ -0,0 +1,520 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Version 1 of rack init types. + +use crate::api::external::{BfdMode, ImportExportPolicy, Name}; +use oxnet::{IpNet, Ipv4Net, Ipv6Net}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + fmt, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + str::FromStr, +}; + +/// Initial network configuration +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] +pub struct RackNetworkConfig { + pub rack_subnet: Ipv6Net, + // TODO: #3591 Consider making infra-ip ranges implicit for uplinks + /// First ip address to be used for configuring network infrastructure + pub infra_ip_first: Ipv4Addr, + /// Last ip address to be used for configuring network infrastructure + pub infra_ip_last: Ipv4Addr, + /// Uplinks for connecting the rack to external networks + pub ports: Vec, + /// BGP configurations for connecting the rack to external networks + pub bgp: Vec, + /// BFD configuration for connecting the rack to external networks + #[serde(default)] + pub bfd: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] +pub struct BgpConfig { + /// The autonomous system number for the BGP configuration. + pub asn: u32, + /// The set of prefixes for the BGP router to originate. + pub originate: Vec, + + /// Shaper to apply to outgoing messages. + #[serde(default)] + pub shaper: Option, + + /// Checker to apply to incoming messages. + #[serde(default)] + pub checker: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] +pub struct BgpPeerConfig { + /// The autonomous system number of the router the peer belongs to. + pub asn: u32, + /// Switch port the peer is reachable on. + pub port: String, + /// Address of the peer. Use `Ipv4Addr::UNSPECIFIED` to indicate an + /// unnumbered BGP session established over the interface specified by + /// `port`. + pub addr: Ipv4Addr, + /// How long to keep a session alive without a keepalive in seconds. + /// Defaults to 6. + pub hold_time: Option, + /// How long to keep a peer in idle after a state machine reset in seconds. + pub idle_hold_time: Option, + /// How long to delay sending open messages to a peer. In seconds. + pub delay_open: Option, + /// The interval in seconds between peer connection retry attempts. + pub connect_retry: Option, + /// The interval to send keepalive messages at. + pub keepalive: Option, + /// Require that a peer has a specified ASN. + #[serde(default)] + pub remote_asn: Option, + /// Require messages from a peer have a minimum IP time to live field. + #[serde(default)] + pub min_ttl: Option, + /// Use the given key for TCP-MD5 authentication with the peer. + #[serde(default)] + pub md5_auth_key: Option, + /// Apply the provided multi-exit discriminator (MED) updates sent to the peer. + #[serde(default)] + pub multi_exit_discriminator: Option, + /// Include the provided communities in updates sent to the peer. + #[serde(default)] + pub communities: Vec, + /// Apply a local preference to routes received from this peer. + #[serde(default)] + pub local_pref: Option, + /// Enforce that the first AS in paths received from this peer is the peer's AS. + #[serde(default)] + pub enforce_first_as: bool, + /// Define import policy for a peer. + #[serde(default)] + pub allowed_import: ImportExportPolicy, + /// Define export policy for a peer. + #[serde(default)] + pub allowed_export: ImportExportPolicy, + /// Associate a VLAN ID with a BGP peer session. + #[serde(default)] + pub vlan_id: Option, +} + +impl BgpPeerConfig { + /// The default hold time for a BGP peer in seconds. + pub const DEFAULT_HOLD_TIME: u64 = 6; + + /// The default idle hold time for a BGP peer in seconds. + pub const DEFAULT_IDLE_HOLD_TIME: u64 = 3; + + /// The default delay open time for a BGP peer in seconds. + pub const DEFAULT_DELAY_OPEN: u64 = 0; + + /// The default connect retry time for a BGP peer in seconds. + pub const DEFAULT_CONNECT_RETRY: u64 = 3; + + /// The default keepalive time for a BGP peer in seconds. + pub const DEFAULT_KEEPALIVE: u64 = 2; + + pub fn hold_time(&self) -> u64 { + self.hold_time.unwrap_or(Self::DEFAULT_HOLD_TIME) + } + + pub fn idle_hold_time(&self) -> u64 { + self.idle_hold_time.unwrap_or(Self::DEFAULT_IDLE_HOLD_TIME) + } + + pub fn delay_open(&self) -> u64 { + self.delay_open.unwrap_or(Self::DEFAULT_DELAY_OPEN) + } + + pub fn connect_retry(&self) -> u64 { + self.connect_retry.unwrap_or(Self::DEFAULT_CONNECT_RETRY) + } + + pub fn keepalive(&self) -> u64 { + self.keepalive.unwrap_or(Self::DEFAULT_KEEPALIVE) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] +pub struct BfdPeerConfig { + pub local: Option, + pub remote: IpAddr, + pub detection_threshold: u8, + pub required_rx: u64, + pub mode: BfdMode, + pub switch: SwitchLocation, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] +pub struct RouteConfig { + /// The destination of the route. + pub destination: IpNet, + /// The nexthop/gateway address. + pub nexthop: IpAddr, + /// The VLAN id associated with this route. + #[serde(default)] + pub vlan_id: Option, + /// The RIB priority (i.e. Admin Distance) associated with this route. + #[serde(default)] + pub rib_priority: Option, +} + +#[derive( + Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Hash, +)] +pub struct UplinkAddressConfig { + pub address: IpNet, + /// The VLAN id (if any) associated with this address. + #[serde(default)] + pub vlan_id: Option, +} + +impl UplinkAddressConfig { + pub fn addr(&self) -> IpAddr { + self.address.addr() + } +} + +impl std::fmt::Display for UplinkAddressConfig { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.vlan_id { + None => write!(f, "{}", self.address), + Some(v) => write!(f, "{};{}", self.address, v), + } + } +} + +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct UplinkAddressConfigError(String); + +impl std::fmt::Display for UplinkAddressConfigError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "parse switch location error: {}", self.0) + } +} + +/// Convert a string into an UplinkAddressConfig. +/// 192.168.1.1/24 => UplinkAddressConfig { 192.168.1.1/24, None } +/// 192.168.1.1/24;200 => UplinkAddressConfig { 192.168.1.1/24, Some(200) } +impl FromStr for UplinkAddressConfig { + type Err = UplinkAddressConfigError; + + fn from_str(s: &str) -> Result { + let fields: Vec<&str> = s.split(';').collect(); + let (address, vlan_id) = match fields.len() { + 1 => Ok((fields[0], None)), + 2 => Ok((fields[0], Some(fields[1]))), + _ => Err(UplinkAddressConfigError(format!( + "not a valid uplink address: {s}" + ))), + }?; + let address = address.parse().map_err(|_| { + UplinkAddressConfigError(format!( + "not a valid ip address: {address}" + )) + })?; + let vlan_id = match vlan_id { + None => Ok(None), + Some(v) => match v.parse() { + Err(_) => Err(format!("invalid vlan id: {v}")), + Ok(vlan_id) if vlan_id > 1 && vlan_id < 4096 => { + Ok(Some(vlan_id)) + } + Ok(vlan_id) => Err(format!("vlan id out of range: {vlan_id}")), + }, + } + .map_err(|e| UplinkAddressConfigError(e))?; + Ok(UplinkAddressConfig { address, vlan_id }) + } +} + +#[derive( + Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, +)] +#[serde(rename_all = "snake_case")] +/// To what extent should this port participate in LLDP +pub enum LldpAdminStatus { + #[default] + Enabled, + Disabled, + RxOnly, + TxOnly, +} + +impl fmt::Display for LldpAdminStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LldpAdminStatus::Enabled => write!(f, "enabled"), + LldpAdminStatus::Disabled => write!(f, "disabled"), + LldpAdminStatus::RxOnly => write!(f, "rx_only"), + LldpAdminStatus::TxOnly => write!(f, "tx_only"), + } + } +} + +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct ParseLldpAdminStatusError(String); + +impl std::fmt::Display for ParseLldpAdminStatusError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "LLDP admin status error: {}", self.0) + } +} + +impl FromStr for LldpAdminStatus { + type Err = ParseLldpAdminStatusError; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "enabled" => Ok(Self::Enabled), + "disabled" => Ok(Self::Disabled), + "rxonly" | "rx_only" => Ok(Self::RxOnly), + "txonly" | "tx_only" => Ok(Self::TxOnly), + _ => Err(ParseLldpAdminStatusError(format!( + "not a valid admin status: {s}" + ))), + } + } +} + +/// Per-port LLDP configuration settings. Only the "status" setting is +/// mandatory. All other fields have natural defaults or may be inherited from +/// the switch. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] +pub struct LldpPortConfig { + /// To what extent should this port participate in LLDP + pub status: LldpAdminStatus, + /// Chassis ID to advertise. If this is set, it will be advertised as a + /// LocallyAssigned ID type. If this is not set, it will be + /// inherited from the switch-level settings. + pub chassis_id: Option, + /// Port ID to advertise. If this is set, it will be advertised as a + /// LocallyAssigned ID type. If this is not set, it will be set to + /// the port name. e.g., qsfp0/0. + pub port_id: Option, + /// Port description to advertise. If this is not set, no + /// description will be advertised. + pub port_description: Option, + /// System name to advertise. If this is not set, it will be + /// inherited from the switch-level settings. + pub system_name: Option, + /// System description to advertise. If this is not set, it will be + /// inherited from the switch-level settings. + pub system_description: Option, + /// Management IP addresses to advertise. If this is not set, it will be + /// inherited from the switch-level settings. + pub management_addrs: Option>, +} + +/// Per-port tx-eq overrides. This can be used to fine-tune the transceiver +/// equalization settings to improve signal integrity. +#[derive( + Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema, +)] +pub struct TxEqConfig { + /// Pre-cursor tap1 + pub pre1: Option, + /// Pre-cursor tap2 + pub pre2: Option, + /// Main tap + pub main: Option, + /// Post-cursor tap2 + pub post2: Option, + /// Post-cursor tap1 + pub post1: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] +pub struct PortConfig { + /// The set of routes associated with this port. + pub routes: Vec, + /// This port's addresses and optional vlan IDs + pub addresses: Vec, + /// Switch the port belongs to. + pub switch: SwitchLocation, + /// Nmae of the port this config applies to. + pub port: String, + /// Port speed. + pub uplink_port_speed: PortSpeed, + /// Port forward error correction type. + pub uplink_port_fec: Option, + /// BGP peers on this port + pub bgp_peers: Vec, + /// Whether or not to set autonegotiation + #[serde(default)] + pub autoneg: bool, + /// LLDP configuration for this port + pub lldp: Option, + /// TX-EQ configuration for this port + pub tx_eq: Option, +} + +/// A set of switch uplinks. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct SwitchPorts { + pub uplinks: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] +pub struct HostPortConfig { + /// Switchport to use for external connectivity + pub port: String, + + /// IP Address and prefix (e.g., `192.168.0.1/16`) to apply to switchport + /// (must be in infra_ip pool). May also include an optional VLAN ID. + pub addrs: Vec, + + pub lldp: Option, + pub tx_eq: Option, +} + +impl From for HostPortConfig { + fn from(x: PortConfig) -> Self { + Self { + port: x.port, + addrs: x.addresses, + lldp: x.lldp.clone(), + tx_eq: x.tx_eq, + } + } +} + +/// Identifies switch physical location +#[derive( + Clone, + Copy, + Debug, + Deserialize, + Serialize, + PartialEq, + JsonSchema, + Hash, + Eq, + PartialOrd, + Ord, +)] +#[serde(rename_all = "snake_case")] +pub enum SwitchLocation { + /// Switch in upper slot + Switch0, + /// Switch in lower slot + Switch1, +} + +impl SwitchLocation { + /// Return the location of the other switch, not ourself. + pub const fn other(&self) -> Self { + match self { + SwitchLocation::Switch0 => SwitchLocation::Switch1, + SwitchLocation::Switch1 => SwitchLocation::Switch0, + } + } +} + +impl fmt::Display for SwitchLocation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SwitchLocation::Switch0 => write!(f, "switch0"), + SwitchLocation::Switch1 => write!(f, "switch1"), + } + } +} + +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct ParseSwitchLocationError(String); + +impl std::fmt::Display for ParseSwitchLocationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "parse switch location error: {}", self.0) + } +} + +impl FromStr for SwitchLocation { + type Err = ParseSwitchLocationError; + + fn from_str(s: &str) -> Result { + match s { + "switch0" => Ok(Self::Switch0), + "switch1" => Ok(Self::Switch1), + _ => Err(ParseSwitchLocationError(format!( + "not a valid location: {s}" + ))), + } + } +} + +#[derive(Debug, Clone, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ExternalPortDiscovery { + // Automatically discover ports via Dendrite + Auto(HashMap), + // Static configuration pairing switches with a collection of ports + Static(HashMap>), +} + +/// Switchport Speed options +#[derive( + Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Hash, +)] +#[serde(rename_all = "snake_case")] +pub enum PortSpeed { + #[serde(alias = "0G")] + Speed0G, + #[serde(alias = "1G")] + Speed1G, + #[serde(alias = "10G")] + Speed10G, + #[serde(alias = "25G")] + Speed25G, + #[serde(alias = "40G")] + Speed40G, + #[serde(alias = "50G")] + Speed50G, + #[serde(alias = "100G")] + Speed100G, + #[serde(alias = "200G")] + Speed200G, + #[serde(alias = "400G")] + Speed400G, +} + +impl fmt::Display for PortSpeed { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PortSpeed::Speed0G => write!(f, "0G"), + PortSpeed::Speed1G => write!(f, "1G"), + PortSpeed::Speed10G => write!(f, "10G"), + PortSpeed::Speed25G => write!(f, "25G"), + PortSpeed::Speed40G => write!(f, "40G"), + PortSpeed::Speed50G => write!(f, "50G"), + PortSpeed::Speed100G => write!(f, "100G"), + PortSpeed::Speed200G => write!(f, "200G"), + PortSpeed::Speed400G => write!(f, "400G"), + } + } +} + +/// Switchport FEC options +#[derive( + Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Hash, +)] +#[serde(rename_all = "snake_case")] +pub enum PortFec { + Firecode, + None, + Rs, +} + +impl fmt::Display for PortFec { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PortFec::Firecode => write!(f, "Firecode R-FEC"), + PortFec::None => write!(f, "None"), + PortFec::Rs => write!(f, "RS-FEC"), + } + } +} diff --git a/sled-agent/tests/integration_tests/early_network.rs b/sled-agent/tests/integration_tests/early_network.rs index ff1b0ee8acb..2f1707c4577 100644 --- a/sled-agent/tests/integration_tests/early_network.rs +++ b/sled-agent/tests/integration_tests/early_network.rs @@ -11,7 +11,7 @@ use bootstore::schemes::v0 as bootstore; use omicron_common::api::{ external::{ImportExportPolicy, SwitchLocation}, internal::shared::{ - BgpConfig, BgpPeerConfig, PortConfigV2, PortFec, PortSpeed, + BgpConfig, BgpPeerConfig, PortConfig, PortFec, PortSpeed, RackNetworkConfig, RouteConfig, }, }; @@ -121,7 +121,7 @@ fn current_config_example() -> (&'static str, EarlyNetworkConfig) { rack_subnet: "ff01::0/32".parse().unwrap(), infra_ip_first: Ipv4Addr::new(127, 0, 0, 1), infra_ip_last: Ipv4Addr::new(127, 1, 0, 1), - ports: vec![PortConfigV2 { + ports: vec![PortConfig { routes: vec![RouteConfig { destination: "10.1.9.32/16".parse().unwrap(), nexthop: "10.1.9.32".parse().unwrap(), diff --git a/sled-agent/types/versions/src/initial/early_networking.rs b/sled-agent/types/versions/src/initial/early_networking.rs index 64750e496ea..f8501c79633 100644 --- a/sled-agent/types/versions/src/initial/early_networking.rs +++ b/sled-agent/types/versions/src/initial/early_networking.rs @@ -68,8 +68,8 @@ pub mod back_compat { use omicron_common::api::{ external::SwitchLocation, internal::shared::{ - BfdPeerConfig, BgpConfig, BgpPeerConfig, PortConfigV2, PortFec, - PortSpeed, RackNetworkConfigV2, RouteConfig, UplinkAddressConfig, + BfdPeerConfig, BgpConfig, BgpPeerConfig, PortConfig, PortFec, + PortSpeed, RackNetworkConfig, RouteConfig, UplinkAddressConfig, }, }; use oxnet::{IpNet, Ipv4Net, Ipv6Net}; @@ -117,22 +117,22 @@ pub mod back_compat { impl RackNetworkConfigV0 { /// Convert from `RackNetworkConfigV0` to `RackNetworkConfigV1` /// - /// We cannot use `From for `RackNetworkConfigV2` + /// We cannot use `From for `RackNetworkConfig` /// because the `rack_subnet` field does not exist in `RackNetworkConfigV0` /// and must be passed in from the `EarlyNetworkConfigV0` struct which /// contains the `RackNetworkConfigV0` struct. pub fn to_v2( rack_subnet: Ipv6Addr, v0: RackNetworkConfigV0, - ) -> RackNetworkConfigV2 { - RackNetworkConfigV2 { + ) -> RackNetworkConfig { + RackNetworkConfig { rack_subnet: Ipv6Net::new(rack_subnet, 56).unwrap(), infra_ip_first: v0.infra_ip_first, infra_ip_last: v0.infra_ip_last, ports: v0 .uplinks .into_iter() - .map(|uplink| PortConfigV2::from(uplink)) + .map(|uplink| PortConfig::from(uplink)) .collect(), bgp: vec![], bfd: vec![], @@ -140,7 +140,7 @@ pub mod back_compat { } } - /// Deprecated, use PortConfigV2 instead. Cannot actually deprecate due to + /// Deprecated, use PortConfig instead. Cannot actually deprecate due to /// #[derive(Clone, Debug, PartialEq, Deserialize, Serialize, JsonSchema)] pub struct PortConfigV1 { @@ -163,9 +163,9 @@ pub mod back_compat { pub autoneg: bool, } - impl From for PortConfigV2 { + impl From for PortConfig { fn from(v1: PortConfigV1) -> Self { - PortConfigV2 { + PortConfig { routes: v1.routes.clone(), addresses: v1 .addresses @@ -184,7 +184,7 @@ pub mod back_compat { } } - /// Deprecated, use PortConfigV2 instead. Cannot actually deprecate due to + /// Deprecated, use PortConfig instead. Cannot actually deprecate due to /// #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] pub struct UplinkConfig { @@ -207,9 +207,9 @@ pub mod back_compat { pub rib_priority: Option, } - impl From for PortConfigV2 { + impl From for PortConfig { fn from(value: UplinkConfig) -> Self { - PortConfigV2 { + PortConfig { routes: vec![RouteConfig { destination: "0.0.0.0/0".parse().unwrap(), nexthop: value.gateway_ip.into(), @@ -255,16 +255,16 @@ pub mod back_compat { pub bfd: Vec, } - impl From for RackNetworkConfigV2 { + impl From for RackNetworkConfig { fn from(v1: RackNetworkConfigV1) -> Self { - RackNetworkConfigV2 { + RackNetworkConfig { rack_subnet: v1.rack_subnet, infra_ip_first: v1.infra_ip_first, infra_ip_last: v1.infra_ip_last, ports: v1 .ports .into_iter() - .map(|ports| PortConfigV2::from(ports)) + .map(|ports| PortConfig::from(ports)) .collect(), bgp: v1.bgp.clone(), bfd: v1.bfd.clone(), @@ -275,7 +275,7 @@ pub mod back_compat { // The second production version of the `EarlyNetworkConfig`. // // If this version is in the bootstore than we need to convert it to - // `EarlyNetworkConfigV2`. + // `EarlyNetworkConfig`. // // Once we do this for all customers that have initialized racks with the // old version we can go ahead and remove this type and its conversion code @@ -298,7 +298,7 @@ pub mod back_compat { // The first production version of the `EarlyNetworkConfig`. // // If this version is in the bootstore than we need to convert it to - // `EarlyNetworkConfigV2`. + // `EarlyNetworkConfig`. // // Once we do this for all customers that have initialized racks with the // old version we can go ahead and remove this type and its conversion code @@ -329,10 +329,10 @@ mod tests { use std::net::Ipv6Addr; use omicron_common::api::external::SwitchLocation; - use omicron_common::api::internal::shared::PortConfigV2; + use omicron_common::api::internal::shared::PortConfig; use omicron_common::api::internal::shared::PortFec; use omicron_common::api::internal::shared::PortSpeed; - use omicron_common::api::internal::shared::RackNetworkConfigV2; + use omicron_common::api::internal::shared::RackNetworkConfig; use omicron_common::api::internal::shared::RouteConfig; use omicron_common::api::internal::shared::UplinkAddressConfig; use omicron_test_utils::dev::test_setup_log; @@ -379,11 +379,11 @@ mod tests { schema_version: EarlyNetworkConfig::schema_version(), body: EarlyNetworkConfigBody { ntp_servers: v0.ntp_servers.clone(), - rack_network_config: Some(RackNetworkConfigV2 { + rack_network_config: Some(RackNetworkConfig { rack_subnet: Ipv6Net::new(v0.rack_subnet, 56).unwrap(), infra_ip_first: v0_rack_network_config.infra_ip_first, infra_ip_last: v0_rack_network_config.infra_ip_last, - ports: vec![PortConfigV2 { + ports: vec![PortConfig { routes: vec![RouteConfig { destination: "0.0.0.0/0".parse().unwrap(), nexthop: uplink.gateway_ip.into(), @@ -467,11 +467,11 @@ mod tests { schema_version: EarlyNetworkConfig::schema_version(), body: EarlyNetworkConfigBody { ntp_servers: v1.body.ntp_servers.clone(), - rack_network_config: Some(RackNetworkConfigV2 { + rack_network_config: Some(RackNetworkConfig { rack_subnet: v1_rack_network_config.rack_subnet, infra_ip_first: v1_rack_network_config.infra_ip_first, infra_ip_last: v1_rack_network_config.infra_ip_last, - ports: vec![PortConfigV2 { + ports: vec![PortConfig { routes: port.routes.clone(), addresses: vec![UplinkAddressConfig { address: port.addresses[0], From 4d42838144faef55d919ea1017bff75c43d216c1 Mon Sep 17 00:00:00 2001 From: Ryan Goodfellow Date: Wed, 21 Jan 2026 18:23:42 +0000 Subject: [PATCH 13/20] the great type migration continues --- Cargo.lock | 1 + dev-tools/ls-apis/tests/api_dependencies.out | 4 +- openapi/bootstrap-agent-lockstep.json | 8 +- openapi/nexus-lockstep.json | 8 +- openapi/wicketd.json | 2 +- sled-agent/src/bin/sled-agent.rs | 4 +- sled-agent/src/rack_setup/service.rs | 9 +- sled-agent/src/sim/server.rs | 4 +- sled-agent/types/src/rack_init.rs | 560 ++++-------------- sled-agent/types/versions/Cargo.toml | 1 + sled-agent/types/versions/src/initial/mod.rs | 1 + .../types/versions/src/initial/rack_init.rs | 232 ++++++++ sled-agent/types/versions/src/latest.rs | 4 + wicket-common/src/rack_setup.rs | 8 +- wicketd/src/rss_config.rs | 12 +- 15 files changed, 392 insertions(+), 466 deletions(-) create mode 100644 sled-agent/types/versions/src/initial/rack_init.rs diff --git a/Cargo.lock b/Cargo.lock index c9eff48878e..86d306fb954 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13166,6 +13166,7 @@ dependencies = [ name = "sled-agent-types-versions" version = "0.1.0" dependencies = [ + "anyhow", "async-trait", "bootstore", "camino", diff --git a/dev-tools/ls-apis/tests/api_dependencies.out b/dev-tools/ls-apis/tests/api_dependencies.out index 4c9d26402c3..61a2aebb13f 100644 --- a/dev-tools/ls-apis/tests/api_dependencies.out +++ b/dev-tools/ls-apis/tests/api_dependencies.out @@ -1,9 +1,9 @@ Bootstrap Agent (client: bootstrap-agent-client) consumed by: omicron-sled-agent (omicron/sled-agent) via 1 path - consumed by: wicketd (omicron/wicketd) via 2 paths + consumed by: wicketd (omicron/wicketd) via 1 path Bootstrap Agent Lockstep API (client: bootstrap-agent-lockstep-client) - consumed by: wicketd (omicron/wicketd) via 1 path + consumed by: wicketd (omicron/wicketd) via 2 paths Clickhouse Cluster Admin for Keepers (client: clickhouse-admin-keeper-client) consumed by: omicron-nexus (omicron/nexus) via 3 paths diff --git a/openapi/bootstrap-agent-lockstep.json b/openapi/bootstrap-agent-lockstep.json index c39c7a85bb6..63c018de9ed 100644 --- a/openapi/bootstrap-agent-lockstep.json +++ b/openapi/bootstrap-agent-lockstep.json @@ -722,7 +722,7 @@ "description": "Password hashes must be in PHC (Password Hashing Competition) string format. Passwords must be hashed with Argon2id. Password hashes may be rejected if the parameters appear not to be secure enough.", "type": "string" }, - "PortConfigV2": { + "PortConfig": { "type": "object", "properties": { "addresses": { @@ -909,7 +909,7 @@ "description": "Initial rack network configuration.", "allOf": [ { - "$ref": "#/components/schemas/RackNetworkConfigV2" + "$ref": "#/components/schemas/RackNetworkConfig" } ] }, @@ -942,7 +942,7 @@ "recovery_silo" ] }, - "RackNetworkConfigV2": { + "RackNetworkConfig": { "description": "Initial network configuration", "type": "object", "properties": { @@ -975,7 +975,7 @@ "description": "Uplinks for connecting the rack to external networks", "type": "array", "items": { - "$ref": "#/components/schemas/PortConfigV2" + "$ref": "#/components/schemas/PortConfig" } }, "rack_subnet": { diff --git a/openapi/nexus-lockstep.json b/openapi/nexus-lockstep.json index 87aecb12208..8c900050a2e 100644 --- a/openapi/nexus-lockstep.json +++ b/openapi/nexus-lockstep.json @@ -7281,7 +7281,7 @@ "waiting_zones" ] }, - "PortConfigV2": { + "PortConfig": { "type": "object", "properties": { "addresses": { @@ -7834,7 +7834,7 @@ "description": "Initial rack network configuration", "allOf": [ { - "$ref": "#/components/schemas/RackNetworkConfigV2" + "$ref": "#/components/schemas/RackNetworkConfig" } ] }, @@ -7869,7 +7869,7 @@ "zpools" ] }, - "RackNetworkConfigV2": { + "RackNetworkConfig": { "description": "Initial network configuration", "type": "object", "properties": { @@ -7902,7 +7902,7 @@ "description": "Uplinks for connecting the rack to external networks", "type": "array", "items": { - "$ref": "#/components/schemas/PortConfigV2" + "$ref": "#/components/schemas/PortConfig" } }, "rack_subnet": { diff --git a/openapi/wicketd.json b/openapi/wicketd.json index 752eb45af65..efcccadcaa1 100644 --- a/openapi/wicketd.json +++ b/openapi/wicketd.json @@ -7703,7 +7703,7 @@ } }, "UserSpecifiedPortConfig": { - "description": "User-specified version of [`PortConfigV2`].\n\nAll of [`PortConfigV2`] is user-specified. But we expect the port name to be a key, rather than a field as in [`PortConfigV2`]. So this has all of the fields other than the port name.\n\n[`PortConfigV2`]: omicron_common::api::internal::shared::PortConfigV2", + "description": "User-specified version of [`PortConfig`].\n\nAll of [`PortConfig`] is user-specified. But we expect the port name to be a key, rather than a field as in [`PortConfig`]. So this has all of the fields other than the port name.\n\n[`PortConfig`]: omicron_common::api::internal::shared::PortConfig", "type": "object", "properties": { "addresses": { diff --git a/sled-agent/src/bin/sled-agent.rs b/sled-agent/src/bin/sled-agent.rs index 19094301b8d..5500ae5dbb6 100644 --- a/sled-agent/src/bin/sled-agent.rs +++ b/sled-agent/src/bin/sled-agent.rs @@ -13,7 +13,7 @@ use omicron_sled_agent::bootstrap::RssAccessError; use omicron_sled_agent::bootstrap::server as bootstrap_server; use omicron_sled_agent::config::Config as SledConfig; use sled_agent_types::rack_init::{ - RackInitializeRequest, RackInitializeRequestParams, + RackInitializeRequestParams, rack_initialize_request_from_file, }; #[derive(Debug, Parser)] @@ -64,7 +64,7 @@ async fn do_run() -> Result<(), CmdError> { }; let rss_config = if rss_config_path.exists() { let rss_config = - RackInitializeRequest::from_file(rss_config_path) + rack_initialize_request_from_file(rss_config_path) .map_err(|e| CmdError::Failure(anyhow!(e)))?; let skip_timesync = config.skip_timesync.unwrap_or(false); Some(RackInitializeRequestParams::new( diff --git a/sled-agent/src/rack_setup/service.rs b/sled-agent/src/rack_setup/service.rs index 2619dc4d4a1..d6d32eef6f5 100644 --- a/sled-agent/src/rack_setup/service.rs +++ b/sled-agent/src/rack_setup/service.rs @@ -867,14 +867,14 @@ impl ServiceInner { let rack_network_config = { let config = &config.rack_network_config; - NexusTypes::RackNetworkConfigV2 { + NexusTypes::RackNetworkConfig { rack_subnet: config.rack_subnet, infra_ip_first: config.infra_ip_first, infra_ip_last: config.infra_ip_last, ports: config .ports .iter() - .map(|config| NexusTypes::PortConfigV2 { + .map(|config| NexusTypes::PortConfig { port: config.port.clone(), routes: config .routes @@ -1728,6 +1728,7 @@ mod test { Inventory, InventoryDisk, OmicronZoneType, SledCpuFamily, SledRole, ZoneImageResolverInventory, }; + use sled_agent_types::rack_init::rack_initialize_request_test_config; fn make_sled_info( sled_id: SledUuid, @@ -1803,7 +1804,7 @@ mod test { } fn make_test_service_plan() -> ServicePlan { - let rss_config = Config::test_config(); + let rss_config = rack_initialize_request_test_config(); let fake_sleds = make_fake_sleds(); let service_plan = ServicePlan::create_transient(&rss_config, fake_sleds) @@ -1934,7 +1935,7 @@ mod test { let fake_sleds = make_fake_sleds(); - let rss_config = Config::test_config(); + let rss_config = rack_initialize_request_test_config(); let service_plan = ServicePlan::create_transient(&rss_config, fake_sleds) .expect("created service plan"); diff --git a/sled-agent/src/sim/server.rs b/sled-agent/src/sim/server.rs index 260e4ef4c06..54eea8fdd24 100644 --- a/sled-agent/src/sim/server.rs +++ b/sled-agent/src/sim/server.rs @@ -28,7 +28,7 @@ use nexus_config::NUM_INITIAL_RESERVED_IP_ADDRESSES; use nexus_lockstep_client::types::{ AllowedSourceIps, CrucibleDatasetCreateRequest, ExternalPortDiscovery, IpRange, Ipv4Range, Ipv6Range, RackInitializationRequest, - RackNetworkConfigV2, + RackNetworkConfig, }; use nexus_types::deployment::{ BlueprintPhysicalDiskConfig, BlueprintPhysicalDiskDisposition, @@ -635,7 +635,7 @@ pub async fn run_standalone_server( external_dns_zone_name: DNS_ZONE_EXTERNAL_TESTING.to_owned(), recovery_silo, external_port_count: ExternalPortDiscovery::Static(HashMap::new()), - rack_network_config: RackNetworkConfigV2 { + rack_network_config: RackNetworkConfig { rack_subnet: Ipv6Net::host_net(Ipv6Addr::LOCALHOST), infra_ip_first: Ipv4Addr::LOCALHOST, infra_ip_last: Ipv4Addr::LOCALHOST, diff --git a/sled-agent/types/src/rack_init.rs b/sled-agent/types/src/rack_init.rs index 499e2c647ad..d72449c93bb 100644 --- a/sled-agent/types/src/rack_init.rs +++ b/sled-agent/types/src/rack_init.rs @@ -4,35 +4,27 @@ //! Rack initialization types. -use std::{ - collections::BTreeSet, - net::{IpAddr, Ipv6Addr}, -}; +use std::net::IpAddr; -use anyhow::{Result, bail}; -use camino::{Utf8Path, Utf8PathBuf}; +use anyhow::Result; +use camino::Utf8Path; use omicron_common::{ - address::{ - AZ_PREFIX, IpRange, Ipv6Subnet, RACK_PREFIX, SLED_PREFIX, get_64_subnet, - }, - api::{ - external::AllowedSourceIps, - internal::{nexus::Certificate, shared::RackNetworkConfig}, - }, + address::IpRange, + api::{external::AllowedSourceIps, internal::nexus::Certificate}, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; pub use sled_agent_types_versions::latest::rack_init::*; use sled_hardware_types::Baseboard; +use crate::early_networking::back_compat::RackNetworkConfigV1; + /// Structures and routines used to maintain backwards compatibility. The /// contents of this module should only be used to convert older data into the /// current format, and not for any ongoing run-time operations. pub mod back_compat { use omicron_common::api::internal::nexus::Certificate; - use crate::early_networking::back_compat::RackNetworkConfigV1; - use super::*; #[derive(Clone, Deserialize)] @@ -51,6 +43,26 @@ pub mod back_compat { allowed_source_ips: AllowedSourceIps, } + fn validate_external_dns( + dns_ips: &Vec, + internal_ranges: &Vec, + ) -> Result<()> { + use anyhow::bail; + if dns_ips.is_empty() { + bail!("At least one external DNS IP is required"); + } + + for &dns_ip in dns_ips { + if !internal_ranges.iter().any(|range| range.contains(dns_ip)) { + bail!( + "External DNS IP {dns_ip} is not contained in \ + `internal_services_ip_pool_ranges`" + ); + } + } + Ok(()) + } + /// This is a deprecated format, maintained to allow importing from older /// versions. #[derive(Clone, Debug, PartialEq, Deserialize, Serialize, JsonSchema)] @@ -116,295 +128,98 @@ pub mod back_compat { } } -// "Shadow" copy of `RackInitializeRequest` that does no validation on its -// fields. -#[derive(Clone, Deserialize)] -struct UnvalidatedRackInitializeRequest { - trust_quorum_peers: Option>, - bootstrap_discovery: BootstrapAddressDiscovery, - ntp_servers: Vec, - dns_servers: Vec, - internal_services_ip_pool_ranges: Vec, - external_dns_ips: Vec, - external_dns_zone_name: String, - external_certificates: Vec, - recovery_silo: RecoverySiloConfig, - rack_network_config: RackNetworkConfig, - #[serde(default = "default_allowed_source_ips")] - allowed_source_ips: AllowedSourceIps, -} - -fn validate_external_dns( - dns_ips: &Vec, - internal_ranges: &Vec, -) -> Result<()> { - if dns_ips.is_empty() { - bail!("At least one external DNS IP is required"); - } - - // Every external DNS IP should also be present in one of the internal - // services IP pool ranges. This check is O(N*M), but we expect both N - // and M to be small (~5 DNS servers, and a small number of pools). - for &dns_ip in dns_ips { - if !internal_ranges.iter().any(|range| range.contains(dns_ip)) { - bail!( - "External DNS IP {dns_ip} is not contained in \ - `internal_services_ip_pool_ranges`" - ); - } - } - Ok(()) -} - -impl TryFrom for RackInitializeRequest { - type Error = anyhow::Error; - - fn try_from(value: UnvalidatedRackInitializeRequest) -> Result { - validate_external_dns( - &value.external_dns_ips, - &value.internal_services_ip_pool_ranges, - )?; - - Ok(RackInitializeRequest { - trust_quorum_peers: value.trust_quorum_peers, - bootstrap_discovery: value.bootstrap_discovery, - ntp_servers: value.ntp_servers, - dns_servers: value.dns_servers, - internal_services_ip_pool_ranges: value - .internal_services_ip_pool_ranges, - external_dns_ips: value.external_dns_ips, - external_dns_zone_name: value.external_dns_zone_name, - external_certificates: value.external_certificates, - recovery_silo: value.recovery_silo, - rack_network_config: value.rack_network_config, - allowed_source_ips: value.allowed_source_ips, - }) - } -} - -/// Configuration for the "rack setup service". -/// -/// The Rack Setup Service should be responsible for one-time setup actions, -/// such as CockroachDB placement and initialization. Without operator -/// intervention, however, these actions need a way to be automated in our -/// deployment. -#[derive(Clone, Deserialize, Serialize, PartialEq, JsonSchema)] -#[serde(try_from = "UnvalidatedRackInitializeRequest")] -pub struct RackInitializeRequest { - /// The set of peer_ids required to initialize trust quorum - /// - /// The value is `None` if we are not using trust quorum - pub trust_quorum_peers: Option>, - - /// Describes how bootstrap addresses should be collected during RSS. - pub bootstrap_discovery: BootstrapAddressDiscovery, - - /// The external NTP server addresses. - pub ntp_servers: Vec, - - /// The external DNS server addresses. - pub dns_servers: Vec, - - /// Ranges of the service IP pool which may be used for internal services. - // TODO(https://github.com/oxidecomputer/omicron/issues/1530): Eventually, - // we want to configure multiple pools. - pub internal_services_ip_pool_ranges: Vec, - - /// Service IP addresses on which we run external DNS servers. - /// - /// Each address must be present in `internal_services_ip_pool_ranges`. - pub external_dns_ips: Vec, - - /// DNS name for the DNS zone delegated to the rack for external DNS - pub external_dns_zone_name: String, - - /// initial TLS certificates for the external API - pub external_certificates: Vec, - - /// Configuration of the Recovery Silo (the initial Silo) - pub recovery_silo: RecoverySiloConfig, - - /// Initial rack network configuration - pub rack_network_config: RackNetworkConfig, - - /// IPs or subnets allowed to make requests to user-facing services - #[serde(default = "default_allowed_source_ips")] - pub allowed_source_ips: AllowedSourceIps, -} - -impl RackInitializeRequest { - pub fn from_file>( - path: P, - ) -> Result { - let path = path.as_ref(); - let contents = std::fs::read_to_string(&path).map_err(|err| { - RackInitializeRequestParseError::Io { path: path.into(), err } - })?; - let mut raw_config = - Self::from_toml_with_fallback(&contents).map_err(|err| { - RackInitializeRequestParseError::Deserialize { - path: path.into(), - err, - } - })?; - - // In the same way that sled-agent itself (our caller) discovers the - // optional config-rss.toml in a well-known path relative to its config - // file, we look for a pair of well-known paths adjacent to - // config-rss.toml that specify an extra TLS certificate and private - // key. This is used by the end-to-end tests. Any developer can also - // use this to inject a TLS certificate into their setup. - // (config-rss.toml is only used for dev/test, not production - // deployments, which will always get their RSS configuration from - // Wicket.) - if let Some(parent) = path.parent() { - let cert_path = parent.join("initial-tls-cert.pem"); - let key_path = parent.join("initial-tls-key.pem"); - let cert_bytes = std::fs::read_to_string(&cert_path); - let key_bytes = std::fs::read_to_string(&key_path); - match (cert_bytes, key_bytes) { - (Ok(cert), Ok(key)) => { - raw_config - .external_certificates - .push(Certificate { key, cert }); - } - (Err(cert_error), Err(key_error)) - if cert_error.kind() == std::io::ErrorKind::NotFound - && key_error.kind() == std::io::ErrorKind::NotFound => - { - // Fine. No extra cert was provided. - } - (Err(cert_error), _) => { - return Err(RackInitializeRequestParseError::Certificate( - anyhow::Error::new(cert_error).context(format!( - "loading certificate from {:?}", - cert_path - )), - )); - } - (_, Err(key_error)) => { - return Err(RackInitializeRequestParseError::Certificate( - anyhow::Error::new(key_error).context(format!( - "loading private key from {:?}", - key_path - )), - )); - } - }; - } - - Ok(raw_config) - } - - pub fn from_toml_with_fallback( - data: &str, - ) -> Result { - // Note that if we fail to parse the request as any known - // version, we return the error corresponding to the parse - // failure for the newest schema. - toml::from_str::(&data).or_else( - |latest_version_err| match toml::from_str::< - back_compat::RackInitializeRequestV1, - >(&data) +/// Load a RackInitializeRequest from a file path. +pub fn rack_initialize_request_from_file>( + path: P, +) -> Result { + let path = path.as_ref(); + let contents = std::fs::read_to_string(&path).map_err(|err| { + RackInitializeRequestParseError::Io { path: path.into(), err } + })?; + let mut raw_config = rack_initialize_request_from_toml_with_fallback( + &contents, + ) + .map_err(|err| RackInitializeRequestParseError::Deserialize { + path: path.into(), + err, + })?; + + // In the same way that sled-agent itself (our caller) discovers the + // optional config-rss.toml in a well-known path relative to its config + // file, we look for a pair of well-known paths adjacent to + // config-rss.toml that specify an extra TLS certificate and private + // key. This is used by the end-to-end tests. Any developer can also + // use this to inject a TLS certificate into their setup. + // (config-rss.toml is only used for dev/test, not production + // deployments, which will always get their RSS configuration from + // Wicket.) + if let Some(parent) = path.parent() { + let cert_path = parent.join("initial-tls-cert.pem"); + let key_path = parent.join("initial-tls-key.pem"); + let cert_bytes = std::fs::read_to_string(&cert_path); + let key_bytes = std::fs::read_to_string(&key_path); + match (cert_bytes, key_bytes) { + (Ok(cert), Ok(key)) => { + raw_config + .external_certificates + .push(Certificate { key, cert }); + } + (Err(cert_error), Err(key_error)) + if cert_error.kind() == std::io::ErrorKind::NotFound + && key_error.kind() == std::io::ErrorKind::NotFound => { - Ok(v1) => Ok(v1.into()), - Err(_v1_err) => Err(latest_version_err.into()), - }, - ) - } - - /// Return a configuration suitable for testing. - pub fn test_config() -> Self { - // Use env! rather than std::env::var because this might be called from - // a dependent crate. - let manifest_dir = Utf8Path::new(env!("CARGO_MANIFEST_DIR")); - let path = manifest_dir - .join("../../smf/sled-agent/non-gimlet/config-rss.toml"); - let contents = std::fs::read_to_string(&path).unwrap(); - toml::from_str(&contents) - .unwrap_or_else(|e| panic!("failed to parse {:?}: {}", &path, e)) - } - - pub fn az_subnet(&self) -> Ipv6Subnet { - Ipv6Subnet::::new( - self.rack_network_config.rack_subnet.addr(), - ) - } - - /// Returns the subnet for our rack. - pub fn rack_subnet(&self) -> Ipv6Subnet { - Ipv6Subnet::::new( - self.rack_network_config.rack_subnet.addr(), - ) - } - - /// Returns the subnet for the `index`-th sled in the rack. - pub fn sled_subnet(&self, index: u8) -> Ipv6Subnet { - get_64_subnet(self.rack_subnet(), index) + // Fine. No extra cert was provided. + } + (Err(cert_error), _) => { + return Err(RackInitializeRequestParseError::Certificate( + anyhow::Error::new(cert_error).context(format!( + "loading certificate from {:?}", + cert_path + )), + )); + } + (_, Err(key_error)) => { + return Err(RackInitializeRequestParseError::Certificate( + anyhow::Error::new(key_error).context(format!( + "loading private key from {:?}", + key_path + )), + )); + } + }; } -} -#[derive(Debug, thiserror::Error)] -pub enum RackInitializeRequestParseError { - #[error("Failed to read config from {path}: {err}")] - Io { - path: Utf8PathBuf, - #[source] - err: std::io::Error, - }, - #[error("Failed to deserialize config from {path}: {err}")] - Deserialize { - path: Utf8PathBuf, - #[source] - err: anyhow::Error, - }, - #[error("Loading certificate: {0}")] - Certificate(#[source] anyhow::Error), + Ok(raw_config) } -/// This field was added after several racks were already deployed. RSS plans -/// for those racks should default to allowing any source IP, since that is -/// effectively what they did. -const fn default_allowed_source_ips() -> AllowedSourceIps { - AllowedSourceIps::Any +/// Parse a RackInitializeRequest from TOML, with fallback to older versions. +pub fn rack_initialize_request_from_toml_with_fallback( + data: &str, +) -> Result { + // Note that if we fail to parse the request as any known + // version, we return the error corresponding to the parse + // failure for the newest schema. + toml::from_str::(&data).or_else( + |latest_version_err| match toml::from_str::< + back_compat::RackInitializeRequestV1, + >(&data) + { + Ok(v1) => Ok(v1.into()), + Err(_v1_err) => Err(latest_version_err.into()), + }, + ) } -// This custom debug implementation hides the private keys. -impl std::fmt::Debug for RackInitializeRequest { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // If you find a compiler error here, and you just added a field to this - // struct, be sure to add it to the Debug impl below! - let RackInitializeRequest { - trust_quorum_peers, - bootstrap_discovery, - ntp_servers, - dns_servers, - internal_services_ip_pool_ranges, - external_dns_ips, - external_dns_zone_name, - external_certificates: _, - recovery_silo, - rack_network_config, - allowed_source_ips, - } = &self; - - f.debug_struct("RackInitializeRequest") - .field("trust_quorum_peers", trust_quorum_peers) - .field("bootstrap_discovery", bootstrap_discovery) - .field("ntp_servers", ntp_servers) - .field("dns_servers", dns_servers) - .field( - "internal_services_ip_pool_ranges", - internal_services_ip_pool_ranges, - ) - .field("external_dns_ips", external_dns_ips) - .field("external_dns_zone_name", external_dns_zone_name) - .field("external_certificates", &"") - .field("recovery_silo", recovery_silo) - .field("rack_network_config", rack_network_config) - .field("allowed_source_ips", allowed_source_ips) - .finish() - } +/// Return a RackInitializeRequest configuration suitable for testing. +pub fn rack_initialize_request_test_config() -> RackInitializeRequest { + // Use env! rather than std::env::var because this might be called from + // a dependent crate. + let manifest_dir = Utf8Path::new(env!("CARGO_MANIFEST_DIR")); + let path = + manifest_dir.join("../../smf/sled-agent/non-gimlet/config-rss.toml"); + let contents = std::fs::read_to_string(&path).unwrap(); + toml::from_str(&contents) + .unwrap_or_else(|e| panic!("failed to parse {:?}: {}", &path, e)) } #[derive(Debug, Clone)] @@ -422,20 +237,15 @@ impl RackInitializeRequestParams { } } -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case", tag = "type")] -pub enum BootstrapAddressDiscovery { - /// Ignore all bootstrap addresses except our own. - OnlyOurs, - /// Ignore all bootstrap addresses except the following. - OnlyThese { addrs: BTreeSet }, -} - #[cfg(test)] mod tests { use std::net::Ipv4Addr; use std::net::Ipv6Addr; + use camino::Utf8PathBuf; + use omicron_common::address::{AZ_PREFIX, RACK_PREFIX, SLED_PREFIX}; + use omicron_common::api::internal::shared::RackNetworkConfig; + use super::*; use anyhow::Context; use oxnet::Ipv6Net; @@ -485,130 +295,6 @@ mod tests { )); } - #[test] - fn validate_external_dns_ips_must_be_in_internal_services_ip_pools() { - // Conjure up a config; we'll tweak the internal services pools and - // external DNS IPs, but no other fields matter. - let mut config = UnvalidatedRackInitializeRequest { - trust_quorum_peers: None, - bootstrap_discovery: BootstrapAddressDiscovery::OnlyOurs, - ntp_servers: Vec::new(), - dns_servers: Vec::new(), - internal_services_ip_pool_ranges: Vec::new(), - external_dns_ips: Vec::new(), - external_dns_zone_name: "".to_string(), - external_certificates: Vec::new(), - recovery_silo: RecoverySiloConfig { - silo_name: "recovery".parse().unwrap(), - user_name: "recovery".parse().unwrap(), - // Generated via `cargo run --example argon2 -- --input oxide`. - user_password_hash: - "$argon2id$v=19$m=98304,t=23,p=1$Effh/p6M2ZKdnpJFeGqtGQ$\ - ZtUwcVODAvUAVK6EQ5FJMv+GMlUCo9PQQsy9cagL+EU" - .parse() - .unwrap(), - }, - rack_network_config: RackNetworkConfig { - rack_subnet: Ipv6Net::host_net(Ipv6Addr::LOCALHOST), - infra_ip_first: Ipv4Addr::LOCALHOST, - infra_ip_last: Ipv4Addr::LOCALHOST, - ports: Vec::new(), - bgp: Vec::new(), - bfd: Vec::new(), - }, - allowed_source_ips: AllowedSourceIps::Any, - }; - - // Valid configs: all external DNS IPs are contained in the IP pool - // ranges. - for (ip_pool_ranges, dns_ips) in [ - ( - &[("fd00::1", "fd00::10")] as &[(&str, &str)], - &["fd00::1", "fd00::5", "fd00::10"] as &[&str], - ), - ( - &[("192.168.1.10", "192.168.1.20")], - &["192.168.1.10", "192.168.1.15", "192.168.1.20"], - ), - ( - &[("fd00::1", "fd00::10"), ("192.168.1.10", "192.168.1.20")], - &[ - "fd00::1", - "fd00::5", - "fd00::10", - "192.168.1.10", - "192.168.1.15", - "192.168.1.20", - ], - ), - ] { - config.internal_services_ip_pool_ranges = ip_pool_ranges - .iter() - .map(|(a, b)| { - IpRange::try_from(( - a.parse::().unwrap(), - b.parse::().unwrap(), - )) - .unwrap() - }) - .collect(); - config.external_dns_ips = - dns_ips.iter().map(|ip| ip.parse().unwrap()).collect(); - - match RackInitializeRequest::try_from(config.clone()) { - Ok(_) => (), - Err(err) => panic!( - "failure on {ip_pool_ranges:?} with DNS IPs {dns_ips:?}: \ - {err}" - ), - } - } - - // Invalid configs: either no DNS IPs, or one or more DNS IPs are not - // contained in the ip pool ranges. - for (ip_pool_ranges, dns_ips) in [ - (&[("fd00::1", "fd00::10")] as &[(&str, &str)], &[] as &[&str]), - (&[("fd00::1", "fd00::10")], &["fd00::1", "fd00::5", "fd00::11"]), - ( - &[("192.168.1.10", "192.168.1.20")], - &["192.168.1.9", "192.168.1.15", "192.168.1.20"], - ), - ( - &[("fd00::1", "fd00::10"), ("192.168.1.10", "192.168.1.20")], - &[ - "fd00::1", - "fd00::5", - "fd00::10", - "192.168.1.10", - "192.168.1.15", - "192.168.1.20", - "192.168.1.21", - ], - ), - ] { - config.internal_services_ip_pool_ranges = ip_pool_ranges - .iter() - .map(|(a, b)| { - IpRange::try_from(( - a.parse::().unwrap(), - b.parse::().unwrap(), - )) - .unwrap() - }) - .collect(); - config.external_dns_ips = - dns_ips.iter().map(|ip| ip.parse().unwrap()).collect(); - - match RackInitializeRequest::try_from(config.clone()) { - Ok(_) => panic!( - "unexpected success on {ip_pool_ranges:?} with \ - DNS IPs {dns_ips:?}" - ), - Err(_) => (), - } - } - } - #[test] fn test_subnets() { let cfg = RackInitializeRequest { @@ -649,7 +335,7 @@ mod tests { }; assert_eq!( - Ipv6Subnet::::new( + omicron_common::address::Ipv6Subnet::::new( // Masked out in AZ Subnet // vv "fd00:1122:3344:0000::".parse::().unwrap(), @@ -657,7 +343,7 @@ mod tests { cfg.az_subnet() ); assert_eq!( - Ipv6Subnet::::new( + omicron_common::address::Ipv6Subnet::::new( // Shows up from Rack Subnet // vv "fd00:1122:3344:0100::".parse::().unwrap(), @@ -665,7 +351,7 @@ mod tests { cfg.rack_subnet() ); assert_eq!( - Ipv6Subnet::::new( + omicron_common::address::Ipv6Subnet::::new( // 0th Sled Subnet // vv "fd00:1122:3344:0100::".parse::().unwrap(), @@ -673,7 +359,7 @@ mod tests { cfg.sled_subnet(0) ); assert_eq!( - Ipv6Subnet::::new( + omicron_common::address::Ipv6Subnet::::new( // 1st Sled Subnet // vv "fd00:1122:3344:0101::".parse::().unwrap(), @@ -681,7 +367,7 @@ mod tests { cfg.sled_subnet(1) ); assert_eq!( - Ipv6Subnet::::new( + omicron_common::address::Ipv6Subnet::::new( // Last Sled Subnet // vv "fd00:1122:3344:01ff::".parse::().unwrap(), @@ -695,7 +381,7 @@ mod tests { // The stock non-Gimlet config has no TLS certificates. let path = Utf8PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../../smf/sled-agent/non-gimlet/config-rss.toml"); - let cfg = RackInitializeRequest::from_file(&path) + let cfg = rack_initialize_request_from_file(&path) .unwrap_or_else(|e| panic!("failed to parse {:?}: {}", &path, e)); assert!(cfg.external_certificates.is_empty()); @@ -745,7 +431,7 @@ mod tests { .unwrap(); // Now try to load it all. - let read_cfg = RackInitializeRequest::from_file(&cfg_path) + let read_cfg = rack_initialize_request_from_file(&cfg_path) .expect("failed to read generated config with certificate"); assert_eq!(read_cfg.external_certificates.len(), 1); let cert = read_cfg.external_certificates.first().unwrap(); diff --git a/sled-agent/types/versions/Cargo.toml b/sled-agent/types/versions/Cargo.toml index 3cf988b53b3..d3508368670 100644 --- a/sled-agent/types/versions/Cargo.toml +++ b/sled-agent/types/versions/Cargo.toml @@ -8,6 +8,7 @@ license = "MPL-2.0" workspace = true [dependencies] +anyhow.workspace = true camino.workspace = true chrono.workspace = true daft.workspace = true diff --git a/sled-agent/types/versions/src/initial/mod.rs b/sled-agent/types/versions/src/initial/mod.rs index e3dd7c4059e..c5527af60e3 100644 --- a/sled-agent/types/versions/src/initial/mod.rs +++ b/sled-agent/types/versions/src/initial/mod.rs @@ -12,6 +12,7 @@ pub mod disk; pub mod early_networking; pub mod instance; pub mod inventory; +pub mod rack_init; pub mod sled; pub mod support_bundle; pub mod zone_bundle; diff --git a/sled-agent/types/versions/src/initial/rack_init.rs b/sled-agent/types/versions/src/initial/rack_init.rs new file mode 100644 index 00000000000..45ebd6a7925 --- /dev/null +++ b/sled-agent/types/versions/src/initial/rack_init.rs @@ -0,0 +1,232 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Rack initialization types + +use std::{collections::BTreeSet, net::IpAddr, net::Ipv6Addr}; + +use anyhow::{Result, bail}; +use camino::Utf8PathBuf; +use omicron_common::{ + address::{ + AZ_PREFIX, IpRange, Ipv6Subnet, RACK_PREFIX, SLED_PREFIX, get_64_subnet, + }, + api::{ + external::AllowedSourceIps, + internal::{nexus::Certificate, shared::RackNetworkConfig}, + }, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use sled_hardware_types::Baseboard; + +use crate::bootstrap_v1::rack_init::RecoverySiloConfig; + +// "Shadow" copy of `RackInitializeRequest` that does no validation on its +// fields. +#[derive(Clone, Deserialize)] +struct UnvalidatedRackInitializeRequest { + trust_quorum_peers: Option>, + bootstrap_discovery: BootstrapAddressDiscovery, + ntp_servers: Vec, + dns_servers: Vec, + internal_services_ip_pool_ranges: Vec, + external_dns_ips: Vec, + external_dns_zone_name: String, + external_certificates: Vec, + recovery_silo: RecoverySiloConfig, + rack_network_config: RackNetworkConfig, + #[serde(default = "default_allowed_source_ips")] + allowed_source_ips: AllowedSourceIps, +} + +fn validate_external_dns( + dns_ips: &Vec, + internal_ranges: &Vec, +) -> Result<()> { + if dns_ips.is_empty() { + bail!("At least one external DNS IP is required"); + } + + // Every external DNS IP should also be present in one of the internal + // services IP pool ranges. This check is O(N*M), but we expect both N + // and M to be small (~5 DNS servers, and a small number of pools). + for &dns_ip in dns_ips { + if !internal_ranges.iter().any(|range| range.contains(dns_ip)) { + bail!( + "External DNS IP {dns_ip} is not contained in \ + `internal_services_ip_pool_ranges`" + ); + } + } + Ok(()) +} + +impl TryFrom for RackInitializeRequest { + type Error = anyhow::Error; + + fn try_from(value: UnvalidatedRackInitializeRequest) -> Result { + validate_external_dns( + &value.external_dns_ips, + &value.internal_services_ip_pool_ranges, + )?; + + Ok(RackInitializeRequest { + trust_quorum_peers: value.trust_quorum_peers, + bootstrap_discovery: value.bootstrap_discovery, + ntp_servers: value.ntp_servers, + dns_servers: value.dns_servers, + internal_services_ip_pool_ranges: value + .internal_services_ip_pool_ranges, + external_dns_ips: value.external_dns_ips, + external_dns_zone_name: value.external_dns_zone_name, + external_certificates: value.external_certificates, + recovery_silo: value.recovery_silo, + rack_network_config: value.rack_network_config, + allowed_source_ips: value.allowed_source_ips, + }) + } +} + +/// Configuration for the "rack setup service". +/// +/// The Rack Setup Service should be responsible for one-time setup actions, +/// such as CockroachDB placement and initialization. Without operator +/// intervention, however, these actions need a way to be automated in our +/// deployment. +#[derive(Clone, Deserialize, Serialize, PartialEq, JsonSchema)] +#[serde(try_from = "UnvalidatedRackInitializeRequest")] +pub struct RackInitializeRequest { + /// The set of peer_ids required to initialize trust quorum + /// + /// The value is `None` if we are not using trust quorum + pub trust_quorum_peers: Option>, + + /// Describes how bootstrap addresses should be collected during RSS. + pub bootstrap_discovery: BootstrapAddressDiscovery, + + /// The external NTP server addresses. + pub ntp_servers: Vec, + + /// The external DNS server addresses. + pub dns_servers: Vec, + + /// Ranges of the service IP pool which may be used for internal services. + // TODO(https://github.com/oxidecomputer/omicron/issues/1530): Eventually, + // we want to configure multiple pools. + pub internal_services_ip_pool_ranges: Vec, + + /// Service IP addresses on which we run external DNS servers. + /// + /// Each address must be present in `internal_services_ip_pool_ranges`. + pub external_dns_ips: Vec, + + /// DNS name for the DNS zone delegated to the rack for external DNS + pub external_dns_zone_name: String, + + /// initial TLS certificates for the external API + pub external_certificates: Vec, + + /// Configuration of the Recovery Silo (the initial Silo) + pub recovery_silo: RecoverySiloConfig, + + /// Initial rack network configuration + pub rack_network_config: RackNetworkConfig, + + /// IPs or subnets allowed to make requests to user-facing services + #[serde(default = "default_allowed_source_ips")] + pub allowed_source_ips: AllowedSourceIps, +} + +impl RackInitializeRequest { + pub fn az_subnet(&self) -> Ipv6Subnet { + Ipv6Subnet::::new( + self.rack_network_config.rack_subnet.addr(), + ) + } + + /// Returns the subnet for our rack. + pub fn rack_subnet(&self) -> Ipv6Subnet { + Ipv6Subnet::::new( + self.rack_network_config.rack_subnet.addr(), + ) + } + + /// Returns the subnet for the `index`-th sled in the rack. + pub fn sled_subnet(&self, index: u8) -> Ipv6Subnet { + get_64_subnet(self.rack_subnet(), index) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum RackInitializeRequestParseError { + #[error("Failed to read config from {path}: {err}")] + Io { + path: Utf8PathBuf, + #[source] + err: std::io::Error, + }, + #[error("Failed to deserialize config from {path}: {err}")] + Deserialize { + path: Utf8PathBuf, + #[source] + err: anyhow::Error, + }, + #[error("Loading certificate: {0}")] + Certificate(#[source] anyhow::Error), +} + +/// This field was added after several racks were already deployed. RSS plans +/// for those racks should default to allowing any source IP, since that is +/// effectively what they did. +pub const fn default_allowed_source_ips() -> AllowedSourceIps { + AllowedSourceIps::Any +} + +// This custom debug implementation hides the private keys. +impl std::fmt::Debug for RackInitializeRequest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // If you find a compiler error here, and you just added a field to this + // struct, be sure to add it to the Debug impl below! + let RackInitializeRequest { + trust_quorum_peers, + bootstrap_discovery, + ntp_servers, + dns_servers, + internal_services_ip_pool_ranges, + external_dns_ips, + external_dns_zone_name, + external_certificates: _, + recovery_silo, + rack_network_config, + allowed_source_ips, + } = &self; + + f.debug_struct("RackInitializeRequest") + .field("trust_quorum_peers", trust_quorum_peers) + .field("bootstrap_discovery", bootstrap_discovery) + .field("ntp_servers", ntp_servers) + .field("dns_servers", dns_servers) + .field( + "internal_services_ip_pool_ranges", + internal_services_ip_pool_ranges, + ) + .field("external_dns_ips", external_dns_ips) + .field("external_dns_zone_name", external_dns_zone_name) + .field("external_certificates", &"") + .field("recovery_silo", recovery_silo) + .field("rack_network_config", rack_network_config) + .field("allowed_source_ips", allowed_source_ips) + .finish() + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case", tag = "type")] +pub enum BootstrapAddressDiscovery { + /// Ignore all bootstrap addresses except our own. + OnlyOurs, + /// Ignore all bootstrap addresses except the following. + OnlyThese { addrs: BTreeSet }, +} diff --git a/sled-agent/types/versions/src/latest.rs b/sled-agent/types/versions/src/latest.rs index 78b1897e2b7..42c280d744b 100644 --- a/sled-agent/types/versions/src/latest.rs +++ b/sled-agent/types/versions/src/latest.rs @@ -134,6 +134,10 @@ pub mod probes { pub mod rack_init { pub use crate::bootstrap_v1::rack_init::RecoverySiloConfig; + pub use crate::v1::rack_init::BootstrapAddressDiscovery; + pub use crate::v1::rack_init::RackInitializeRequest; + pub use crate::v1::rack_init::RackInitializeRequestParseError; + pub use crate::v1::rack_init::default_allowed_source_ips; } pub mod sled { diff --git a/wicket-common/src/rack_setup.rs b/wicket-common/src/rack_setup.rs index b4da40cd9b2..73dfa647fb7 100644 --- a/wicket-common/src/rack_setup.rs +++ b/wicket-common/src/rack_setup.rs @@ -171,13 +171,13 @@ impl UserSpecifiedRackNetworkConfig { } } -/// User-specified version of [`PortConfigV2`]. +/// User-specified version of [`PortConfig`]. /// -/// All of [`PortConfigV2`] is user-specified. But we expect the port name to -/// be a key, rather than a field as in [`PortConfigV2`]. So this has all of +/// All of [`PortConfig`] is user-specified. But we expect the port name to +/// be a key, rather than a field as in [`PortConfig`]. So this has all of /// the fields other than the port name. /// -/// [`PortConfigV2`]: omicron_common::api::internal::shared::PortConfigV2 +/// [`PortConfig`]: omicron_common::api::internal::shared::PortConfig #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct UserSpecifiedPortConfig { diff --git a/wicketd/src/rss_config.rs b/wicketd/src/rss_config.rs index 8f6a781d1d8..65980d6ad17 100644 --- a/wicketd/src/rss_config.rs +++ b/wicketd/src/rss_config.rs @@ -12,7 +12,7 @@ use anyhow::bail; use bootstrap_agent_lockstep_client::types::BootstrapAddressDiscovery; use bootstrap_agent_lockstep_client::types::Certificate; use bootstrap_agent_lockstep_client::types::Name; -use bootstrap_agent_lockstep_client::types::PortConfigV2 as BaPortConfigV2; +use bootstrap_agent_lockstep_client::types::PortConfig as BaPortConfig; use bootstrap_agent_lockstep_client::types::RackInitializeRequest; use bootstrap_agent_lockstep_client::types::RecoverySiloConfig; use bootstrap_agent_lockstep_client::types::UserId; @@ -620,7 +620,7 @@ pub(crate) enum BgpAuthKeyError { fn validate_rack_network_config( config: &UserSpecifiedRackNetworkConfig, bgp_auth_keys: &BTreeMap>, -) -> Result { +) -> Result { use bootstrap_agent_lockstep_client::types::BgpConfig as BaBgpConfig; // Ensure that there is at least one uplink @@ -662,7 +662,7 @@ fn validate_rack_network_config( // TODO Add more client side checks on `rack_network_config` contents? - Ok(bootstrap_agent_lockstep_client::types::RackNetworkConfigV2 { + Ok(bootstrap_agent_lockstep_client::types::RackNetworkConfig { rack_subnet: RACK_SUBNET.net(), infra_ip_first: config.infra_ip_first, infra_ip_last: config.infra_ip_last, @@ -687,7 +687,7 @@ fn validate_rack_network_config( }) } -/// Builds a `BaPortConfigV2` from a `UserSpecifiedPortConfig`. +/// Builds a `BaPortConfig` from a `UserSpecifiedPortConfig`. /// /// Assumes that all auth keys are present in `bgp_auth_keys`. fn build_port_config( @@ -695,7 +695,7 @@ fn build_port_config( port: &str, config: &UserSpecifiedPortConfig, bgp_auth_keys: &BTreeMap>, -) -> BaPortConfigV2 { +) -> BaPortConfig { use bootstrap_agent_lockstep_client::types::BgpPeerConfig as BaBgpPeerConfig; use bootstrap_agent_lockstep_client::types::LldpAdminStatus as BaLldpAdminStatus; use bootstrap_agent_lockstep_client::types::LldpPortConfig as BaLldpPortConfig; @@ -709,7 +709,7 @@ fn build_port_config( use omicron_common::api::internal::shared::PortFec; use omicron_common::api::internal::shared::PortSpeed; - BaPortConfigV2 { + BaPortConfig { port: port.to_owned(), routes: config .routes From 6225e55be145dfb0748888fe5a4c3fc680e3bd3b Mon Sep 17 00:00:00 2001 From: Ryan Goodfellow Date: Thu, 22 Jan 2026 00:28:12 +0000 Subject: [PATCH 14/20] remove long dead compat types --- sled-agent/types/src/rack_init.rs | 157 +++--------------------------- 1 file changed, 13 insertions(+), 144 deletions(-) diff --git a/sled-agent/types/src/rack_init.rs b/sled-agent/types/src/rack_init.rs index d72449c93bb..c39e3247228 100644 --- a/sled-agent/types/src/rack_init.rs +++ b/sled-agent/types/src/rack_init.rs @@ -4,129 +4,9 @@ //! Rack initialization types. -use std::net::IpAddr; - -use anyhow::Result; use camino::Utf8Path; -use omicron_common::{ - address::IpRange, - api::{external::AllowedSourceIps, internal::nexus::Certificate}, -}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; +use omicron_common::api::internal::nexus::Certificate; pub use sled_agent_types_versions::latest::rack_init::*; -use sled_hardware_types::Baseboard; - -use crate::early_networking::back_compat::RackNetworkConfigV1; - -/// Structures and routines used to maintain backwards compatibility. The -/// contents of this module should only be used to convert older data into the -/// current format, and not for any ongoing run-time operations. -pub mod back_compat { - use omicron_common::api::internal::nexus::Certificate; - - use super::*; - - #[derive(Clone, Deserialize)] - struct UnvalidatedRackInitializeRequestV1 { - trust_quorum_peers: Option>, - bootstrap_discovery: BootstrapAddressDiscovery, - ntp_servers: Vec, - dns_servers: Vec, - internal_services_ip_pool_ranges: Vec, - external_dns_ips: Vec, - external_dns_zone_name: String, - external_certificates: Vec, - recovery_silo: RecoverySiloConfig, - rack_network_config: RackNetworkConfigV1, - #[serde(default = "default_allowed_source_ips")] - allowed_source_ips: AllowedSourceIps, - } - - fn validate_external_dns( - dns_ips: &Vec, - internal_ranges: &Vec, - ) -> Result<()> { - use anyhow::bail; - if dns_ips.is_empty() { - bail!("At least one external DNS IP is required"); - } - - for &dns_ip in dns_ips { - if !internal_ranges.iter().any(|range| range.contains(dns_ip)) { - bail!( - "External DNS IP {dns_ip} is not contained in \ - `internal_services_ip_pool_ranges`" - ); - } - } - Ok(()) - } - - /// This is a deprecated format, maintained to allow importing from older - /// versions. - #[derive(Clone, Debug, PartialEq, Deserialize, Serialize, JsonSchema)] - #[serde(try_from = "UnvalidatedRackInitializeRequestV1")] - pub struct RackInitializeRequestV1 { - pub trust_quorum_peers: Option>, - pub bootstrap_discovery: BootstrapAddressDiscovery, - pub ntp_servers: Vec, - pub dns_servers: Vec, - pub internal_services_ip_pool_ranges: Vec, - pub external_dns_ips: Vec, - pub external_dns_zone_name: String, - pub external_certificates: Vec, - pub recovery_silo: RecoverySiloConfig, - pub rack_network_config: RackNetworkConfigV1, - #[serde(default = "default_allowed_source_ips")] - pub allowed_source_ips: AllowedSourceIps, - } - - impl TryFrom for RackInitializeRequestV1 { - type Error = anyhow::Error; - - fn try_from(value: UnvalidatedRackInitializeRequestV1) -> Result { - validate_external_dns( - &value.external_dns_ips, - &value.internal_services_ip_pool_ranges, - )?; - - Ok(RackInitializeRequestV1 { - trust_quorum_peers: value.trust_quorum_peers, - bootstrap_discovery: value.bootstrap_discovery, - ntp_servers: value.ntp_servers, - dns_servers: value.dns_servers, - internal_services_ip_pool_ranges: value - .internal_services_ip_pool_ranges, - external_dns_ips: value.external_dns_ips, - external_dns_zone_name: value.external_dns_zone_name, - external_certificates: value.external_certificates, - recovery_silo: value.recovery_silo, - rack_network_config: value.rack_network_config, - allowed_source_ips: value.allowed_source_ips, - }) - } - } - - impl From for RackInitializeRequest { - fn from(v1: RackInitializeRequestV1) -> Self { - RackInitializeRequest { - trust_quorum_peers: v1.trust_quorum_peers, - bootstrap_discovery: v1.bootstrap_discovery, - ntp_servers: v1.ntp_servers, - dns_servers: v1.dns_servers, - internal_services_ip_pool_ranges: v1 - .internal_services_ip_pool_ranges, - external_dns_ips: v1.external_dns_ips, - external_dns_zone_name: v1.external_dns_zone_name, - external_certificates: v1.external_certificates, - recovery_silo: v1.recovery_silo, - rack_network_config: v1.rack_network_config.into(), - allowed_source_ips: v1.allowed_source_ips, - } - } - } -} /// Load a RackInitializeRequest from a file path. pub fn rack_initialize_request_from_file>( @@ -136,13 +16,11 @@ pub fn rack_initialize_request_from_file>( let contents = std::fs::read_to_string(&path).map_err(|err| { RackInitializeRequestParseError::Io { path: path.into(), err } })?; - let mut raw_config = rack_initialize_request_from_toml_with_fallback( - &contents, - ) - .map_err(|err| RackInitializeRequestParseError::Deserialize { - path: path.into(), - err, - })?; + let mut raw_config = rack_initialize_request_from_toml(&contents) + .map_err(|err| RackInitializeRequestParseError::Deserialize { + path: path.into(), + err: err.into(), + })?; // In the same way that sled-agent itself (our caller) discovers the // optional config-rss.toml in a well-known path relative to its config @@ -192,22 +70,11 @@ pub fn rack_initialize_request_from_file>( Ok(raw_config) } -/// Parse a RackInitializeRequest from TOML, with fallback to older versions. -pub fn rack_initialize_request_from_toml_with_fallback( +/// Parse a RackInitializeRequest from TOML. +pub fn rack_initialize_request_from_toml( data: &str, -) -> Result { - // Note that if we fail to parse the request as any known - // version, we return the error corresponding to the parse - // failure for the newest schema. - toml::from_str::(&data).or_else( - |latest_version_err| match toml::from_str::< - back_compat::RackInitializeRequestV1, - >(&data) - { - Ok(v1) => Ok(v1.into()), - Err(_v1_err) => Err(latest_version_err.into()), - }, - ) +) -> Result { + toml::from_str::(data) } /// Return a RackInitializeRequest configuration suitable for testing. @@ -239,11 +106,13 @@ impl RackInitializeRequestParams { #[cfg(test)] mod tests { + use std::net::IpAddr; use std::net::Ipv4Addr; use std::net::Ipv6Addr; use camino::Utf8PathBuf; - use omicron_common::address::{AZ_PREFIX, RACK_PREFIX, SLED_PREFIX}; + use omicron_common::address::{AZ_PREFIX, IpRange, RACK_PREFIX, SLED_PREFIX}; + use omicron_common::api::external::AllowedSourceIps; use omicron_common::api::internal::shared::RackNetworkConfig; use super::*; From 62b248ce13822d043469b727803b3e11729d2592 Mon Sep 17 00:00:00 2001 From: Ryan Goodfellow Date: Thu, 22 Jan 2026 01:43:39 +0000 Subject: [PATCH 15/20] the final type shuffle? --- Cargo.lock | 1 + .../src/api/internal/shared/rack_init/mod.rs | 39 +- .../src/api/internal/shared/rack_init/v2.rs | 522 ++++++++++++++++++ sled-agent/bootstrap-agent-api/Cargo.toml | 1 + sled-agent/bootstrap-agent-api/src/lib.rs | 5 +- sled-agent/src/bootstrap/http_entrypoints.rs | 8 +- sled-agent/types/src/rack_init.rs | 30 +- .../types/versions/src/initial/rack_init.rs | 201 +++---- sled-agent/types/versions/src/latest.rs | 8 +- sled-agent/types/versions/src/lib.rs | 2 + .../versions/src/lockstep_rack_init/mod.rs | 8 + .../src/lockstep_rack_init/rack_init.rs | 243 ++++++++ 12 files changed, 923 insertions(+), 145 deletions(-) create mode 100644 common/src/api/internal/shared/rack_init/v2.rs create mode 100644 sled-agent/types/versions/src/lockstep_rack_init/mod.rs create mode 100644 sled-agent/types/versions/src/lockstep_rack_init/rack_init.rs diff --git a/Cargo.lock b/Cargo.lock index 86d306fb954..851def23e9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1004,6 +1004,7 @@ dependencies = [ "schemars 0.8.22", "serde", "sled-agent-types", + "sled-agent-types-versions", "sled-hardware-types", "tufaceous-artifact", ] diff --git a/common/src/api/internal/shared/rack_init/mod.rs b/common/src/api/internal/shared/rack_init/mod.rs index 2c3196348e4..8c07842e439 100644 --- a/common/src/api/internal/shared/rack_init/mod.rs +++ b/common/src/api/internal/shared/rack_init/mod.rs @@ -5,25 +5,26 @@ //! Rack init types. pub mod v1; +pub mod v2; // Re-export latest version types for convenience. // Note: New versions of these types will be added to the top-level module. -pub use v1::BfdPeerConfig; -pub use v1::BgpConfig; -pub use v1::BgpPeerConfig; -pub use v1::ExternalPortDiscovery; -pub use v1::HostPortConfig; -pub use v1::LldpAdminStatus; -pub use v1::LldpPortConfig; -pub use v1::ParseLldpAdminStatusError; -pub use v1::ParseSwitchLocationError; -pub use v1::PortConfig; -pub use v1::PortFec; -pub use v1::PortSpeed; -pub use v1::RackNetworkConfig; -pub use v1::RouteConfig; -pub use v1::SwitchLocation; -pub use v1::SwitchPorts; -pub use v1::TxEqConfig; -pub use v1::UplinkAddressConfig; -pub use v1::UplinkAddressConfigError; +pub use v2::BfdPeerConfig; +pub use v2::BgpConfig; +pub use v2::BgpPeerConfig; +pub use v2::ExternalPortDiscovery; +pub use v2::HostPortConfig; +pub use v2::LldpAdminStatus; +pub use v2::LldpPortConfig; +pub use v2::ParseLldpAdminStatusError; +pub use v2::ParseSwitchLocationError; +pub use v2::PortConfig; +pub use v2::PortFec; +pub use v2::PortSpeed; +pub use v2::RackNetworkConfig; +pub use v2::RouteConfig; +pub use v2::SwitchLocation; +pub use v2::SwitchPorts; +pub use v2::TxEqConfig; +pub use v2::UplinkAddressConfig; +pub use v2::UplinkAddressConfigError; diff --git a/common/src/api/internal/shared/rack_init/v2.rs b/common/src/api/internal/shared/rack_init/v2.rs new file mode 100644 index 00000000000..a7c99fc7267 --- /dev/null +++ b/common/src/api/internal/shared/rack_init/v2.rs @@ -0,0 +1,522 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Version 2 of rack init types. The rack initialization API is now +//! lockstep, so this should be the last version of these types and +//! they now may be modified freely. + +use crate::api::external::{BfdMode, ImportExportPolicy, Name}; +use oxnet::{IpNet, Ipv4Net, Ipv6Net}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + fmt, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + str::FromStr, +}; + +/// Initial network configuration +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] +pub struct RackNetworkConfig { + pub rack_subnet: Ipv6Net, + // TODO: #3591 Consider making infra-ip ranges implicit for uplinks + /// First ip address to be used for configuring network infrastructure + pub infra_ip_first: Ipv4Addr, + /// Last ip address to be used for configuring network infrastructure + pub infra_ip_last: Ipv4Addr, + /// Uplinks for connecting the rack to external networks + pub ports: Vec, + /// BGP configurations for connecting the rack to external networks + pub bgp: Vec, + /// BFD configuration for connecting the rack to external networks + #[serde(default)] + pub bfd: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] +pub struct BgpConfig { + /// The autonomous system number for the BGP configuration. + pub asn: u32, + /// The set of prefixes for the BGP router to originate. + pub originate: Vec, + + /// Shaper to apply to outgoing messages. + #[serde(default)] + pub shaper: Option, + + /// Checker to apply to incoming messages. + #[serde(default)] + pub checker: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] +pub struct BgpPeerConfig { + /// The autonomous system number of the router the peer belongs to. + pub asn: u32, + /// Switch port the peer is reachable on. + pub port: String, + /// Address of the peer. Use `Ipv4Addr::UNSPECIFIED` to indicate an + /// unnumbered BGP session established over the interface specified by + /// `port`. + pub addr: Ipv4Addr, + /// How long to keep a session alive without a keepalive in seconds. + /// Defaults to 6. + pub hold_time: Option, + /// How long to keep a peer in idle after a state machine reset in seconds. + pub idle_hold_time: Option, + /// How long to delay sending open messages to a peer. In seconds. + pub delay_open: Option, + /// The interval in seconds between peer connection retry attempts. + pub connect_retry: Option, + /// The interval to send keepalive messages at. + pub keepalive: Option, + /// Require that a peer has a specified ASN. + #[serde(default)] + pub remote_asn: Option, + /// Require messages from a peer have a minimum IP time to live field. + #[serde(default)] + pub min_ttl: Option, + /// Use the given key for TCP-MD5 authentication with the peer. + #[serde(default)] + pub md5_auth_key: Option, + /// Apply the provided multi-exit discriminator (MED) updates sent to the peer. + #[serde(default)] + pub multi_exit_discriminator: Option, + /// Include the provided communities in updates sent to the peer. + #[serde(default)] + pub communities: Vec, + /// Apply a local preference to routes received from this peer. + #[serde(default)] + pub local_pref: Option, + /// Enforce that the first AS in paths received from this peer is the peer's AS. + #[serde(default)] + pub enforce_first_as: bool, + /// Define import policy for a peer. + #[serde(default)] + pub allowed_import: ImportExportPolicy, + /// Define export policy for a peer. + #[serde(default)] + pub allowed_export: ImportExportPolicy, + /// Associate a VLAN ID with a BGP peer session. + #[serde(default)] + pub vlan_id: Option, +} + +impl BgpPeerConfig { + /// The default hold time for a BGP peer in seconds. + pub const DEFAULT_HOLD_TIME: u64 = 6; + + /// The default idle hold time for a BGP peer in seconds. + pub const DEFAULT_IDLE_HOLD_TIME: u64 = 3; + + /// The default delay open time for a BGP peer in seconds. + pub const DEFAULT_DELAY_OPEN: u64 = 0; + + /// The default connect retry time for a BGP peer in seconds. + pub const DEFAULT_CONNECT_RETRY: u64 = 3; + + /// The default keepalive time for a BGP peer in seconds. + pub const DEFAULT_KEEPALIVE: u64 = 2; + + pub fn hold_time(&self) -> u64 { + self.hold_time.unwrap_or(Self::DEFAULT_HOLD_TIME) + } + + pub fn idle_hold_time(&self) -> u64 { + self.idle_hold_time.unwrap_or(Self::DEFAULT_IDLE_HOLD_TIME) + } + + pub fn delay_open(&self) -> u64 { + self.delay_open.unwrap_or(Self::DEFAULT_DELAY_OPEN) + } + + pub fn connect_retry(&self) -> u64 { + self.connect_retry.unwrap_or(Self::DEFAULT_CONNECT_RETRY) + } + + pub fn keepalive(&self) -> u64 { + self.keepalive.unwrap_or(Self::DEFAULT_KEEPALIVE) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] +pub struct BfdPeerConfig { + pub local: Option, + pub remote: IpAddr, + pub detection_threshold: u8, + pub required_rx: u64, + pub mode: BfdMode, + pub switch: SwitchLocation, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] +pub struct RouteConfig { + /// The destination of the route. + pub destination: IpNet, + /// The nexthop/gateway address. + pub nexthop: IpAddr, + /// The VLAN id associated with this route. + #[serde(default)] + pub vlan_id: Option, + /// The RIB priority (i.e. Admin Distance) associated with this route. + #[serde(default)] + pub rib_priority: Option, +} + +#[derive( + Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Hash, +)] +pub struct UplinkAddressConfig { + pub address: IpNet, + /// The VLAN id (if any) associated with this address. + #[serde(default)] + pub vlan_id: Option, +} + +impl UplinkAddressConfig { + pub fn addr(&self) -> IpAddr { + self.address.addr() + } +} + +impl std::fmt::Display for UplinkAddressConfig { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.vlan_id { + None => write!(f, "{}", self.address), + Some(v) => write!(f, "{};{}", self.address, v), + } + } +} + +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct UplinkAddressConfigError(String); + +impl std::fmt::Display for UplinkAddressConfigError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "parse switch location error: {}", self.0) + } +} + +/// Convert a string into an UplinkAddressConfig. +/// 192.168.1.1/24 => UplinkAddressConfig { 192.168.1.1/24, None } +/// 192.168.1.1/24;200 => UplinkAddressConfig { 192.168.1.1/24, Some(200) } +impl FromStr for UplinkAddressConfig { + type Err = UplinkAddressConfigError; + + fn from_str(s: &str) -> Result { + let fields: Vec<&str> = s.split(';').collect(); + let (address, vlan_id) = match fields.len() { + 1 => Ok((fields[0], None)), + 2 => Ok((fields[0], Some(fields[1]))), + _ => Err(UplinkAddressConfigError(format!( + "not a valid uplink address: {s}" + ))), + }?; + let address = address.parse().map_err(|_| { + UplinkAddressConfigError(format!( + "not a valid ip address: {address}" + )) + })?; + let vlan_id = match vlan_id { + None => Ok(None), + Some(v) => match v.parse() { + Err(_) => Err(format!("invalid vlan id: {v}")), + Ok(vlan_id) if vlan_id > 1 && vlan_id < 4096 => { + Ok(Some(vlan_id)) + } + Ok(vlan_id) => Err(format!("vlan id out of range: {vlan_id}")), + }, + } + .map_err(|e| UplinkAddressConfigError(e))?; + Ok(UplinkAddressConfig { address, vlan_id }) + } +} + +#[derive( + Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, +)] +#[serde(rename_all = "snake_case")] +/// To what extent should this port participate in LLDP +pub enum LldpAdminStatus { + #[default] + Enabled, + Disabled, + RxOnly, + TxOnly, +} + +impl fmt::Display for LldpAdminStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LldpAdminStatus::Enabled => write!(f, "enabled"), + LldpAdminStatus::Disabled => write!(f, "disabled"), + LldpAdminStatus::RxOnly => write!(f, "rx_only"), + LldpAdminStatus::TxOnly => write!(f, "tx_only"), + } + } +} + +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct ParseLldpAdminStatusError(String); + +impl std::fmt::Display for ParseLldpAdminStatusError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "LLDP admin status error: {}", self.0) + } +} + +impl FromStr for LldpAdminStatus { + type Err = ParseLldpAdminStatusError; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "enabled" => Ok(Self::Enabled), + "disabled" => Ok(Self::Disabled), + "rxonly" | "rx_only" => Ok(Self::RxOnly), + "txonly" | "tx_only" => Ok(Self::TxOnly), + _ => Err(ParseLldpAdminStatusError(format!( + "not a valid admin status: {s}" + ))), + } + } +} + +/// Per-port LLDP configuration settings. Only the "status" setting is +/// mandatory. All other fields have natural defaults or may be inherited from +/// the switch. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] +pub struct LldpPortConfig { + /// To what extent should this port participate in LLDP + pub status: LldpAdminStatus, + /// Chassis ID to advertise. If this is set, it will be advertised as a + /// LocallyAssigned ID type. If this is not set, it will be + /// inherited from the switch-level settings. + pub chassis_id: Option, + /// Port ID to advertise. If this is set, it will be advertised as a + /// LocallyAssigned ID type. If this is not set, it will be set to + /// the port name. e.g., qsfp0/0. + pub port_id: Option, + /// Port description to advertise. If this is not set, no + /// description will be advertised. + pub port_description: Option, + /// System name to advertise. If this is not set, it will be + /// inherited from the switch-level settings. + pub system_name: Option, + /// System description to advertise. If this is not set, it will be + /// inherited from the switch-level settings. + pub system_description: Option, + /// Management IP addresses to advertise. If this is not set, it will be + /// inherited from the switch-level settings. + pub management_addrs: Option>, +} + +/// Per-port tx-eq overrides. This can be used to fine-tune the transceiver +/// equalization settings to improve signal integrity. +#[derive( + Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema, +)] +pub struct TxEqConfig { + /// Pre-cursor tap1 + pub pre1: Option, + /// Pre-cursor tap2 + pub pre2: Option, + /// Main tap + pub main: Option, + /// Post-cursor tap2 + pub post2: Option, + /// Post-cursor tap1 + pub post1: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] +pub struct PortConfig { + /// The set of routes associated with this port. + pub routes: Vec, + /// This port's addresses and optional vlan IDs + pub addresses: Vec, + /// Switch the port belongs to. + pub switch: SwitchLocation, + /// Nmae of the port this config applies to. + pub port: String, + /// Port speed. + pub uplink_port_speed: PortSpeed, + /// Port forward error correction type. + pub uplink_port_fec: Option, + /// BGP peers on this port + pub bgp_peers: Vec, + /// Whether or not to set autonegotiation + #[serde(default)] + pub autoneg: bool, + /// LLDP configuration for this port + pub lldp: Option, + /// TX-EQ configuration for this port + pub tx_eq: Option, +} + +/// A set of switch uplinks. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct SwitchPorts { + pub uplinks: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] +pub struct HostPortConfig { + /// Switchport to use for external connectivity + pub port: String, + + /// IP Address and prefix (e.g., `192.168.0.1/16`) to apply to switchport + /// (must be in infra_ip pool). May also include an optional VLAN ID. + pub addrs: Vec, + + pub lldp: Option, + pub tx_eq: Option, +} + +impl From for HostPortConfig { + fn from(x: PortConfig) -> Self { + Self { + port: x.port, + addrs: x.addresses, + lldp: x.lldp.clone(), + tx_eq: x.tx_eq, + } + } +} + +/// Identifies switch physical location +#[derive( + Clone, + Copy, + Debug, + Deserialize, + Serialize, + PartialEq, + JsonSchema, + Hash, + Eq, + PartialOrd, + Ord, +)] +#[serde(rename_all = "snake_case")] +pub enum SwitchLocation { + /// Switch in upper slot + Switch0, + /// Switch in lower slot + Switch1, +} + +impl SwitchLocation { + /// Return the location of the other switch, not ourself. + pub const fn other(&self) -> Self { + match self { + SwitchLocation::Switch0 => SwitchLocation::Switch1, + SwitchLocation::Switch1 => SwitchLocation::Switch0, + } + } +} + +impl fmt::Display for SwitchLocation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SwitchLocation::Switch0 => write!(f, "switch0"), + SwitchLocation::Switch1 => write!(f, "switch1"), + } + } +} + +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct ParseSwitchLocationError(String); + +impl std::fmt::Display for ParseSwitchLocationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "parse switch location error: {}", self.0) + } +} + +impl FromStr for SwitchLocation { + type Err = ParseSwitchLocationError; + + fn from_str(s: &str) -> Result { + match s { + "switch0" => Ok(Self::Switch0), + "switch1" => Ok(Self::Switch1), + _ => Err(ParseSwitchLocationError(format!( + "not a valid location: {s}" + ))), + } + } +} + +#[derive(Debug, Clone, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ExternalPortDiscovery { + // Automatically discover ports via Dendrite + Auto(HashMap), + // Static configuration pairing switches with a collection of ports + Static(HashMap>), +} + +/// Switchport Speed options +#[derive( + Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Hash, +)] +#[serde(rename_all = "snake_case")] +pub enum PortSpeed { + #[serde(alias = "0G")] + Speed0G, + #[serde(alias = "1G")] + Speed1G, + #[serde(alias = "10G")] + Speed10G, + #[serde(alias = "25G")] + Speed25G, + #[serde(alias = "40G")] + Speed40G, + #[serde(alias = "50G")] + Speed50G, + #[serde(alias = "100G")] + Speed100G, + #[serde(alias = "200G")] + Speed200G, + #[serde(alias = "400G")] + Speed400G, +} + +impl fmt::Display for PortSpeed { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PortSpeed::Speed0G => write!(f, "0G"), + PortSpeed::Speed1G => write!(f, "1G"), + PortSpeed::Speed10G => write!(f, "10G"), + PortSpeed::Speed25G => write!(f, "25G"), + PortSpeed::Speed40G => write!(f, "40G"), + PortSpeed::Speed50G => write!(f, "50G"), + PortSpeed::Speed100G => write!(f, "100G"), + PortSpeed::Speed200G => write!(f, "200G"), + PortSpeed::Speed400G => write!(f, "400G"), + } + } +} + +/// Switchport FEC options +#[derive( + Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Hash, +)] +#[serde(rename_all = "snake_case")] +pub enum PortFec { + Firecode, + None, + Rs, +} + +impl fmt::Display for PortFec { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PortFec::Firecode => write!(f, "Firecode R-FEC"), + PortFec::None => write!(f, "None"), + PortFec::Rs => write!(f, "RS-FEC"), + } + } +} diff --git a/sled-agent/bootstrap-agent-api/Cargo.toml b/sled-agent/bootstrap-agent-api/Cargo.toml index fca38ae7f4a..8834952fa26 100644 --- a/sled-agent/bootstrap-agent-api/Cargo.toml +++ b/sled-agent/bootstrap-agent-api/Cargo.toml @@ -16,5 +16,6 @@ omicron-workspace-hack.workspace = true schemars.workspace = true serde.workspace = true sled-agent-types.workspace = true +sled-agent-types-versions.workspace = true sled-hardware-types.workspace = true tufaceous-artifact.workspace = true diff --git a/sled-agent/bootstrap-agent-api/src/lib.rs b/sled-agent/bootstrap-agent-api/src/lib.rs index de966027041..327bd7774e7 100644 --- a/sled-agent/bootstrap-agent-api/src/lib.rs +++ b/sled-agent/bootstrap-agent-api/src/lib.rs @@ -15,9 +15,8 @@ use dropshot_api_manager_types::api_versions; use omicron_uuid_kinds::{RackInitUuid, RackResetUuid}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use sled_agent_types::{ - rack_init::RackInitializeRequest, rack_ops::RackOperationStatus, -}; +use sled_agent_types::rack_ops::RackOperationStatus; +use sled_agent_types_versions::v1::rack_init::RackInitializeRequest; use sled_hardware_types::Baseboard; use tufaceous_artifact::ArtifactVersion; diff --git a/sled-agent/src/bootstrap/http_entrypoints.rs b/sled-agent/src/bootstrap/http_entrypoints.rs index fe20bffdd68..58552eb6455 100644 --- a/sled-agent/src/bootstrap/http_entrypoints.rs +++ b/sled-agent/src/bootstrap/http_entrypoints.rs @@ -25,9 +25,7 @@ use omicron_common::api::external::Error; use omicron_uuid_kinds::RackInitUuid; use omicron_uuid_kinds::RackResetUuid; use sled_agent_config_reconciler::InternalDisksReceiver; -use sled_agent_types::rack_init::{ - RackInitializeRequest, RackInitializeRequestParams, -}; +use sled_agent_types::rack_init::RackInitializeRequestParams; use sled_agent_types::rack_ops::RackOperationStatus; use sled_hardware_types::Baseboard; use slog::Logger; @@ -115,7 +113,9 @@ impl BootstrapAgentApi for BootstrapAgentImpl { async fn rack_initialize( _rqctx: RequestContext, - _body: TypedBody, + _body: TypedBody< + sled_agent_types_versions::v1::rack_init::RackInitializeRequest, + >, ) -> Result, HttpError> { // This endpoint has moved to the lockstep API on port 8080. Err(HttpError::for_client_error( diff --git a/sled-agent/types/src/rack_init.rs b/sled-agent/types/src/rack_init.rs index c39e3247228..f394b4ac74d 100644 --- a/sled-agent/types/src/rack_init.rs +++ b/sled-agent/types/src/rack_init.rs @@ -6,6 +6,7 @@ use camino::Utf8Path; use omicron_common::api::internal::nexus::Certificate; + pub use sled_agent_types_versions::latest::rack_init::*; /// Load a RackInitializeRequest from a file path. @@ -16,10 +17,12 @@ pub fn rack_initialize_request_from_file>( let contents = std::fs::read_to_string(&path).map_err(|err| { RackInitializeRequestParseError::Io { path: path.into(), err } })?; - let mut raw_config = rack_initialize_request_from_toml(&contents) - .map_err(|err| RackInitializeRequestParseError::Deserialize { - path: path.into(), - err: err.into(), + let mut raw_config = + rack_initialize_request_from_toml(&contents).map_err(|err| { + RackInitializeRequestParseError::Deserialize { + path: path.into(), + err: err.into(), + } })?; // In the same way that sled-agent itself (our caller) discovers the @@ -89,21 +92,6 @@ pub fn rack_initialize_request_test_config() -> RackInitializeRequest { .unwrap_or_else(|e| panic!("failed to parse {:?}: {}", &path, e)) } -#[derive(Debug, Clone)] -pub struct RackInitializeRequestParams { - pub rack_initialize_request: RackInitializeRequest, - pub skip_timesync: bool, -} - -impl RackInitializeRequestParams { - pub fn new( - rack_initialize_request: RackInitializeRequest, - skip_timesync: bool, - ) -> RackInitializeRequestParams { - RackInitializeRequestParams { rack_initialize_request, skip_timesync } - } -} - #[cfg(test)] mod tests { use std::net::IpAddr; @@ -111,7 +99,9 @@ mod tests { use std::net::Ipv6Addr; use camino::Utf8PathBuf; - use omicron_common::address::{AZ_PREFIX, IpRange, RACK_PREFIX, SLED_PREFIX}; + use omicron_common::address::{ + AZ_PREFIX, IpRange, RACK_PREFIX, SLED_PREFIX, + }; use omicron_common::api::external::AllowedSourceIps; use omicron_common::api::internal::shared::RackNetworkConfig; diff --git a/sled-agent/types/versions/src/initial/rack_init.rs b/sled-agent/types/versions/src/initial/rack_init.rs index 45ebd6a7925..08433f07364 100644 --- a/sled-agent/types/versions/src/initial/rack_init.rs +++ b/sled-agent/types/versions/src/initial/rack_init.rs @@ -4,8 +4,7 @@ //! Rack initialization types -use std::{collections::BTreeSet, net::IpAddr, net::Ipv6Addr}; - +use crate::bootstrap_v1::rack_init::RecoverySiloConfig; use anyhow::{Result, bail}; use camino::Utf8PathBuf; use omicron_common::{ @@ -20,74 +19,7 @@ use omicron_common::{ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use sled_hardware_types::Baseboard; - -use crate::bootstrap_v1::rack_init::RecoverySiloConfig; - -// "Shadow" copy of `RackInitializeRequest` that does no validation on its -// fields. -#[derive(Clone, Deserialize)] -struct UnvalidatedRackInitializeRequest { - trust_quorum_peers: Option>, - bootstrap_discovery: BootstrapAddressDiscovery, - ntp_servers: Vec, - dns_servers: Vec, - internal_services_ip_pool_ranges: Vec, - external_dns_ips: Vec, - external_dns_zone_name: String, - external_certificates: Vec, - recovery_silo: RecoverySiloConfig, - rack_network_config: RackNetworkConfig, - #[serde(default = "default_allowed_source_ips")] - allowed_source_ips: AllowedSourceIps, -} - -fn validate_external_dns( - dns_ips: &Vec, - internal_ranges: &Vec, -) -> Result<()> { - if dns_ips.is_empty() { - bail!("At least one external DNS IP is required"); - } - - // Every external DNS IP should also be present in one of the internal - // services IP pool ranges. This check is O(N*M), but we expect both N - // and M to be small (~5 DNS servers, and a small number of pools). - for &dns_ip in dns_ips { - if !internal_ranges.iter().any(|range| range.contains(dns_ip)) { - bail!( - "External DNS IP {dns_ip} is not contained in \ - `internal_services_ip_pool_ranges`" - ); - } - } - Ok(()) -} - -impl TryFrom for RackInitializeRequest { - type Error = anyhow::Error; - - fn try_from(value: UnvalidatedRackInitializeRequest) -> Result { - validate_external_dns( - &value.external_dns_ips, - &value.internal_services_ip_pool_ranges, - )?; - - Ok(RackInitializeRequest { - trust_quorum_peers: value.trust_quorum_peers, - bootstrap_discovery: value.bootstrap_discovery, - ntp_servers: value.ntp_servers, - dns_servers: value.dns_servers, - internal_services_ip_pool_ranges: value - .internal_services_ip_pool_ranges, - external_dns_ips: value.external_dns_ips, - external_dns_zone_name: value.external_dns_zone_name, - external_certificates: value.external_certificates, - recovery_silo: value.recovery_silo, - rack_network_config: value.rack_network_config, - allowed_source_ips: value.allowed_source_ips, - }) - } -} +use std::{collections::BTreeSet, net::IpAddr, net::Ipv6Addr}; /// Configuration for the "rack setup service". /// @@ -159,31 +91,6 @@ impl RackInitializeRequest { } } -#[derive(Debug, thiserror::Error)] -pub enum RackInitializeRequestParseError { - #[error("Failed to read config from {path}: {err}")] - Io { - path: Utf8PathBuf, - #[source] - err: std::io::Error, - }, - #[error("Failed to deserialize config from {path}: {err}")] - Deserialize { - path: Utf8PathBuf, - #[source] - err: anyhow::Error, - }, - #[error("Loading certificate: {0}")] - Certificate(#[source] anyhow::Error), -} - -/// This field was added after several racks were already deployed. RSS plans -/// for those racks should default to allowing any source IP, since that is -/// effectively what they did. -pub const fn default_allowed_source_ips() -> AllowedSourceIps { - AllowedSourceIps::Any -} - // This custom debug implementation hides the private keys. impl std::fmt::Debug for RackInitializeRequest { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -222,6 +129,24 @@ impl std::fmt::Debug for RackInitializeRequest { } } +#[derive(Debug, thiserror::Error)] +pub enum RackInitializeRequestParseError { + #[error("Failed to read config from {path}: {err}")] + Io { + path: Utf8PathBuf, + #[source] + err: std::io::Error, + }, + #[error("Failed to deserialize config from {path}: {err}")] + Deserialize { + path: Utf8PathBuf, + #[source] + err: anyhow::Error, + }, + #[error("Loading certificate: {0}")] + Certificate(#[source] anyhow::Error), +} + #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case", tag = "type")] pub enum BootstrapAddressDiscovery { @@ -230,3 +155,89 @@ pub enum BootstrapAddressDiscovery { /// Ignore all bootstrap addresses except the following. OnlyThese { addrs: BTreeSet }, } +// "Shadow" copy of `RackInitializeRequest` that does no validation on its +// fields. +#[derive(Clone, Deserialize)] +struct UnvalidatedRackInitializeRequest { + trust_quorum_peers: Option>, + bootstrap_discovery: BootstrapAddressDiscovery, + ntp_servers: Vec, + dns_servers: Vec, + internal_services_ip_pool_ranges: Vec, + external_dns_ips: Vec, + external_dns_zone_name: String, + external_certificates: Vec, + recovery_silo: RecoverySiloConfig, + rack_network_config: RackNetworkConfig, + #[serde(default = "default_allowed_source_ips")] + allowed_source_ips: AllowedSourceIps, +} + +#[derive(Debug, Clone)] +pub struct RackInitializeRequestParams { + pub rack_initialize_request: RackInitializeRequest, + pub skip_timesync: bool, +} + +impl RackInitializeRequestParams { + pub fn new( + rack_initialize_request: RackInitializeRequest, + skip_timesync: bool, + ) -> RackInitializeRequestParams { + RackInitializeRequestParams { rack_initialize_request, skip_timesync } + } +} +impl TryFrom for RackInitializeRequest { + type Error = anyhow::Error; + + fn try_from(value: UnvalidatedRackInitializeRequest) -> Result { + validate_external_dns( + &value.external_dns_ips, + &value.internal_services_ip_pool_ranges, + )?; + + Ok(RackInitializeRequest { + trust_quorum_peers: value.trust_quorum_peers, + bootstrap_discovery: value.bootstrap_discovery, + ntp_servers: value.ntp_servers, + dns_servers: value.dns_servers, + internal_services_ip_pool_ranges: value + .internal_services_ip_pool_ranges, + external_dns_ips: value.external_dns_ips, + external_dns_zone_name: value.external_dns_zone_name, + external_certificates: value.external_certificates, + recovery_silo: value.recovery_silo, + rack_network_config: value.rack_network_config, + allowed_source_ips: value.allowed_source_ips, + }) + } +} + +/// This field was added after several racks were already deployed. RSS plans +/// for those racks should default to allowing any source IP, since that is +/// effectively what they did. +pub const fn default_allowed_source_ips() -> AllowedSourceIps { + AllowedSourceIps::Any +} + +fn validate_external_dns( + dns_ips: &Vec, + internal_ranges: &Vec, +) -> Result<()> { + if dns_ips.is_empty() { + bail!("At least one external DNS IP is required"); + } + + // Every external DNS IP should also be present in one of the internal + // services IP pool ranges. This check is O(N*M), but we expect both N + // and M to be small (~5 DNS servers, and a small number of pools). + for &dns_ip in dns_ips { + if !internal_ranges.iter().any(|range| range.contains(dns_ip)) { + bail!( + "External DNS IP {dns_ip} is not contained in \ + `internal_services_ip_pool_ranges`" + ); + } + } + Ok(()) +} diff --git a/sled-agent/types/versions/src/latest.rs b/sled-agent/types/versions/src/latest.rs index 42c280d744b..6a96cbf18e5 100644 --- a/sled-agent/types/versions/src/latest.rs +++ b/sled-agent/types/versions/src/latest.rs @@ -134,10 +134,10 @@ pub mod probes { pub mod rack_init { pub use crate::bootstrap_v1::rack_init::RecoverySiloConfig; - pub use crate::v1::rack_init::BootstrapAddressDiscovery; - pub use crate::v1::rack_init::RackInitializeRequest; - pub use crate::v1::rack_init::RackInitializeRequestParseError; - pub use crate::v1::rack_init::default_allowed_source_ips; + pub use crate::v14::rack_init::BootstrapAddressDiscovery; + pub use crate::v14::rack_init::RackInitializeRequest; + pub use crate::v14::rack_init::RackInitializeRequestParams; + pub use crate::v14::rack_init::RackInitializeRequestParseError; } pub mod sled { diff --git a/sled-agent/types/versions/src/lib.rs b/sled-agent/types/versions/src/lib.rs index 9e534c776a4..ac9183d2998 100644 --- a/sled-agent/types/versions/src/lib.rs +++ b/sled-agent/types/versions/src/lib.rs @@ -43,6 +43,8 @@ pub mod v11; pub mod v12; #[path = "add_trust_quorum/mod.rs"] pub mod v13; +#[path = "lockstep_rack_init/mod.rs"] +pub mod v14; #[path = "add_switch_zone_operator_policy/mod.rs"] pub mod v3; #[path = "add_nexus_lockstep_port_to_inventory/mod.rs"] diff --git a/sled-agent/types/versions/src/lockstep_rack_init/mod.rs b/sled-agent/types/versions/src/lockstep_rack_init/mod.rs new file mode 100644 index 00000000000..62b981640fb --- /dev/null +++ b/sled-agent/types/versions/src/lockstep_rack_init/mod.rs @@ -0,0 +1,8 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Locksetp rack init types. This should be the last version of the rack +//! init types since they now are part of a lockstep API. + +pub mod rack_init; diff --git a/sled-agent/types/versions/src/lockstep_rack_init/rack_init.rs b/sled-agent/types/versions/src/lockstep_rack_init/rack_init.rs new file mode 100644 index 00000000000..08433f07364 --- /dev/null +++ b/sled-agent/types/versions/src/lockstep_rack_init/rack_init.rs @@ -0,0 +1,243 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Rack initialization types + +use crate::bootstrap_v1::rack_init::RecoverySiloConfig; +use anyhow::{Result, bail}; +use camino::Utf8PathBuf; +use omicron_common::{ + address::{ + AZ_PREFIX, IpRange, Ipv6Subnet, RACK_PREFIX, SLED_PREFIX, get_64_subnet, + }, + api::{ + external::AllowedSourceIps, + internal::{nexus::Certificate, shared::RackNetworkConfig}, + }, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use sled_hardware_types::Baseboard; +use std::{collections::BTreeSet, net::IpAddr, net::Ipv6Addr}; + +/// Configuration for the "rack setup service". +/// +/// The Rack Setup Service should be responsible for one-time setup actions, +/// such as CockroachDB placement and initialization. Without operator +/// intervention, however, these actions need a way to be automated in our +/// deployment. +#[derive(Clone, Deserialize, Serialize, PartialEq, JsonSchema)] +#[serde(try_from = "UnvalidatedRackInitializeRequest")] +pub struct RackInitializeRequest { + /// The set of peer_ids required to initialize trust quorum + /// + /// The value is `None` if we are not using trust quorum + pub trust_quorum_peers: Option>, + + /// Describes how bootstrap addresses should be collected during RSS. + pub bootstrap_discovery: BootstrapAddressDiscovery, + + /// The external NTP server addresses. + pub ntp_servers: Vec, + + /// The external DNS server addresses. + pub dns_servers: Vec, + + /// Ranges of the service IP pool which may be used for internal services. + // TODO(https://github.com/oxidecomputer/omicron/issues/1530): Eventually, + // we want to configure multiple pools. + pub internal_services_ip_pool_ranges: Vec, + + /// Service IP addresses on which we run external DNS servers. + /// + /// Each address must be present in `internal_services_ip_pool_ranges`. + pub external_dns_ips: Vec, + + /// DNS name for the DNS zone delegated to the rack for external DNS + pub external_dns_zone_name: String, + + /// initial TLS certificates for the external API + pub external_certificates: Vec, + + /// Configuration of the Recovery Silo (the initial Silo) + pub recovery_silo: RecoverySiloConfig, + + /// Initial rack network configuration + pub rack_network_config: RackNetworkConfig, + + /// IPs or subnets allowed to make requests to user-facing services + #[serde(default = "default_allowed_source_ips")] + pub allowed_source_ips: AllowedSourceIps, +} + +impl RackInitializeRequest { + pub fn az_subnet(&self) -> Ipv6Subnet { + Ipv6Subnet::::new( + self.rack_network_config.rack_subnet.addr(), + ) + } + + /// Returns the subnet for our rack. + pub fn rack_subnet(&self) -> Ipv6Subnet { + Ipv6Subnet::::new( + self.rack_network_config.rack_subnet.addr(), + ) + } + + /// Returns the subnet for the `index`-th sled in the rack. + pub fn sled_subnet(&self, index: u8) -> Ipv6Subnet { + get_64_subnet(self.rack_subnet(), index) + } +} + +// This custom debug implementation hides the private keys. +impl std::fmt::Debug for RackInitializeRequest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // If you find a compiler error here, and you just added a field to this + // struct, be sure to add it to the Debug impl below! + let RackInitializeRequest { + trust_quorum_peers, + bootstrap_discovery, + ntp_servers, + dns_servers, + internal_services_ip_pool_ranges, + external_dns_ips, + external_dns_zone_name, + external_certificates: _, + recovery_silo, + rack_network_config, + allowed_source_ips, + } = &self; + + f.debug_struct("RackInitializeRequest") + .field("trust_quorum_peers", trust_quorum_peers) + .field("bootstrap_discovery", bootstrap_discovery) + .field("ntp_servers", ntp_servers) + .field("dns_servers", dns_servers) + .field( + "internal_services_ip_pool_ranges", + internal_services_ip_pool_ranges, + ) + .field("external_dns_ips", external_dns_ips) + .field("external_dns_zone_name", external_dns_zone_name) + .field("external_certificates", &"") + .field("recovery_silo", recovery_silo) + .field("rack_network_config", rack_network_config) + .field("allowed_source_ips", allowed_source_ips) + .finish() + } +} + +#[derive(Debug, thiserror::Error)] +pub enum RackInitializeRequestParseError { + #[error("Failed to read config from {path}: {err}")] + Io { + path: Utf8PathBuf, + #[source] + err: std::io::Error, + }, + #[error("Failed to deserialize config from {path}: {err}")] + Deserialize { + path: Utf8PathBuf, + #[source] + err: anyhow::Error, + }, + #[error("Loading certificate: {0}")] + Certificate(#[source] anyhow::Error), +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case", tag = "type")] +pub enum BootstrapAddressDiscovery { + /// Ignore all bootstrap addresses except our own. + OnlyOurs, + /// Ignore all bootstrap addresses except the following. + OnlyThese { addrs: BTreeSet }, +} +// "Shadow" copy of `RackInitializeRequest` that does no validation on its +// fields. +#[derive(Clone, Deserialize)] +struct UnvalidatedRackInitializeRequest { + trust_quorum_peers: Option>, + bootstrap_discovery: BootstrapAddressDiscovery, + ntp_servers: Vec, + dns_servers: Vec, + internal_services_ip_pool_ranges: Vec, + external_dns_ips: Vec, + external_dns_zone_name: String, + external_certificates: Vec, + recovery_silo: RecoverySiloConfig, + rack_network_config: RackNetworkConfig, + #[serde(default = "default_allowed_source_ips")] + allowed_source_ips: AllowedSourceIps, +} + +#[derive(Debug, Clone)] +pub struct RackInitializeRequestParams { + pub rack_initialize_request: RackInitializeRequest, + pub skip_timesync: bool, +} + +impl RackInitializeRequestParams { + pub fn new( + rack_initialize_request: RackInitializeRequest, + skip_timesync: bool, + ) -> RackInitializeRequestParams { + RackInitializeRequestParams { rack_initialize_request, skip_timesync } + } +} +impl TryFrom for RackInitializeRequest { + type Error = anyhow::Error; + + fn try_from(value: UnvalidatedRackInitializeRequest) -> Result { + validate_external_dns( + &value.external_dns_ips, + &value.internal_services_ip_pool_ranges, + )?; + + Ok(RackInitializeRequest { + trust_quorum_peers: value.trust_quorum_peers, + bootstrap_discovery: value.bootstrap_discovery, + ntp_servers: value.ntp_servers, + dns_servers: value.dns_servers, + internal_services_ip_pool_ranges: value + .internal_services_ip_pool_ranges, + external_dns_ips: value.external_dns_ips, + external_dns_zone_name: value.external_dns_zone_name, + external_certificates: value.external_certificates, + recovery_silo: value.recovery_silo, + rack_network_config: value.rack_network_config, + allowed_source_ips: value.allowed_source_ips, + }) + } +} + +/// This field was added after several racks were already deployed. RSS plans +/// for those racks should default to allowing any source IP, since that is +/// effectively what they did. +pub const fn default_allowed_source_ips() -> AllowedSourceIps { + AllowedSourceIps::Any +} + +fn validate_external_dns( + dns_ips: &Vec, + internal_ranges: &Vec, +) -> Result<()> { + if dns_ips.is_empty() { + bail!("At least one external DNS IP is required"); + } + + // Every external DNS IP should also be present in one of the internal + // services IP pool ranges. This check is O(N*M), but we expect both N + // and M to be small (~5 DNS servers, and a small number of pools). + for &dns_ip in dns_ips { + if !internal_ranges.iter().any(|range| range.contains(dns_ip)) { + bail!( + "External DNS IP {dns_ip} is not contained in \ + `internal_services_ip_pool_ranges`" + ); + } + } + Ok(()) +} From c6bbb3466b88a70d593378429e5f2f46b5ffab6c Mon Sep 17 00:00:00 2001 From: Ryan Goodfellow Date: Thu, 22 Jan 2026 08:54:39 +0000 Subject: [PATCH 16/20] turn an ipv4addr into an ipaddr --- clients/sled-agent-client/src/lib.rs | 2 +- .../src/api/internal/shared/rack_init/v2.rs | 7 +- .../tasks/sync_switch_configuration.rs | 21 +- nexus/src/app/rack.rs | 2 +- nexus/test-utils/src/starter.rs | 4 +- openapi/bootstrap-agent-lockstep.json | 4 +- openapi/nexus-lockstep.json | 4 +- .../sled-agent/sled-agent-14.0.0-6ac051.json | 10132 ++++++++++++++++ openapi/sled-agent/sled-agent-latest.json | 2 +- openapi/wicketd.json | 2 +- sled-agent/api/src/lib.rs | 45 + .../tests/integration_tests/early_network.rs | 4 +- sled-agent/types/src/early_networking.rs | 3 - .../versions/src/bgp_v6/early_networking.rs | 232 + sled-agent/types/versions/src/bgp_v6/mod.rs | 7 + .../versions/src/impls/early_networking.rs | 103 +- .../versions/src/initial/early_networking.rs | 636 +- .../types/versions/src/initial/rack_init.rs | 4 +- sled-agent/types/versions/src/latest.rs | 4 +- sled-agent/types/versions/src/lib.rs | 2 + wicket-common/src/rack_setup.rs | 2 +- 21 files changed, 10667 insertions(+), 555 deletions(-) create mode 100644 openapi/sled-agent/sled-agent-14.0.0-6ac051.json create mode 100644 sled-agent/types/versions/src/bgp_v6/early_networking.rs create mode 100644 sled-agent/types/versions/src/bgp_v6/mod.rs diff --git a/clients/sled-agent-client/src/lib.rs b/clients/sled-agent-client/src/lib.rs index 59204f21452..ea6105c4e71 100644 --- a/clients/sled-agent-client/src/lib.rs +++ b/clients/sled-agent-client/src/lib.rs @@ -36,7 +36,7 @@ progenitor::generate_api!( LldpPortConfig = { derives = [Eq, Hash, PartialOrd, Ord] }, TxEqConfig = { derives = [Eq, Hash] }, OmicronPhysicalDiskConfig = { derives = [Eq, Hash, PartialOrd, Ord] }, - PortConfigV2 = { derives = [Eq, Hash] }, + PortConfig = { derives = [Eq, Hash] }, RouteConfig = { derives = [Eq, Hash] }, UplinkAddressConfig = { derives = [Eq, Hash] }, VirtualNetworkInterfaceHost = { derives = [Eq, Hash] }, diff --git a/common/src/api/internal/shared/rack_init/v2.rs b/common/src/api/internal/shared/rack_init/v2.rs index a7c99fc7267..1a9c4400a19 100644 --- a/common/src/api/internal/shared/rack_init/v2.rs +++ b/common/src/api/internal/shared/rack_init/v2.rs @@ -57,10 +57,9 @@ pub struct BgpPeerConfig { pub asn: u32, /// Switch port the peer is reachable on. pub port: String, - /// Address of the peer. Use `Ipv4Addr::UNSPECIFIED` to indicate an - /// unnumbered BGP session established over the interface specified by - /// `port`. - pub addr: Ipv4Addr, + /// Address of the peer. Use `UNSPECIFIED` to indicate an unnumbered BGP + /// session established over the interface specified by `port`. + pub addr: IpAddr, /// How long to keep a session alive without a keepalive in seconds. /// Defaults to 6. pub hold_time: Option, diff --git a/nexus/src/app/background/tasks/sync_switch_configuration.rs b/nexus/src/app/background/tasks/sync_switch_configuration.rs index 1d4e6ada3a9..03acb6ed15d 100644 --- a/nexus/src/app/background/tasks/sync_switch_configuration.rs +++ b/nexus/src/app/background/tasks/sync_switch_configuration.rs @@ -54,7 +54,7 @@ use serde_json::json; use sled_agent_client::types::{ BgpConfig as SledBgpConfig, BgpPeerConfig as SledBgpPeerConfig, EarlyNetworkConfig, EarlyNetworkConfigBody, HostPortConfig, - LldpAdminStatus, LldpPortConfig, PortConfigV2, RackNetworkConfigV2, + LldpAdminStatus, LldpPortConfig, PortConfig, RackNetworkConfig, RouteConfig as SledRouteConfig, TxEqConfig, UplinkAddressConfig, }; use std::{ @@ -1072,7 +1072,7 @@ impl BackgroundTask for SwitchPortSettingsManager { bgp.dedup(); - let mut ports: Vec = vec![]; + let mut ports: Vec = vec![]; for (location, port, change) in &changes { let PortSettingsChange::Apply(info) = change else { @@ -1106,7 +1106,7 @@ impl BackgroundTask for SwitchPortSettingsManager { None }; - let mut port_config = PortConfigV2 { + let mut port_config = PortConfig { addresses: info.addresses.iter().map(|a| UplinkAddressConfig { address: a.address, @@ -1118,15 +1118,10 @@ impl BackgroundTask for SwitchPortSettingsManager { .map(|l| l.autoneg) .unwrap_or(false), bgp_peers: peer_configs.into_iter() - // filter maps are cool // For unnumbered peers (addr is None), use UNSPECIFIED - // For IPv6 peers, skip (not supported in this context) - .filter_map(|c| match c.addr { - None => Some((c, Ipv4Addr::UNSPECIFIED)), - Some(addr) => match addr.ip() { - IpAddr::V4(v4) => Some((c, v4)), - IpAddr::V6(_) => None, - }, + .map(|c| match c.addr { + None => (c, IpAddr::V6(Ipv6Addr::UNSPECIFIED)), + Some(addr) => (c, addr.ip()), }) .map(|(c, addr)| { SledBgpPeerConfig { @@ -1197,7 +1192,7 @@ impl BackgroundTask for SwitchPortSettingsManager { let peer_addr_for_lookup = if peer.addr.is_unspecified() { None } else { - Some(IpNetwork::from(IpAddr::from(peer.addr))) + Some(IpNetwork::from(peer.addr)) }; peer.communities = match self @@ -1316,7 +1311,7 @@ impl BackgroundTask for SwitchPortSettingsManager { schema_version: 2, body: EarlyNetworkConfigBody { ntp_servers, - rack_network_config: Some(RackNetworkConfigV2 { + rack_network_config: Some(RackNetworkConfig { rack_subnet: subnet, infra_ip_first, infra_ip_last, diff --git a/nexus/src/app/rack.rs b/nexus/src/app/rack.rs index 4ec340973ea..514ecc7db47 100644 --- a/nexus/src/app/rack.rs +++ b/nexus/src/app/rack.rs @@ -618,7 +618,7 @@ impl super::Nexus { addr: if r.addr.is_unspecified() { None } else { - Some(std::net::IpAddr::V4(r.addr)) + Some(r.addr) }, hold_time: r.hold_time() as u32, idle_hold_time: r.idle_hold_time() as u32, diff --git a/nexus/test-utils/src/starter.rs b/nexus/test-utils/src/starter.rs index d045fe79671..6511274a753 100644 --- a/nexus/test-utils/src/starter.rs +++ b/nexus/test-utils/src/starter.rs @@ -92,7 +92,7 @@ use oximeter_producer::LogConfig; use oximeter_producer::Server as ProducerServer; use sled_agent_client::types::EarlyNetworkConfig; use sled_agent_client::types::EarlyNetworkConfigBody; -use sled_agent_client::types::RackNetworkConfigV2; +use sled_agent_client::types::RackNetworkConfig; use sled_agent_types::inventory::HostPhase2DesiredSlots; use sled_agent_types::inventory::OmicronSledConfig; use sled_agent_types::inventory::OmicronZoneDataset; @@ -922,7 +922,7 @@ impl<'a, N: NexusServer> ControlPlaneStarter<'a, N> { let early_network_config = EarlyNetworkConfig { body: EarlyNetworkConfigBody { ntp_servers: Vec::new(), - rack_network_config: Some(RackNetworkConfigV2 { + rack_network_config: Some(RackNetworkConfig { bfd: Vec::new(), bgp: Vec::new(), infra_ip_first: "192.0.2.10".parse().unwrap(), diff --git a/openapi/bootstrap-agent-lockstep.json b/openapi/bootstrap-agent-lockstep.json index 63c018de9ed..122aaf7b4f3 100644 --- a/openapi/bootstrap-agent-lockstep.json +++ b/openapi/bootstrap-agent-lockstep.json @@ -285,9 +285,9 @@ "type": "object", "properties": { "addr": { - "description": "Address of the peer. Use `Ipv4Addr::UNSPECIFIED` to indicate an unnumbered BGP session established over the interface specified by `port`.", + "description": "Address of the peer. Use `UNSPECIFIED` to indicate an unnumbered BGP session established over the interface specified by `port`.", "type": "string", - "format": "ipv4" + "format": "ip" }, "allowed_export": { "description": "Define export policy for a peer.", diff --git a/openapi/nexus-lockstep.json b/openapi/nexus-lockstep.json index 8c900050a2e..439a4af8d29 100644 --- a/openapi/nexus-lockstep.json +++ b/openapi/nexus-lockstep.json @@ -1657,9 +1657,9 @@ "type": "object", "properties": { "addr": { - "description": "Address of the peer. Use `Ipv4Addr::UNSPECIFIED` to indicate an unnumbered BGP session established over the interface specified by `port`.", + "description": "Address of the peer. Use `UNSPECIFIED` to indicate an unnumbered BGP session established over the interface specified by `port`.", "type": "string", - "format": "ipv4" + "format": "ip" }, "allowed_export": { "description": "Define export policy for a peer.", diff --git a/openapi/sled-agent/sled-agent-14.0.0-6ac051.json b/openapi/sled-agent/sled-agent-14.0.0-6ac051.json new file mode 100644 index 00000000000..c49b0166643 --- /dev/null +++ b/openapi/sled-agent/sled-agent-14.0.0-6ac051.json @@ -0,0 +1,10132 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Oxide Sled Agent API", + "description": "API for interacting with individual sleds", + "contact": { + "url": "https://oxide.computer", + "email": "api@oxide.computer" + }, + "version": "14.0.0" + }, + "paths": { + "/artifacts": { + "get": { + "operationId": "artifact_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArtifactListResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/artifacts/{sha256}": { + "put": { + "operationId": "artifact_put", + "parameters": [ + { + "in": "path", + "name": "sha256", + "required": true, + "schema": { + "type": "string", + "format": "hex string (32 bytes)" + } + }, + { + "in": "query", + "name": "generation", + "required": true, + "schema": { + "$ref": "#/components/schemas/Generation" + } + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArtifactPutResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/artifacts/{sha256}/copy-from-depot": { + "post": { + "operationId": "artifact_copy_from_depot", + "parameters": [ + { + "in": "path", + "name": "sha256", + "required": true, + "schema": { + "type": "string", + "format": "hex string (32 bytes)" + } + }, + { + "in": "query", + "name": "generation", + "required": true, + "schema": { + "$ref": "#/components/schemas/Generation" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArtifactCopyFromDepotBody" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArtifactCopyFromDepotResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/artifacts-config": { + "get": { + "operationId": "artifact_config_get", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArtifactConfig" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "operationId": "artifact_config_put", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArtifactConfig" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/bootstore/status": { + "get": { + "summary": "Get the internal state of the local bootstore node", + "operationId": "bootstore_status", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BootstoreStatus" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/debug/switch-zone-policy": { + "get": { + "summary": "A debugging endpoint only used by `omdb` that allows us to test", + "description": "restarting the switch zone without restarting sled-agent. See for context.", + "operationId": "debug_operator_switch_zone_policy_get", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OperatorSwitchZonePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "A debugging endpoint only used by `omdb` that allows us to test", + "description": "restarting the switch zone without restarting sled-agent. See for context.\n\nSetting the switch zone policy is asynchronous and inherently racy with the standard process of starting the switch zone. If the switch zone is in the process of being started or stopped when this policy is changed, the new policy may not take effect until that transition completes.", + "operationId": "debug_operator_switch_zone_policy_put", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OperatorSwitchZonePolicy" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/disks/{disk_id}": { + "put": { + "operationId": "disk_put", + "parameters": [ + { + "in": "path", + "name": "disk_id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DiskEnsureBody" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DiskRuntimeState" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/eip-gateways": { + "put": { + "summary": "Update per-NIC IP address <-> internet gateway mappings.", + "operationId": "set_eip_gateways", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalIpGatewayMap" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/inventory": { + "get": { + "summary": "Fetch basic information about this sled", + "operationId": "inventory", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Inventory" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/local-storage/{zpool_id}/{dataset_id}": { + "post": { + "summary": "Create a local storage dataset", + "operationId": "local_storage_dataset_ensure", + "parameters": [ + { + "in": "path", + "name": "dataset_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/ExternalZpoolUuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocalStorageDatasetEnsureRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Delete a local storage dataset", + "operationId": "local_storage_dataset_delete", + "parameters": [ + { + "in": "path", + "name": "dataset_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/ExternalZpoolUuid" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/network-bootstore-config": { + "get": { + "summary": "This API endpoint is only reading the local sled agent's view of the", + "description": "bootstore. The boostore is a distributed data store that is eventually consistent. Reads from individual nodes may not represent the latest state.", + "operationId": "read_network_bootstore_config_cache", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EarlyNetworkConfig" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "operationId": "write_network_bootstore_config", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EarlyNetworkConfig" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/omicron-config": { + "put": { + "operationId": "omicron_config_put", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OmicronSledConfig" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/probes": { + "put": { + "summary": "Update the entire set of probe zones on this sled.", + "description": "Probe zones are used to debug networking configuration. They look similar to instances, in that they have an OPTE port on a VPC subnet and external addresses, but no actual VM.", + "operationId": "probes_put", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProbeSet" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/sled-identifiers": { + "get": { + "summary": "Fetch sled identifiers", + "operationId": "sled_identifiers", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledIdentifiers" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/sleds": { + "put": { + "summary": "Add a sled to a rack that was already initialized via RSS", + "operationId": "sled_add", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddSledRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/dladm-info": { + "get": { + "operationId": "support_dladm_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_SledDiagnosticsQueryOutput", + "type": "array", + "items": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/health-check": { + "get": { + "operationId": "support_health_check", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_SledDiagnosticsQueryOutput", + "type": "array", + "items": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/ipadm-info": { + "get": { + "operationId": "support_ipadm_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_SledDiagnosticsQueryOutput", + "type": "array", + "items": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/logs/download/{zone}": { + "get": { + "summary": "This endpoint returns a zip file of a zone's logs organized by service.", + "operationId": "support_logs_download", + "parameters": [ + { + "in": "path", + "name": "zone", + "description": "The zone for which one would like to collect logs for", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "max_rotated", + "description": "The max number of rotated logs to include in the final support bundle", + "required": true, + "schema": { + "type": "integer", + "format": "uint", + "minimum": 0 + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + } + }, + "/support/logs/zones": { + "get": { + "summary": "This endpoint returns a list of known zones on a sled that have service", + "description": "logs that can be collected into a support bundle.", + "operationId": "support_logs", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_String", + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/nvmeadm-info": { + "get": { + "operationId": "support_nvmeadm_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/pargs-info": { + "get": { + "operationId": "support_pargs_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_SledDiagnosticsQueryOutput", + "type": "array", + "items": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/pfiles-info": { + "get": { + "operationId": "support_pfiles_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_SledDiagnosticsQueryOutput", + "type": "array", + "items": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/pstack-info": { + "get": { + "operationId": "support_pstack_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_SledDiagnosticsQueryOutput", + "type": "array", + "items": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/zfs-info": { + "get": { + "operationId": "support_zfs_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/zoneadm-info": { + "get": { + "operationId": "support_zoneadm_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/zpool-info": { + "get": { + "operationId": "support_zpool_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support-bundles/{zpool_id}/{dataset_id}": { + "get": { + "summary": "List all support bundles within a particular dataset", + "operationId": "support_bundle_list", + "parameters": [ + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_SupportBundleMetadata", + "type": "array", + "items": { + "$ref": "#/components/schemas/SupportBundleMetadata" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}": { + "post": { + "summary": "Starts creation of a support bundle within a particular dataset", + "description": "Callers should transfer chunks of the bundle with \"support_bundle_transfer\", and then call \"support_bundle_finalize\" once the bundle has finished transferring.\n\nIf a support bundle was previously created without being finalized successfully, this endpoint will reset the state.\n\nIf a support bundle was previously created and finalized successfully, this endpoint will return metadata indicating that it already exists.", + "operationId": "support_bundle_start_creation", + "parameters": [ + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + } + ], + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupportBundleMetadata" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Delete a support bundle from a particular dataset", + "operationId": "support_bundle_delete", + "parameters": [ + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}/download": { + "get": { + "summary": "Fetch a support bundle from a particular dataset", + "operationId": "support_bundle_download", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + }, + "head": { + "summary": "Fetch metadata about a support bundle from a particular dataset", + "operationId": "support_bundle_head", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + } + }, + "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}/download/{file}": { + "get": { + "summary": "Fetch a file within a support bundle from a particular dataset", + "operationId": "support_bundle_download_file", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "file", + "description": "The path of the file within the support bundle to query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + }, + "head": { + "summary": "Fetch metadata about a file within a support bundle from a particular dataset", + "operationId": "support_bundle_head_file", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "file", + "description": "The path of the file within the support bundle to query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + } + }, + "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}/finalize": { + "post": { + "summary": "Finalizes the creation of a support bundle", + "description": "If the requested hash matched the bundle, the bundle is created. Otherwise, an error is returned.", + "operationId": "support_bundle_finalize", + "parameters": [ + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + }, + { + "in": "query", + "name": "hash", + "required": true, + "schema": { + "type": "string", + "format": "hex string (32 bytes)" + } + } + ], + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupportBundleMetadata" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}/index": { + "get": { + "summary": "Fetch the index (list of files within a support bundle)", + "operationId": "support_bundle_index", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + }, + "head": { + "summary": "Fetch metadata about the list of files within a support bundle", + "operationId": "support_bundle_head_index", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + } + }, + "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}/transfer": { + "put": { + "summary": "Transfers a chunk of a support bundle within a particular dataset", + "operationId": "support_bundle_transfer", + "parameters": [ + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + }, + { + "in": "query", + "name": "offset", + "required": true, + "schema": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupportBundleMetadata" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/switch-ports": { + "post": { + "operationId": "uplink_ensure", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchPorts" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/trust-quorum/commit": { + "put": { + "summary": "Commit a trust quorum configuration", + "operationId": "trust_quorum_commit", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CommitRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/trust-quorum/configuration": { + "post": { + "summary": "Initiate a trust quorum reconfiguration", + "operationId": "trust_quorum_reconfigure", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReconfigureMsg" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/trust-quorum/coordinator-status": { + "get": { + "summary": "Get the coordinator status if this node is coordinating a reconfiguration", + "operationId": "trust_quorum_coordinator_status", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CoordinatorStatus" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/trust-quorum/prepare-and-commit": { + "put": { + "summary": "Attempt to prepare and commit a trust quorum configuration", + "operationId": "trust_quorum_prepare_and_commit", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PrepareAndCommitRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CommitStatus" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/trust-quorum/proxy/commit": { + "put": { + "summary": "Proxy a commit operation to another trust quorum node", + "operationId": "trust_quorum_proxy_commit", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProxyCommitRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/trust-quorum/proxy/prepare-and-commit": { + "put": { + "summary": "Proxy a prepare-and-commit operation to another trust quorum node", + "operationId": "trust_quorum_proxy_prepare_and_commit", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProxyPrepareAndCommitRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CommitStatus" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/trust-quorum/proxy/status": { + "get": { + "summary": "Proxy a status request to another trust quorum node", + "operationId": "trust_quorum_proxy_status", + "parameters": [ + { + "in": "query", + "name": "part_number", + "description": "Oxide Part Number", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "serial_number", + "description": "Serial number (unique for a given part number)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NodeStatus" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/trust-quorum/upgrade": { + "post": { + "summary": "Initiate an upgrade from LRTQ", + "operationId": "trust_quorum_upgrade_from_lrtq", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LrtqUpgradeMsg" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v2p": { + "get": { + "summary": "List v2p mappings present on sled", + "operationId": "list_v2p", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_VirtualNetworkInterfaceHost", + "type": "array", + "items": { + "$ref": "#/components/schemas/VirtualNetworkInterfaceHost" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Create a mapping from a virtual NIC to a physical host", + "operationId": "set_v2p", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VirtualNetworkInterfaceHost" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Delete a mapping from a virtual NIC to a physical host", + "operationId": "del_v2p", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VirtualNetworkInterfaceHost" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/vmms/{propolis_id}": { + "put": { + "operationId": "vmm_register", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceEnsureBody" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledVmmState" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "operationId": "vmm_unregister", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VmmUnregisterResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/vmms/{propolis_id}/disks/{disk_id}/snapshot": { + "post": { + "summary": "Take a snapshot of a disk that is attached to an instance", + "operationId": "vmm_issue_disk_snapshot_request", + "parameters": [ + { + "in": "path", + "name": "disk_id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VmmIssueDiskSnapshotRequestBody" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VmmIssueDiskSnapshotRequestResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/vmms/{propolis_id}/external-ip": { + "put": { + "operationId": "vmm_put_external_ip", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceExternalIpBody" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "operationId": "vmm_delete_external_ip", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceExternalIpBody" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/vmms/{propolis_id}/multicast-group": { + "put": { + "operationId": "vmm_join_multicast_group", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceMulticastBody" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "operationId": "vmm_leave_multicast_group", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceMulticastBody" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/vmms/{propolis_id}/state": { + "get": { + "operationId": "vmm_get_state", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledVmmState" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "operationId": "vmm_put_state", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VmmPutStateBody" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VmmPutStateResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/vpc/{vpc_id}/firewall/rules": { + "put": { + "operationId": "vpc_firewall_rules_put", + "parameters": [ + { + "in": "path", + "name": "vpc_id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcFirewallRulesEnsureBody" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/vpc-routes": { + "get": { + "summary": "Get the current versions of VPC routing rules.", + "operationId": "list_vpc_routes", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_ResolvedVpcRouteState", + "type": "array", + "items": { + "$ref": "#/components/schemas/ResolvedVpcRouteState" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Update VPC routing rules.", + "operationId": "set_vpc_routes", + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Array_of_ResolvedVpcRouteSet", + "type": "array", + "items": { + "$ref": "#/components/schemas/ResolvedVpcRouteSet" + } + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/zones": { + "get": { + "summary": "List the zones that are currently managed by the sled agent.", + "operationId": "zones_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_String", + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/zones/bundle-cleanup": { + "post": { + "summary": "Trigger a zone bundle cleanup.", + "operationId": "zone_bundle_cleanup", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Map_of_CleanupCount", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/CleanupCount" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/zones/bundle-cleanup/context": { + "get": { + "summary": "Return context used by the zone-bundle cleanup task.", + "operationId": "zone_bundle_cleanup_context", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CleanupContext" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Update context used by the zone-bundle cleanup task.", + "operationId": "zone_bundle_cleanup_context_update", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CleanupContextUpdate" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/zones/bundle-cleanup/utilization": { + "get": { + "summary": "Return utilization information about all zone bundles.", + "operationId": "zone_bundle_utilization", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Map_of_BundleUtilization", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/BundleUtilization" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/zones/bundles": { + "get": { + "summary": "List all zone bundles that exist, even for now-deleted zones.", + "operationId": "zone_bundle_list_all", + "parameters": [ + { + "in": "query", + "name": "filter", + "description": "An optional substring used to filter zone bundles.", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_ZoneBundleMetadata", + "type": "array", + "items": { + "$ref": "#/components/schemas/ZoneBundleMetadata" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/zones/bundles/{zone_name}": { + "get": { + "summary": "List the zone bundles that are available for a running zone.", + "operationId": "zone_bundle_list", + "parameters": [ + { + "in": "path", + "name": "zone_name", + "description": "The name of the zone.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_ZoneBundleMetadata", + "type": "array", + "items": { + "$ref": "#/components/schemas/ZoneBundleMetadata" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/zones/bundles/{zone_name}/{bundle_id}": { + "get": { + "summary": "Fetch the binary content of a single zone bundle.", + "operationId": "zone_bundle_get", + "parameters": [ + { + "in": "path", + "name": "bundle_id", + "description": "The ID for this bundle itself.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "path", + "name": "zone_name", + "description": "The name of the zone this bundle is derived from.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Delete a zone bundle.", + "operationId": "zone_bundle_delete", + "parameters": [ + { + "in": "path", + "name": "bundle_id", + "description": "The ID for this bundle itself.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "path", + "name": "zone_name", + "description": "The name of the zone this bundle is derived from.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + } + }, + "components": { + "schemas": { + "AddSledRequest": { + "description": "A request to Add a given sled after rack initialization has occurred", + "type": "object", + "properties": { + "sled_id": { + "$ref": "#/components/schemas/BaseboardId" + }, + "start_request": { + "$ref": "#/components/schemas/StartSledAgentRequest" + } + }, + "required": [ + "sled_id", + "start_request" + ] + }, + "Alarm": { + "description": "An alarm indicating a protocol invariant violation.", + "oneOf": [ + { + "description": "Different configurations found for the same epoch.\n\nReason: Nexus creates configurations and stores them in CRDB before sending them to a coordinator of its choosing. Nexus will not send the same reconfiguration request to different coordinators. If it does those coordinators will generate different key shares. However, since Nexus will not tell different nodes to coordinate the same configuration, this state should be impossible to reach.", + "type": "object", + "properties": { + "mismatched_configurations": { + "type": "object", + "properties": { + "config1": { + "$ref": "#/components/schemas/Configuration" + }, + "config2": { + "$ref": "#/components/schemas/Configuration" + }, + "from": { + "description": "Either a stringified `BaseboardId` or \"Nexus\".", + "type": "string" + } + }, + "required": [ + "config1", + "config2", + "from" + ] + } + }, + "required": [ + "mismatched_configurations" + ], + "additionalProperties": false + }, + { + "description": "The `keyShareComputer` could not compute this node's share.\n\nReason: A threshold of valid key shares were received based on the the share digests in the Configuration. However, computation of the share still failed. This should be impossible.", + "type": "object", + "properties": { + "share_computation_failed": { + "type": "object", + "properties": { + "epoch": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "err": { + "$ref": "#/components/schemas/CombineError" + } + }, + "required": [ + "epoch", + "err" + ] + } + }, + "required": [ + "share_computation_failed" + ], + "additionalProperties": false + }, + { + "description": "We started collecting shares for a committed configuration, but we no longer have that configuration in our persistent state.", + "type": "object", + "properties": { + "committed_configuration_lost": { + "type": "object", + "properties": { + "collecting_epoch": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "latest_committed_epoch": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "collecting_epoch", + "latest_committed_epoch" + ] + } + }, + "required": [ + "committed_configuration_lost" + ], + "additionalProperties": false + }, + { + "description": "Decrypting the encrypted rack secrets failed when presented with a `valid` RackSecret.\n\n`Configuration` membership contains the hashes of each valid share. All shares utilized to reconstruct the rack secret were validated against these hashes, and the rack secret was reconstructed. However, using the rack secret to derive encryption keys and decrypt the secrets from old configurations still failed. This should never be possible, and therefore we raise an alarm.", + "type": "object", + "properties": { + "rack_secret_decryption_failed": { + "type": "object", + "properties": { + "epoch": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "err": { + "$ref": "#/components/schemas/DecryptionError" + } + }, + "required": [ + "epoch", + "err" + ] + } + }, + "required": [ + "rack_secret_decryption_failed" + ], + "additionalProperties": false + }, + { + "description": "Reconstructing the rack secret failed when presented with `valid` shares.\n\n`Configuration` membership contains the hashes of each valid share. All shares utilized to reconstruct the rack secret were validated against these hashes, and yet, the reconstruction still failed. This indicates either a bit flip in a share after validation, or, more likely, an invalid hash.", + "type": "object", + "properties": { + "rack_secret_reconstruction_failed": { + "type": "object", + "properties": { + "epoch": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "err": { + "$ref": "#/components/schemas/RackSecretReconstructError" + } + }, + "required": [ + "epoch", + "err" + ] + } + }, + "required": [ + "rack_secret_reconstruction_failed" + ], + "additionalProperties": false + } + ] + }, + "ArtifactConfig": { + "description": "Artifact configuration.\n\nThis type is used in both GET (response) and PUT (request) operations.", + "type": "object", + "properties": { + "artifacts": { + "type": "array", + "items": { + "type": "string", + "format": "hex string (32 bytes)" + }, + "uniqueItems": true + }, + "generation": { + "$ref": "#/components/schemas/Generation" + } + }, + "required": [ + "artifacts", + "generation" + ] + }, + "ArtifactCopyFromDepotBody": { + "description": "Request body for copying artifacts from a depot.", + "type": "object", + "properties": { + "depot_base_url": { + "type": "string" + } + }, + "required": [ + "depot_base_url" + ] + }, + "ArtifactCopyFromDepotResponse": { + "description": "Response for copying artifacts from a depot.", + "type": "object" + }, + "ArtifactListResponse": { + "description": "Response for listing artifacts.", + "type": "object", + "properties": { + "generation": { + "$ref": "#/components/schemas/Generation" + }, + "list": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "uint", + "minimum": 0 + } + } + }, + "required": [ + "generation", + "list" + ] + }, + "ArtifactPutResponse": { + "description": "Response for putting an artifact.", + "type": "object", + "properties": { + "datasets": { + "description": "The number of valid M.2 artifact datasets we found on the sled. There is typically one of these datasets for each functional M.2.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "successful_writes": { + "description": "The number of valid writes to the M.2 artifact datasets. This should be less than or equal to the number of artifact datasets.", + "type": "integer", + "format": "uint", + "minimum": 0 + } + }, + "required": [ + "datasets", + "successful_writes" + ] + }, + "Baseboard": { + "description": "Describes properties that should uniquely identify a Gimlet.", + "oneOf": [ + { + "type": "object", + "properties": { + "identifier": { + "type": "string" + }, + "model": { + "type": "string" + }, + "revision": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "gimlet" + ] + } + }, + "required": [ + "identifier", + "model", + "revision", + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "unknown" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "identifier": { + "type": "string" + }, + "model": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "pc" + ] + } + }, + "required": [ + "identifier", + "model", + "type" + ] + } + ] + }, + "BaseboardId": { + "description": "A representation of a Baseboard ID as used in the inventory subsystem.\n\nThis type is essentially the same as a `Baseboard` except it doesn't have a revision or HW type (Gimlet, PC, Unknown).", + "type": "object", + "properties": { + "part_number": { + "description": "Oxide Part Number", + "type": "string" + }, + "serial_number": { + "description": "Serial number (unique for a given part number)", + "type": "string" + } + }, + "required": [ + "part_number", + "serial_number" + ] + }, + "BfdMode": { + "description": "BFD connection mode.", + "type": "string", + "enum": [ + "single_hop", + "multi_hop" + ] + }, + "BfdPeerConfig": { + "type": "object", + "properties": { + "detection_threshold": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "local": { + "nullable": true, + "type": "string", + "format": "ip" + }, + "mode": { + "$ref": "#/components/schemas/BfdMode" + }, + "remote": { + "type": "string", + "format": "ip" + }, + "required_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "switch": { + "$ref": "#/components/schemas/SwitchLocation" + } + }, + "required": [ + "detection_threshold", + "mode", + "remote", + "required_rx", + "switch" + ] + }, + "BgpConfig": { + "type": "object", + "properties": { + "asn": { + "description": "The autonomous system number for the BGP configuration.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "checker": { + "nullable": true, + "description": "Checker to apply to incoming messages.", + "default": null, + "type": "string" + }, + "originate": { + "description": "The set of prefixes for the BGP router to originate.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv4Net" + } + }, + "shaper": { + "nullable": true, + "description": "Shaper to apply to outgoing messages.", + "default": null, + "type": "string" + } + }, + "required": [ + "asn", + "originate" + ] + }, + "BgpPeerConfig": { + "type": "object", + "properties": { + "addr": { + "description": "Address of the peer. Use `UNSPECIFIED` to indicate an unnumbered BGP session established over the interface specified by `port`.", + "type": "string", + "format": "ip" + }, + "allowed_export": { + "description": "Define export policy for a peer.", + "default": { + "type": "no_filtering" + }, + "allOf": [ + { + "$ref": "#/components/schemas/ImportExportPolicy" + } + ] + }, + "allowed_import": { + "description": "Define import policy for a peer.", + "default": { + "type": "no_filtering" + }, + "allOf": [ + { + "$ref": "#/components/schemas/ImportExportPolicy" + } + ] + }, + "asn": { + "description": "The autonomous system number of the router the peer belongs to.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "communities": { + "description": "Include the provided communities in updates sent to the peer.", + "default": [], + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "connect_retry": { + "nullable": true, + "description": "The interval in seconds between peer connection retry attempts.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "delay_open": { + "nullable": true, + "description": "How long to delay sending open messages to a peer. In seconds.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "enforce_first_as": { + "description": "Enforce that the first AS in paths received from this peer is the peer's AS.", + "default": false, + "type": "boolean" + }, + "hold_time": { + "nullable": true, + "description": "How long to keep a session alive without a keepalive in seconds. Defaults to 6.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "idle_hold_time": { + "nullable": true, + "description": "How long to keep a peer in idle after a state machine reset in seconds.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "keepalive": { + "nullable": true, + "description": "The interval to send keepalive messages at.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "local_pref": { + "nullable": true, + "description": "Apply a local preference to routes received from this peer.", + "default": null, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "md5_auth_key": { + "nullable": true, + "description": "Use the given key for TCP-MD5 authentication with the peer.", + "default": null, + "type": "string" + }, + "min_ttl": { + "nullable": true, + "description": "Require messages from a peer have a minimum IP time to live field.", + "default": null, + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "multi_exit_discriminator": { + "nullable": true, + "description": "Apply the provided multi-exit discriminator (MED) updates sent to the peer.", + "default": null, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "port": { + "description": "Switch port the peer is reachable on.", + "type": "string" + }, + "remote_asn": { + "nullable": true, + "description": "Require that a peer has a specified ASN.", + "default": null, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "vlan_id": { + "nullable": true, + "description": "Associate a VLAN ID with a BGP peer session.", + "default": null, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "addr", + "asn", + "port" + ] + }, + "BlobStorageBackend": { + "description": "A storage backend for a disk whose initial contents are given explicitly by the specification.", + "type": "object", + "properties": { + "base64": { + "description": "The disk's initial contents, encoded as a base64 string.", + "type": "string" + }, + "readonly": { + "description": "Indicates whether the storage is read-only.", + "type": "boolean" + } + }, + "required": [ + "base64", + "readonly" + ], + "additionalProperties": false + }, + "Board": { + "description": "A VM's mainboard.", + "type": "object", + "properties": { + "chipset": { + "description": "The chipset to expose to guest software.", + "allOf": [ + { + "$ref": "#/components/schemas/Chipset" + } + ] + }, + "cpuid": { + "nullable": true, + "description": "The CPUID values to expose to the guest. If `None`, bhyve will derive default values from the host's CPUID values.", + "allOf": [ + { + "$ref": "#/components/schemas/Cpuid" + } + ] + }, + "cpus": { + "description": "The number of virtual logical processors attached to this VM.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "guest_hv_interface": { + "description": "The hypervisor platform to expose to the guest. The default is a bhyve-compatible interface with no additional features.\n\nFor compatibility with older versions of Propolis, this field is only serialized if it specifies a non-default interface.", + "allOf": [ + { + "$ref": "#/components/schemas/GuestHypervisorInterface" + } + ] + }, + "memory_mb": { + "description": "The amount of guest RAM attached to this VM.", + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "chipset", + "cpus", + "memory_mb" + ], + "additionalProperties": false + }, + "BootImageHeader": { + "type": "object", + "properties": { + "data_size": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "flags": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "image_name": { + "type": "string" + }, + "image_size": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "sha256": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "minItems": 32, + "maxItems": 32 + }, + "target_size": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "data_size", + "flags", + "image_name", + "image_size", + "sha256", + "target_size" + ] + }, + "BootOrderEntry": { + "description": "An entry in the boot order stored in a [`BootSettings`] component.", + "type": "object", + "properties": { + "id": { + "description": "The ID of another component in the spec that Propolis should try to boot from.\n\nCurrently, only disk device components are supported.", + "allOf": [ + { + "$ref": "#/components/schemas/SpecKey" + } + ] + } + }, + "required": [ + "id" + ] + }, + "BootPartitionContents": { + "type": "object", + "properties": { + "boot_disk": { + "x-rust-type": { + "crate": "std", + "parameters": [ + { + "$ref": "#/components/schemas/M2Slot" + }, + { + "type": "string" + } + ], + "path": "::std::result::Result", + "version": "*" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "ok": { + "$ref": "#/components/schemas/M2Slot" + } + }, + "required": [ + "ok" + ] + }, + { + "type": "object", + "properties": { + "err": { + "type": "string" + } + }, + "required": [ + "err" + ] + } + ] + }, + "slot_a": { + "x-rust-type": { + "crate": "std", + "parameters": [ + { + "$ref": "#/components/schemas/BootPartitionDetails" + }, + { + "type": "string" + } + ], + "path": "::std::result::Result", + "version": "*" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "ok": { + "$ref": "#/components/schemas/BootPartitionDetails" + } + }, + "required": [ + "ok" + ] + }, + { + "type": "object", + "properties": { + "err": { + "type": "string" + } + }, + "required": [ + "err" + ] + } + ] + }, + "slot_b": { + "x-rust-type": { + "crate": "std", + "parameters": [ + { + "$ref": "#/components/schemas/BootPartitionDetails" + }, + { + "type": "string" + } + ], + "path": "::std::result::Result", + "version": "*" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "ok": { + "$ref": "#/components/schemas/BootPartitionDetails" + } + }, + "required": [ + "ok" + ] + }, + { + "type": "object", + "properties": { + "err": { + "type": "string" + } + }, + "required": [ + "err" + ] + } + ] + } + }, + "required": [ + "boot_disk", + "slot_a", + "slot_b" + ] + }, + "BootPartitionDetails": { + "type": "object", + "properties": { + "artifact_hash": { + "type": "string", + "format": "hex string (32 bytes)" + }, + "artifact_size": { + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "header": { + "$ref": "#/components/schemas/BootImageHeader" + } + }, + "required": [ + "artifact_hash", + "artifact_size", + "header" + ] + }, + "BootSettings": { + "description": "Settings supplied to the guest's firmware image that specify the order in which it should consider its options when selecting a device to try to boot from.", + "type": "object", + "properties": { + "order": { + "description": "An ordered list of components to attempt to boot from.", + "type": "array", + "items": { + "$ref": "#/components/schemas/BootOrderEntry" + } + } + }, + "required": [ + "order" + ], + "additionalProperties": false + }, + "BootstoreStatus": { + "description": "Status of the local bootstore node.", + "type": "object", + "properties": { + "accepted_connections": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "established_connections": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EstablishedConnection" + } + }, + "fsm_ledger_generation": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "fsm_state": { + "type": "string" + }, + "negotiating_connections": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "network_config_ledger_generation": { + "nullable": true, + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "peers": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + "required": [ + "accepted_connections", + "established_connections", + "fsm_ledger_generation", + "fsm_state", + "negotiating_connections", + "peers" + ] + }, + "BundleUtilization": { + "description": "The portion of a debug dataset used for zone bundles.", + "type": "object", + "properties": { + "bytes_available": { + "description": "The total number of bytes available for zone bundles.\n\nThis is `dataset_quota` multiplied by the context's storage limit.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "bytes_used": { + "description": "Total bundle usage, in bytes.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "dataset_quota": { + "description": "The total dataset quota, in bytes.", + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "bytes_available", + "bytes_used", + "dataset_quota" + ] + }, + "ByteCount": { + "description": "Byte count to express memory or storage capacity.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "Chipset": { + "description": "A kind of virtual chipset.", + "oneOf": [ + { + "description": "An Intel 440FX-compatible chipset.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "i440_fx" + ] + }, + "value": { + "$ref": "#/components/schemas/I440Fx" + } + }, + "required": [ + "type", + "value" + ], + "additionalProperties": false + } + ] + }, + "CleanupContext": { + "description": "Context provided for the zone bundle cleanup task.", + "type": "object", + "properties": { + "period": { + "description": "The period on which automatic checks and cleanup is performed.", + "allOf": [ + { + "$ref": "#/components/schemas/CleanupPeriod" + } + ] + }, + "priority": { + "description": "The priority ordering for keeping old bundles.", + "allOf": [ + { + "$ref": "#/components/schemas/PriorityOrder" + } + ] + }, + "storage_limit": { + "description": "The limit on the dataset quota available for zone bundles.", + "allOf": [ + { + "$ref": "#/components/schemas/StorageLimit" + } + ] + } + }, + "required": [ + "period", + "priority", + "storage_limit" + ] + }, + "CleanupContextUpdate": { + "description": "Parameters used to update the zone bundle cleanup context.", + "type": "object", + "properties": { + "period": { + "nullable": true, + "description": "The new period on which automatic cleanups are run.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "priority": { + "nullable": true, + "description": "The priority ordering for preserving old zone bundles.", + "allOf": [ + { + "$ref": "#/components/schemas/PriorityOrder" + } + ] + }, + "storage_limit": { + "nullable": true, + "description": "The new limit on the underlying dataset quota allowed for bundles.", + "type": "integer", + "format": "uint8", + "minimum": 0 + } + } + }, + "CleanupCount": { + "description": "The count of bundles / bytes removed during a cleanup operation.", + "type": "object", + "properties": { + "bundles": { + "description": "The number of bundles removed.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "bytes": { + "description": "The number of bytes removed.", + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "bundles", + "bytes" + ] + }, + "CleanupPeriod": { + "description": "A period on which bundles are automatically cleaned up.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "CombineError": { + "type": "string", + "enum": [ + "too_few_shares", + "duplicate_x_coordinates", + "invalid_share_lengths", + "invalid_share_id" + ] + }, + "CommitRequest": { + "description": "Request to commit a trust quorum configuration at a given epoch.", + "type": "object", + "properties": { + "epoch": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "rack_id": { + "$ref": "#/components/schemas/RackUuid" + } + }, + "required": [ + "epoch", + "rack_id" + ] + }, + "CommitStatus": { + "description": "Whether or not a configuration has been committed or is still underway.", + "type": "string", + "enum": [ + "committed", + "pending" + ] + }, + "ComponentV0": { + "oneOf": [ + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/VirtioDisk" + }, + "type": { + "type": "string", + "enum": [ + "virtio_disk" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/NvmeDisk" + }, + "type": { + "type": "string", + "enum": [ + "nvme_disk" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/VirtioNic" + }, + "type": { + "type": "string", + "enum": [ + "virtio_nic" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/SerialPort" + }, + "type": { + "type": "string", + "enum": [ + "serial_port" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/PciPciBridge" + }, + "type": { + "type": "string", + "enum": [ + "pci_pci_bridge" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/QemuPvpanic" + }, + "type": { + "type": "string", + "enum": [ + "qemu_pvpanic" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/BootSettings" + }, + "type": { + "type": "string", + "enum": [ + "boot_settings" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/SoftNpuPciPort" + }, + "type": { + "type": "string", + "enum": [ + "soft_npu_pci_port" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/SoftNpuPort" + }, + "type": { + "type": "string", + "enum": [ + "soft_npu_port" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/SoftNpuP9" + }, + "type": { + "type": "string", + "enum": [ + "soft_npu_p9" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/P9fs" + }, + "type": { + "type": "string", + "enum": [ + "p9fs" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/MigrationFailureInjector" + }, + "type": { + "type": "string", + "enum": [ + "migration_failure_injector" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/CrucibleStorageBackend" + }, + "type": { + "type": "string", + "enum": [ + "crucible_storage_backend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/FileStorageBackend" + }, + "type": { + "type": "string", + "enum": [ + "file_storage_backend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/BlobStorageBackend" + }, + "type": { + "type": "string", + "enum": [ + "blob_storage_backend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/VirtioNetworkBackend" + }, + "type": { + "type": "string", + "enum": [ + "virtio_network_backend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/DlpiNetworkBackend" + }, + "type": { + "type": "string", + "enum": [ + "dlpi_network_backend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + } + ] + }, + "CompressionAlgorithm": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "on" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "off" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "gzip" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "level": { + "$ref": "#/components/schemas/GzipLevel" + }, + "type": { + "type": "string", + "enum": [ + "gzip_n" + ] + } + }, + "required": [ + "level", + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "lz4" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "lzjb" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "zle" + ] + } + }, + "required": [ + "type" + ] + } + ] + }, + "ConfigReconcilerInventory": { + "description": "Describes the last attempt made by the sled-agent-config-reconciler to reconcile the current sled config against the actual state of the sled.", + "type": "object", + "properties": { + "boot_partitions": { + "$ref": "#/components/schemas/BootPartitionContents" + }, + "datasets": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ConfigReconcilerInventoryResult" + } + }, + "external_disks": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ConfigReconcilerInventoryResult" + } + }, + "last_reconciled_config": { + "$ref": "#/components/schemas/OmicronSledConfig" + }, + "orphaned_datasets": { + "title": "IdOrdMap", + "x-rust-type": { + "crate": "iddqd", + "parameters": [ + { + "$ref": "#/components/schemas/OrphanedDataset" + } + ], + "path": "iddqd::IdOrdMap", + "version": "*" + }, + "type": "array", + "items": { + "$ref": "#/components/schemas/OrphanedDataset" + }, + "uniqueItems": true + }, + "remove_mupdate_override": { + "nullable": true, + "description": "The result of removing the mupdate override file on disk.\n\n`None` if `remove_mupdate_override` was not provided in the sled config.", + "allOf": [ + { + "$ref": "#/components/schemas/RemoveMupdateOverrideInventory" + } + ] + }, + "zones": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ConfigReconcilerInventoryResult" + } + } + }, + "required": [ + "boot_partitions", + "datasets", + "external_disks", + "last_reconciled_config", + "orphaned_datasets", + "zones" + ] + }, + "ConfigReconcilerInventoryResult": { + "oneOf": [ + { + "type": "object", + "properties": { + "result": { + "type": "string", + "enum": [ + "ok" + ] + } + }, + "required": [ + "result" + ] + }, + { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "result": { + "type": "string", + "enum": [ + "err" + ] + } + }, + "required": [ + "message", + "result" + ] + } + ] + }, + "ConfigReconcilerInventoryStatus": { + "description": "Status of the sled-agent-config-reconciler task.", + "oneOf": [ + { + "description": "The reconciler task has not yet run for the first time since sled-agent started.", + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "not_yet_run" + ] + } + }, + "required": [ + "status" + ] + }, + { + "description": "The reconciler task is actively running.", + "type": "object", + "properties": { + "config": { + "$ref": "#/components/schemas/OmicronSledConfig" + }, + "running_for": { + "$ref": "#/components/schemas/Duration" + }, + "started_at": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string", + "enum": [ + "running" + ] + } + }, + "required": [ + "config", + "running_for", + "started_at", + "status" + ] + }, + { + "description": "The reconciler task is currently idle, but previously did complete a reconciliation attempt.\n\nThis variant does not include the `OmicronSledConfig` used in the last attempt, because that's always available via [`ConfigReconcilerInventory::last_reconciled_config`].", + "type": "object", + "properties": { + "completed_at": { + "type": "string", + "format": "date-time" + }, + "ran_for": { + "$ref": "#/components/schemas/Duration" + }, + "status": { + "type": "string", + "enum": [ + "idle" + ] + } + }, + "required": [ + "completed_at", + "ran_for", + "status" + ] + } + ] + }, + "Configuration": { + "description": "The configuration for a given epoch.\n\nOnly valid for non-lrtq configurations.", + "type": "object", + "properties": { + "coordinator": { + "description": "Who was the coordinator of this reconfiguration?", + "allOf": [ + { + "$ref": "#/components/schemas/BaseboardId" + } + ] + }, + "encrypted_rack_secrets": { + "nullable": true, + "description": "There are no encrypted rack secrets for the initial configuration.", + "allOf": [ + { + "$ref": "#/components/schemas/EncryptedRackSecrets" + } + ] + }, + "epoch": { + "description": "Unique, monotonically increasing identifier for a configuration.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "members": { + "description": "All members of the current configuration and the hash of their key shares.", + "type": "array", + "items": { + "$ref": "#/components/schemas/ConfigurationMember" + } + }, + "rack_id": { + "description": "Unique Id of the rack.", + "allOf": [ + { + "$ref": "#/components/schemas/RackUuid" + } + ] + }, + "threshold": { + "description": "The number of sleds required to reconstruct the rack secret.", + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "coordinator", + "epoch", + "members", + "rack_id", + "threshold" + ] + }, + "ConfigurationMember": { + "description": "A member entry in a trust quorum configuration.\n\nThis type is used for OpenAPI schema generation since OpenAPI v3.0.x doesn't support tuple arrays.", + "type": "object", + "properties": { + "id": { + "description": "The baseboard ID of the member.", + "allOf": [ + { + "$ref": "#/components/schemas/BaseboardId" + } + ] + }, + "share_digest": { + "description": "The SHA3-256 hash of the member's key share.", + "type": "string", + "format": "hex string (32 bytes)" + } + }, + "required": [ + "id", + "share_digest" + ] + }, + "CoordinatorStatus": { + "description": "Status of the node coordinating the reconfiguration or LRTQ upgrade.", + "type": "object", + "properties": { + "acked_prepares": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseboardId" + }, + "uniqueItems": true + }, + "config": { + "$ref": "#/components/schemas/Configuration" + } + }, + "required": [ + "acked_prepares", + "config" + ] + }, + "Cpuid": { + "description": "A set of CPUID values to expose to a guest.", + "type": "object", + "properties": { + "entries": { + "description": "A list of CPUID leaves/subleaves and their associated values.\n\nPropolis servers require that each entry's `leaf` be unique and that it falls in either the \"standard\" (0 to 0xFFFF) or \"extended\" (0x8000_0000 to 0x8000_FFFF) function ranges, since these are the only valid input ranges currently defined by Intel and AMD. See the Intel 64 and IA-32 Architectures Software Developer's Manual (June 2024) Table 3-17 and the AMD64 Architecture Programmer's Manual (March 2024) Volume 3's documentation of the CPUID instruction.", + "type": "array", + "items": { + "$ref": "#/components/schemas/CpuidEntry" + } + }, + "vendor": { + "description": "The CPU vendor to emulate.\n\nCPUID leaves in the extended range (0x8000_0000 to 0x8000_FFFF) have vendor-defined semantics. Propolis uses this value to determine these semantics when deciding whether it needs to specialize the supplied template values for these leaves.", + "allOf": [ + { + "$ref": "#/components/schemas/CpuidVendor" + } + ] + } + }, + "required": [ + "entries", + "vendor" + ], + "additionalProperties": false + }, + "CpuidEntry": { + "description": "A full description of a CPUID leaf/subleaf and the values it produces.", + "type": "object", + "properties": { + "eax": { + "description": "The value to return in eax.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "ebx": { + "description": "The value to return in ebx.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "ecx": { + "description": "The value to return in ecx.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "edx": { + "description": "The value to return in edx.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "leaf": { + "description": "The leaf (function) number for this entry.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "subleaf": { + "nullable": true, + "description": "The subleaf (index) number for this entry, if it uses subleaves.", + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "eax", + "ebx", + "ecx", + "edx", + "leaf" + ], + "additionalProperties": false + }, + "CpuidVendor": { + "description": "A CPU vendor to use when interpreting the meanings of CPUID leaves in the extended ID range (0x80000000 to 0x8000FFFF).", + "type": "string", + "enum": [ + "amd", + "intel" + ] + }, + "CrucibleStorageBackend": { + "description": "A Crucible storage backend.", + "type": "object", + "properties": { + "readonly": { + "description": "Indicates whether the storage is read-only.", + "type": "boolean" + }, + "request_json": { + "description": "A serialized `[crucible_client_types::VolumeConstructionRequest]`. This is stored in serialized form so that breaking changes to the definition of a `VolumeConstructionRequest` do not inadvertently break instance spec deserialization.\n\nWhen using a spec to initialize a new instance, the spec author must ensure this request is well-formed and can be deserialized by the version of `crucible_client_types` used by the target Propolis.", + "type": "string" + } + }, + "required": [ + "readonly", + "request_json" + ], + "additionalProperties": false + }, + "DatasetConfig": { + "description": "Configuration information necessary to request a single dataset.\n\nThese datasets are tracked directly by Nexus.", + "type": "object", + "properties": { + "compression": { + "description": "The compression mode to be used by the dataset", + "allOf": [ + { + "$ref": "#/components/schemas/CompressionAlgorithm" + } + ] + }, + "id": { + "description": "The UUID of the dataset being requested", + "allOf": [ + { + "$ref": "#/components/schemas/DatasetUuid" + } + ] + }, + "name": { + "description": "The dataset's name", + "allOf": [ + { + "$ref": "#/components/schemas/DatasetName" + } + ] + }, + "quota": { + "nullable": true, + "description": "The upper bound on the amount of storage used by this dataset", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + }, + "reservation": { + "nullable": true, + "description": "The lower bound on the amount of storage usable by this dataset", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + } + }, + "required": [ + "compression", + "id", + "name" + ] + }, + "DatasetKind": { + "description": "The kind of dataset. See the `DatasetKind` enum in omicron-common for possible values.", + "type": "string" + }, + "DatasetName": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/components/schemas/DatasetKind" + }, + "pool_name": { + "$ref": "#/components/schemas/ZpoolName" + } + }, + "required": [ + "kind", + "pool_name" + ] + }, + "DatasetUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::DatasetUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "DecryptionError": { + "description": "Error decrypting rack secrets.", + "oneOf": [ + { + "description": "An opaque error indicating decryption failed.", + "type": "string", + "enum": [ + "aead" + ] + }, + { + "description": "The length of the plaintext is not the correct size and cannot be decoded.", + "type": "string", + "enum": [ + "invalid_length" + ] + } + ] + }, + "DelegatedZvol": { + "description": "Delegate a ZFS volume to a zone", + "oneOf": [ + { + "description": "Delegate a slice of the local storage dataset present on this pool into the zone.", + "type": "object", + "properties": { + "dataset_id": { + "$ref": "#/components/schemas/DatasetUuid" + }, + "type": { + "type": "string", + "enum": [ + "local_storage" + ] + }, + "zpool_id": { + "$ref": "#/components/schemas/ExternalZpoolUuid" + } + }, + "required": [ + "dataset_id", + "type", + "zpool_id" + ] + } + ] + }, + "DhcpConfig": { + "description": "DHCP configuration for a port\n\nNot present here: Hostname (DHCPv4 option 12; used in DHCPv6 option 39); we use `InstanceRuntimeState::hostname` for this value.", + "type": "object", + "properties": { + "dns_servers": { + "description": "DNS servers to send to the instance\n\n(DHCPv4 option 6; DHCPv6 option 23)", + "type": "array", + "items": { + "type": "string", + "format": "ip" + } + }, + "host_domain": { + "nullable": true, + "description": "DNS zone this instance's hostname belongs to (e.g. the `project.example` part of `instance1.project.example`)\n\n(DHCPv4 option 15; used in DHCPv6 option 39)", + "type": "string" + }, + "search_domains": { + "description": "DNS search domains\n\n(DHCPv4 option 119; DHCPv6 option 24)", + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "dns_servers", + "search_domains" + ] + }, + "DiskEnsureBody": { + "description": "Sent from to a sled agent to establish the runtime state of a Disk", + "type": "object", + "properties": { + "initial_runtime": { + "description": "Last runtime state of the Disk known to Nexus (used if the agent has never seen this Disk before).", + "allOf": [ + { + "$ref": "#/components/schemas/DiskRuntimeState" + } + ] + }, + "target": { + "description": "requested runtime state of the Disk", + "allOf": [ + { + "$ref": "#/components/schemas/DiskStateRequested" + } + ] + } + }, + "required": [ + "initial_runtime", + "target" + ] + }, + "DiskIdentity": { + "description": "Uniquely identifies a disk.", + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "serial": { + "type": "string" + }, + "vendor": { + "type": "string" + } + }, + "required": [ + "model", + "serial", + "vendor" + ] + }, + "DiskRuntimeState": { + "description": "Runtime state of the Disk, which includes its attach state and some minimal metadata", + "type": "object", + "properties": { + "disk_state": { + "description": "runtime state of the Disk", + "allOf": [ + { + "$ref": "#/components/schemas/DiskState" + } + ] + }, + "gen": { + "description": "generation number for this state", + "allOf": [ + { + "$ref": "#/components/schemas/Generation" + } + ] + }, + "time_updated": { + "description": "timestamp for this information", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "disk_state", + "gen", + "time_updated" + ] + }, + "DiskState": { + "description": "State of a Disk", + "oneOf": [ + { + "description": "Disk is being initialized", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "creating" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is ready but detached from any Instance", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "detached" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is ready to receive blocks from an external source", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "import_ready" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is importing blocks from a URL", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "importing_from_url" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is importing blocks from bulk writes", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "importing_from_bulk_writes" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is being finalized to state Detached", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "finalizing" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is undergoing maintenance", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "maintenance" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is being attached to the given Instance", + "type": "object", + "properties": { + "instance": { + "type": "string", + "format": "uuid" + }, + "state": { + "type": "string", + "enum": [ + "attaching" + ] + } + }, + "required": [ + "instance", + "state" + ] + }, + { + "description": "Disk is attached to the given Instance", + "type": "object", + "properties": { + "instance": { + "type": "string", + "format": "uuid" + }, + "state": { + "type": "string", + "enum": [ + "attached" + ] + } + }, + "required": [ + "instance", + "state" + ] + }, + { + "description": "Disk is being detached from the given Instance", + "type": "object", + "properties": { + "instance": { + "type": "string", + "format": "uuid" + }, + "state": { + "type": "string", + "enum": [ + "detaching" + ] + } + }, + "required": [ + "instance", + "state" + ] + }, + { + "description": "Disk has been destroyed", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "destroyed" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is unavailable", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "faulted" + ] + } + }, + "required": [ + "state" + ] + } + ] + }, + "DiskStateRequested": { + "description": "Used to request a Disk state change", + "oneOf": [ + { + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "detached" + ] + } + }, + "required": [ + "state" + ] + }, + { + "type": "object", + "properties": { + "instance": { + "type": "string", + "format": "uuid" + }, + "state": { + "type": "string", + "enum": [ + "attached" + ] + } + }, + "required": [ + "instance", + "state" + ] + }, + { + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "destroyed" + ] + } + }, + "required": [ + "state" + ] + }, + { + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "faulted" + ] + } + }, + "required": [ + "state" + ] + } + ] + }, + "DiskVariant": { + "type": "string", + "enum": [ + "U2", + "M2" + ] + }, + "DlpiNetworkBackend": { + "description": "A network backend associated with a DLPI VNIC on the host.", + "type": "object", + "properties": { + "vnic_name": { + "description": "The name of the VNIC to use as a backend.", + "type": "string" + } + }, + "required": [ + "vnic_name" + ], + "additionalProperties": false + }, + "Duration": { + "type": "object", + "properties": { + "nanos": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "secs": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "nanos", + "secs" + ] + }, + "EarlyNetworkConfig": { + "description": "Network configuration required to bring up the control plane\n\nThe fields in this structure are those from `RackInitializeRequest` necessary for use beyond RSS. This is just for the initial rack configuration and cold boot purposes. Updates come from Nexus.", + "type": "object", + "properties": { + "body": { + "$ref": "#/components/schemas/EarlyNetworkConfigBody" + }, + "generation": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "schema_version": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "body", + "generation", + "schema_version" + ] + }, + "EarlyNetworkConfigBody": { + "description": "This is the actual configuration of EarlyNetworking.\n\nWe nest it below the \"header\" of `generation` and `schema_version` so that we can perform partial deserialization of `EarlyNetworkConfig` to only read the header and defer deserialization of the body once we know the schema version. This is possible via the use of [`serde_json::value::RawValue`] in future (post-v1) deserialization paths.", + "type": "object", + "properties": { + "ntp_servers": { + "description": "The external NTP server addresses.", + "type": "array", + "items": { + "type": "string" + } + }, + "rack_network_config": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/RackNetworkConfig" + } + ] + } + }, + "required": [ + "ntp_servers" + ] + }, + "EncryptedRackSecrets": { + "description": "All possibly relevant __encrypted__ rack secrets for _prior_ committed configurations.", + "type": "object", + "properties": { + "data": { + "description": "Encrypted data.", + "type": "string", + "format": "hex string" + }, + "salt": { + "description": "A random value used to derive the key to encrypt the rack secrets for prior committed epochs.", + "type": "string", + "format": "hex string (32 bytes)" + } + }, + "required": [ + "data", + "salt" + ] + }, + "Error": { + "description": "Error information from a response.", + "type": "object", + "properties": { + "error_code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "request_id": { + "type": "string" + } + }, + "required": [ + "message", + "request_id" + ] + }, + "EstablishedConnection": { + "description": "An established connection to a bootstore peer.", + "type": "object", + "properties": { + "addr": { + "type": "string" + }, + "baseboard": { + "$ref": "#/components/schemas/Baseboard" + } + }, + "required": [ + "addr", + "baseboard" + ] + }, + "ExpungedMetadata": { + "description": "Metadata about a node being expunged from the trust quorum.", + "type": "object", + "properties": { + "epoch": { + "description": "The committed epoch, later than its current configuration at which the node learned that it had been expunged.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "from": { + "description": "Which node this commit information was learned from.", + "allOf": [ + { + "$ref": "#/components/schemas/BaseboardId" + } + ] + } + }, + "required": [ + "epoch", + "from" + ] + }, + "ExternalIp": { + "description": "An external IP address used by a probe.", + "type": "object", + "properties": { + "first_port": { + "description": "The first port used by the address.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "ip": { + "description": "The external IP address.", + "type": "string", + "format": "ip" + }, + "kind": { + "description": "The kind of address this is.", + "allOf": [ + { + "$ref": "#/components/schemas/IpKind" + } + ] + }, + "last_port": { + "description": "The last port used by the address.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "first_port", + "ip", + "kind", + "last_port" + ] + }, + "ExternalIpConfig": { + "description": "A single- or dual-stack external IP configuration.", + "oneOf": [ + { + "description": "Single-stack IPv4 external IP configuration.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "v4" + ] + }, + "value": { + "$ref": "#/components/schemas/ExternalIpv4Config" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "Single-stack IPv6 external IP configuration.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "v6" + ] + }, + "value": { + "$ref": "#/components/schemas/ExternalIpv6Config" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "Both IPv4 and IPv6 external IP configuration.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "dual_stack" + ] + }, + "value": { + "type": "object", + "properties": { + "v4": { + "$ref": "#/components/schemas/ExternalIpv4Config" + }, + "v6": { + "$ref": "#/components/schemas/ExternalIpv6Config" + } + }, + "required": [ + "v4", + "v6" + ] + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "ExternalIpGatewayMap": { + "description": "Per-NIC mappings from external IP addresses to the Internet Gateways which can choose them as a source.", + "type": "object", + "properties": { + "mappings": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "uniqueItems": true + } + } + } + }, + "required": [ + "mappings" + ] + }, + "ExternalIpv4Config": { + "description": "External IP address configuration.\n\nThis encapsulates all the external addresses of a single IP version, including source NAT, Ephemeral, and Floating IPs. Note that not all of these need to be specified, but this type can only be constructed if _at least one_ of them is.", + "type": "object", + "properties": { + "ephemeral_ip": { + "nullable": true, + "description": "An Ephemeral address for in- and outbound connectivity.", + "type": "string", + "format": "ipv4" + }, + "floating_ips": { + "description": "Additional Floating IPs for in- and outbound connectivity.", + "type": "array", + "items": { + "type": "string", + "format": "ipv4" + } + }, + "source_nat": { + "nullable": true, + "description": "Source NAT configuration, for outbound-only connectivity.", + "allOf": [ + { + "$ref": "#/components/schemas/SourceNatConfigV4" + } + ] + } + }, + "required": [ + "floating_ips" + ] + }, + "ExternalIpv6Config": { + "description": "External IP address configuration.\n\nThis encapsulates all the external addresses of a single IP version, including source NAT, Ephemeral, and Floating IPs. Note that not all of these need to be specified, but this type can only be constructed if _at least one_ of them is.", + "type": "object", + "properties": { + "ephemeral_ip": { + "nullable": true, + "description": "An Ephemeral address for in- and outbound connectivity.", + "type": "string", + "format": "ipv6" + }, + "floating_ips": { + "description": "Additional Floating IPs for in- and outbound connectivity.", + "type": "array", + "items": { + "type": "string", + "format": "ipv6" + } + }, + "source_nat": { + "nullable": true, + "description": "Source NAT configuration, for outbound-only connectivity.", + "allOf": [ + { + "$ref": "#/components/schemas/SourceNatConfigV6" + } + ] + } + }, + "required": [ + "floating_ips" + ] + }, + "ExternalZpoolUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::ExternalZpoolUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "FileStorageBackend": { + "description": "A storage backend backed by a file in the host system's file system.", + "type": "object", + "properties": { + "block_size": { + "description": "Block size of the backend", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "path": { + "description": "A path to a file that backs a disk.", + "type": "string" + }, + "readonly": { + "description": "Indicates whether the storage is read-only.", + "type": "boolean" + }, + "workers": { + "nullable": true, + "description": "Optional worker threads for the file backend, exposed for testing only.", + "type": "integer", + "format": "uint", + "minimum": 1 + } + }, + "required": [ + "block_size", + "path", + "readonly" + ], + "additionalProperties": false + }, + "Generation": { + "description": "Generation numbers stored in the database, used for optimistic concurrency control", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "GuestHypervisorInterface": { + "description": "A hypervisor interface to expose to the guest.", + "oneOf": [ + { + "description": "Expose a bhyve-like interface (\"bhyve bhyve \" as the hypervisor ID in leaf 0x4000_0000 and no additional leaves or features).", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "bhyve" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + { + "description": "Expose a Hyper-V-compatible hypervisor interface with the supplied features enabled.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "hyper_v" + ] + }, + "value": { + "type": "object", + "properties": { + "features": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HyperVFeatureFlag" + }, + "uniqueItems": true + } + }, + "required": [ + "features" + ], + "additionalProperties": false + } + }, + "required": [ + "type", + "value" + ], + "additionalProperties": false + } + ] + }, + "GzipLevel": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "HealthMonitorInventory": { + "description": "Fields of sled-agent inventory reported by the health monitor subsystem.", + "type": "object", + "properties": { + "smf_services_in_maintenance": { + "x-rust-type": { + "crate": "std", + "parameters": [ + { + "$ref": "#/components/schemas/SvcsInMaintenanceResult" + }, + { + "type": "string" + } + ], + "path": "::std::result::Result", + "version": "*" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "ok": { + "$ref": "#/components/schemas/SvcsInMaintenanceResult" + } + }, + "required": [ + "ok" + ] + }, + { + "type": "object", + "properties": { + "err": { + "type": "string" + } + }, + "required": [ + "err" + ] + } + ] + } + }, + "required": [ + "smf_services_in_maintenance" + ] + }, + "HostIdentifier": { + "description": "A `HostIdentifier` represents either an IP host or network (v4 or v6), or an entire VPC (identified by its VNI). It is used in firewall rule host filters.", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ip" + ] + }, + "value": { + "$ref": "#/components/schemas/IpNet" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "vpc" + ] + }, + "value": { + "$ref": "#/components/schemas/Vni" + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "HostPhase2DesiredContents": { + "description": "Describes the desired contents of a host phase 2 slot (i.e., the boot partition on one of the internal M.2 drives).", + "oneOf": [ + { + "description": "Do not change the current contents.\n\nWe use this value when we've detected a sled has been mupdated (and we don't want to overwrite phase 2 images until we understand how to recover from that mupdate) and as the default value when reading an [`OmicronSledConfig`] that was ledgered before this concept existed.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "current_contents" + ] + } + }, + "required": [ + "type" + ] + }, + { + "description": "Set the phase 2 slot to the given artifact.\n\nThe artifact will come from an unpacked and distributed TUF repo.", + "type": "object", + "properties": { + "hash": { + "type": "string", + "format": "hex string (32 bytes)" + }, + "type": { + "type": "string", + "enum": [ + "artifact" + ] + } + }, + "required": [ + "hash", + "type" + ] + } + ] + }, + "HostPhase2DesiredSlots": { + "description": "Describes the desired contents for both host phase 2 slots.", + "type": "object", + "properties": { + "slot_a": { + "$ref": "#/components/schemas/HostPhase2DesiredContents" + }, + "slot_b": { + "$ref": "#/components/schemas/HostPhase2DesiredContents" + } + }, + "required": [ + "slot_a", + "slot_b" + ] + }, + "HostPortConfig": { + "type": "object", + "properties": { + "addrs": { + "description": "IP Address and prefix (e.g., `192.168.0.1/16`) to apply to switchport (must be in infra_ip pool). May also include an optional VLAN ID.", + "type": "array", + "items": { + "$ref": "#/components/schemas/UplinkAddressConfig" + } + }, + "lldp": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/LldpPortConfig" + } + ] + }, + "port": { + "description": "Switchport to use for external connectivity", + "type": "string" + }, + "tx_eq": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/TxEqConfig" + } + ] + } + }, + "required": [ + "addrs", + "port" + ] + }, + "Hostname": { + "title": "An RFC-1035-compliant hostname", + "description": "A hostname identifies a host on a network, and is usually a dot-delimited sequence of labels, where each label contains only letters, digits, or the hyphen. See RFCs 1035 and 952 for more details.", + "type": "string", + "pattern": "^([a-zA-Z0-9]+[a-zA-Z0-9\\-]*(? for background.", + "oneOf": [ + { + "description": "Start the switch zone if a switch is present.\n\nThis is the default policy.", + "type": "object", + "properties": { + "policy": { + "type": "string", + "enum": [ + "start_if_switch_present" + ] + } + }, + "required": [ + "policy" + ] + }, + { + "description": "Even if a switch zone is present, stop the switch zone.", + "type": "object", + "properties": { + "policy": { + "type": "string", + "enum": [ + "stop_despite_switch_presence" + ] + } + }, + "required": [ + "policy" + ] + } + ] + }, + "OrphanedDataset": { + "type": "object", + "properties": { + "available": { + "$ref": "#/components/schemas/ByteCount" + }, + "id": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/DatasetUuid" + } + ] + }, + "mounted": { + "type": "boolean" + }, + "name": { + "$ref": "#/components/schemas/DatasetName" + }, + "reason": { + "type": "string" + }, + "used": { + "$ref": "#/components/schemas/ByteCount" + } + }, + "required": [ + "available", + "mounted", + "name", + "reason", + "used" + ] + }, + "P9fs": { + "description": "Describes a filesystem to expose through a P9 device.\n\nThis is only supported by Propolis servers compiled with the `falcon` feature.", + "type": "object", + "properties": { + "chunk_size": { + "description": "The chunk size to use in the 9P protocol. Vanilla Helios images should use 8192. Falcon Helios base images and Linux can use up to 65536.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "pci_path": { + "description": "The PCI path at which to attach the guest to this P9 filesystem.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + }, + "source": { + "description": "The host source path to mount into the guest.", + "type": "string" + }, + "target": { + "description": "The 9P target filesystem tag.", + "type": "string" + } + }, + "required": [ + "chunk_size", + "pci_path", + "source", + "target" + ], + "additionalProperties": false + }, + "PciPath": { + "description": "A PCI bus/device/function tuple.", + "type": "object", + "properties": { + "bus": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "device": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "function": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "bus", + "device", + "function" + ] + }, + "PciPciBridge": { + "description": "A PCI-PCI bridge.", + "type": "object", + "properties": { + "downstream_bus": { + "description": "The logical bus number of this bridge's downstream bus. Other devices may use this bus number in their PCI paths to indicate they should be attached to this bridge's bus.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "pci_path": { + "description": "The PCI path at which to attach this bridge.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + } + }, + "required": [ + "downstream_bus", + "pci_path" + ], + "additionalProperties": false + }, + "PhysicalDiskUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::PhysicalDiskUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "PortConfig": { + "type": "object", + "properties": { + "addresses": { + "description": "This port's addresses and optional vlan IDs", + "type": "array", + "items": { + "$ref": "#/components/schemas/UplinkAddressConfig" + } + }, + "autoneg": { + "description": "Whether or not to set autonegotiation", + "default": false, + "type": "boolean" + }, + "bgp_peers": { + "description": "BGP peers on this port", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpPeerConfig" + } + }, + "lldp": { + "nullable": true, + "description": "LLDP configuration for this port", + "allOf": [ + { + "$ref": "#/components/schemas/LldpPortConfig" + } + ] + }, + "port": { + "description": "Nmae of the port this config applies to.", + "type": "string" + }, + "routes": { + "description": "The set of routes associated with this port.", + "type": "array", + "items": { + "$ref": "#/components/schemas/RouteConfig" + } + }, + "switch": { + "description": "Switch the port belongs to.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchLocation" + } + ] + }, + "tx_eq": { + "nullable": true, + "description": "TX-EQ configuration for this port", + "allOf": [ + { + "$ref": "#/components/schemas/TxEqConfig" + } + ] + }, + "uplink_port_fec": { + "nullable": true, + "description": "Port forward error correction type.", + "allOf": [ + { + "$ref": "#/components/schemas/PortFec" + } + ] + }, + "uplink_port_speed": { + "description": "Port speed.", + "allOf": [ + { + "$ref": "#/components/schemas/PortSpeed" + } + ] + } + }, + "required": [ + "addresses", + "bgp_peers", + "port", + "routes", + "switch", + "uplink_port_speed" + ] + }, + "PortFec": { + "description": "Switchport FEC options", + "type": "string", + "enum": [ + "firecode", + "none", + "rs" + ] + }, + "PortSpeed": { + "description": "Switchport Speed options", + "type": "string", + "enum": [ + "speed0_g", + "speed1_g", + "speed10_g", + "speed25_g", + "speed40_g", + "speed50_g", + "speed100_g", + "speed200_g", + "speed400_g" + ] + }, + "PrepareAndCommitRequest": { + "description": "Request to prepare and commit a trust quorum configuration.\n\nThis is the `Configuration` sent to a node that missed the `Prepare` phase.", + "type": "object", + "properties": { + "config": { + "$ref": "#/components/schemas/Configuration" + } + }, + "required": [ + "config" + ] + }, + "PriorityDimension": { + "description": "A dimension along with bundles can be sorted, to determine priority.", + "oneOf": [ + { + "description": "Sorting by time, with older bundles with lower priority.", + "type": "string", + "enum": [ + "time" + ] + }, + { + "description": "Sorting by the cause for creating the bundle.", + "type": "string", + "enum": [ + "cause" + ] + } + ] + }, + "PriorityOrder": { + "description": "The priority order for bundles during cleanup.\n\nBundles are sorted along the dimensions in [`PriorityDimension`], with each dimension appearing exactly once. During cleanup, lesser-priority bundles are pruned first, to maintain the dataset quota. Note that bundles are sorted by each dimension in the order in which they appear, with each dimension having higher priority than the next.\n\nTODO: The serde deserializer does not currently verify uniqueness of dimensions.", + "type": "array", + "items": { + "$ref": "#/components/schemas/PriorityDimension" + }, + "minItems": 2, + "maxItems": 2 + }, + "PrivateIpConfig": { + "description": "VPC-private IP address configuration for a network interface.", + "oneOf": [ + { + "description": "The interface has only an IPv4 configuration.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "v4" + ] + }, + "value": { + "$ref": "#/components/schemas/PrivateIpv4Config" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The interface has only an IPv6 configuration.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "v6" + ] + }, + "value": { + "$ref": "#/components/schemas/PrivateIpv6Config" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The interface is dual-stack.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "dual_stack" + ] + }, + "value": { + "type": "object", + "properties": { + "v4": { + "description": "The interface's IPv4 configuration.", + "allOf": [ + { + "$ref": "#/components/schemas/PrivateIpv4Config" + } + ] + }, + "v6": { + "description": "The interface's IPv6 configuration.", + "allOf": [ + { + "$ref": "#/components/schemas/PrivateIpv6Config" + } + ] + } + }, + "required": [ + "v4", + "v6" + ] + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "PrivateIpv4Config": { + "description": "VPC-private IPv4 configuration for a network interface.", + "type": "object", + "properties": { + "ip": { + "description": "VPC-private IP address.", + "type": "string", + "format": "ipv4" + }, + "subnet": { + "description": "The IP subnet.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4Net" + } + ] + }, + "transit_ips": { + "description": "Additional networks on which the interface can send / receive traffic.", + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv4Net" + } + } + }, + "required": [ + "ip", + "subnet" + ] + }, + "PrivateIpv6Config": { + "description": "VPC-private IPv6 configuration for a network interface.", + "type": "object", + "properties": { + "ip": { + "description": "VPC-private IP address.", + "type": "string", + "format": "ipv6" + }, + "subnet": { + "description": "The IP subnet.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Net" + } + ] + }, + "transit_ips": { + "description": "Additional networks on which the interface can send / receive traffic.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv6Net" + } + } + }, + "required": [ + "ip", + "subnet", + "transit_ips" + ] + }, + "ProbeCreate": { + "description": "Parameters used to create a probe.", + "type": "object", + "properties": { + "external_ips": { + "description": "The external IP addresses assigned to the probe.", + "type": "array", + "items": { + "$ref": "#/components/schemas/ExternalIp" + } + }, + "id": { + "description": "The ID for the probe.", + "allOf": [ + { + "$ref": "#/components/schemas/ProbeUuid" + } + ] + }, + "interface": { + "description": "The probe's networking interface.", + "allOf": [ + { + "$ref": "#/components/schemas/NetworkInterface" + } + ] + } + }, + "required": [ + "external_ips", + "id", + "interface" + ] + }, + "ProbeSet": { + "description": "A set of probes that the target sled should run.", + "type": "object", + "properties": { + "probes": { + "title": "IdHashMap", + "description": "The exact set of probes to run.", + "x-rust-type": { + "crate": "iddqd", + "parameters": [ + { + "$ref": "#/components/schemas/ProbeCreate" + } + ], + "path": "iddqd::IdHashMap", + "version": "*" + }, + "type": "array", + "items": { + "$ref": "#/components/schemas/ProbeCreate" + }, + "uniqueItems": true + } + }, + "required": [ + "probes" + ] + }, + "ProbeUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::ProbeUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "ProxyCommitRequest": { + "description": "Request to proxy a commit operation to another trust quorum node.", + "type": "object", + "properties": { + "destination": { + "description": "The target node to proxy the request to.", + "allOf": [ + { + "$ref": "#/components/schemas/BaseboardId" + } + ] + }, + "request": { + "description": "The commit request to proxy.", + "allOf": [ + { + "$ref": "#/components/schemas/CommitRequest" + } + ] + } + }, + "required": [ + "destination", + "request" + ] + }, + "ProxyPrepareAndCommitRequest": { + "description": "Request to proxy a prepare-and-commit operation to another trust quorum node.", + "type": "object", + "properties": { + "destination": { + "description": "The target node to proxy the request to.", + "allOf": [ + { + "$ref": "#/components/schemas/BaseboardId" + } + ] + }, + "request": { + "description": "The prepare-and-commit request to proxy.", + "allOf": [ + { + "$ref": "#/components/schemas/PrepareAndCommitRequest" + } + ] + } + }, + "required": [ + "destination", + "request" + ] + }, + "QemuPvpanic": { + "type": "object", + "properties": { + "enable_isa": { + "description": "Enable the QEMU PVPANIC ISA bus device (I/O port 0x505).", + "type": "boolean" + } + }, + "required": [ + "enable_isa" + ], + "additionalProperties": false + }, + "RackNetworkConfig": { + "description": "Initial network configuration", + "type": "object", + "properties": { + "bfd": { + "description": "BFD configuration for connecting the rack to external networks", + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/BfdPeerConfig" + } + }, + "bgp": { + "description": "BGP configurations for connecting the rack to external networks", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpConfig" + } + }, + "infra_ip_first": { + "description": "First ip address to be used for configuring network infrastructure", + "type": "string", + "format": "ipv4" + }, + "infra_ip_last": { + "description": "Last ip address to be used for configuring network infrastructure", + "type": "string", + "format": "ipv4" + }, + "ports": { + "description": "Uplinks for connecting the rack to external networks", + "type": "array", + "items": { + "$ref": "#/components/schemas/PortConfig" + } + }, + "rack_subnet": { + "$ref": "#/components/schemas/Ipv6Net" + } + }, + "required": [ + "bgp", + "infra_ip_first", + "infra_ip_last", + "ports", + "rack_subnet" + ] + }, + "RackSecretReconstructError": { + "description": "Error reconstructing a rack secret from shares.", + "oneOf": [ + { + "type": "object", + "properties": { + "combine": { + "$ref": "#/components/schemas/CombineError" + } + }, + "required": [ + "combine" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "size": { + "$ref": "#/components/schemas/InvalidRackSecretSizeError" + } + }, + "required": [ + "size" + ], + "additionalProperties": false + } + ] + }, + "RackUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::RackUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "ReconfigureMsg": { + "description": "A request from Nexus informing a node to start coordinating a reconfiguration.", + "type": "object", + "properties": { + "epoch": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "last_committed_epoch": { + "nullable": true, + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseboardId" + }, + "uniqueItems": true + }, + "rack_id": { + "$ref": "#/components/schemas/RackUuid" + }, + "threshold": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "epoch", + "members", + "rack_id", + "threshold" + ] + }, + "RemoveMupdateOverrideBootSuccessInventory": { + "description": "Status of removing the mupdate override on the boot disk.", + "oneOf": [ + { + "description": "The mupdate override was successfully removed.", + "type": "string", + "enum": [ + "removed" + ] + }, + { + "description": "No mupdate override was found.\n\nThis is considered a success for idempotency reasons.", + "type": "string", + "enum": [ + "no_override" + ] + } + ] + }, + "RemoveMupdateOverrideInventory": { + "description": "Status of removing the mupdate override in the inventory.", + "type": "object", + "properties": { + "boot_disk_result": { + "description": "The result of removing the mupdate override on the boot disk.", + "x-rust-type": { + "crate": "std", + "parameters": [ + { + "$ref": "#/components/schemas/RemoveMupdateOverrideBootSuccessInventory" + }, + { + "type": "string" + } + ], + "path": "::std::result::Result", + "version": "*" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "ok": { + "$ref": "#/components/schemas/RemoveMupdateOverrideBootSuccessInventory" + } + }, + "required": [ + "ok" + ] + }, + { + "type": "object", + "properties": { + "err": { + "type": "string" + } + }, + "required": [ + "err" + ] + } + ] + }, + "non_boot_message": { + "description": "What happened on non-boot disks.\n\nWe aren't modeling this out in more detail, because we plan to not try and keep ledgered data in sync across both disks in the future.", + "type": "string" + } + }, + "required": [ + "boot_disk_result", + "non_boot_message" + ] + }, + "ResolvedVpcFirewallRule": { + "description": "VPC firewall rule after object name resolution has been performed by Nexus", + "type": "object", + "properties": { + "action": { + "$ref": "#/components/schemas/VpcFirewallRuleAction" + }, + "direction": { + "$ref": "#/components/schemas/VpcFirewallRuleDirection" + }, + "filter_hosts": { + "nullable": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/HostIdentifier" + }, + "uniqueItems": true + }, + "filter_ports": { + "nullable": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/L4PortRange" + } + }, + "filter_protocols": { + "nullable": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/VpcFirewallRuleProtocol" + } + }, + "priority": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "status": { + "$ref": "#/components/schemas/VpcFirewallRuleStatus" + }, + "targets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NetworkInterface" + } + } + }, + "required": [ + "action", + "direction", + "priority", + "status", + "targets" + ] + }, + "ResolvedVpcRoute": { + "description": "A VPC route resolved into a concrete target.", + "type": "object", + "properties": { + "dest": { + "$ref": "#/components/schemas/IpNet" + }, + "target": { + "$ref": "#/components/schemas/RouterTarget" + } + }, + "required": [ + "dest", + "target" + ] + }, + "ResolvedVpcRouteSet": { + "description": "An updated set of routes for a given VPC and/or subnet.", + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/RouterId" + }, + "routes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResolvedVpcRoute" + }, + "uniqueItems": true + }, + "version": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/RouterVersion" + } + ] + } + }, + "required": [ + "id", + "routes" + ] + }, + "ResolvedVpcRouteState": { + "description": "Version information for routes on a given VPC subnet.", + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/RouterId" + }, + "version": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/RouterVersion" + } + ] + } + }, + "required": [ + "id" + ] + }, + "RouteConfig": { + "type": "object", + "properties": { + "destination": { + "description": "The destination of the route.", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + }, + "nexthop": { + "description": "The nexthop/gateway address.", + "type": "string", + "format": "ip" + }, + "rib_priority": { + "nullable": true, + "description": "The RIB priority (i.e. Admin Distance) associated with this route.", + "default": null, + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "vlan_id": { + "nullable": true, + "description": "The VLAN id associated with this route.", + "default": null, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "destination", + "nexthop" + ] + }, + "RouterId": { + "description": "Identifier for a VPC and/or subnet.", + "type": "object", + "properties": { + "kind": { + "$ref": "#/components/schemas/RouterKind" + }, + "vni": { + "$ref": "#/components/schemas/Vni" + } + }, + "required": [ + "kind", + "vni" + ] + }, + "RouterKind": { + "description": "The scope of a set of VPC router rules.", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "system" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "subnet": { + "$ref": "#/components/schemas/IpNet" + }, + "type": { + "type": "string", + "enum": [ + "custom" + ] + } + }, + "required": [ + "subnet", + "type" + ] + } + ] + }, + "RouterTarget": { + "description": "The target for a given router entry.", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "drop" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "internet_gateway" + ] + }, + "value": { + "$ref": "#/components/schemas/InternetGatewayRouterTarget" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ip" + ] + }, + "value": { + "type": "string", + "format": "ip" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "vpc_subnet" + ] + }, + "value": { + "$ref": "#/components/schemas/IpNet" + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "RouterVersion": { + "description": "Information on the current parent router (and version) of a route set according to the control plane.", + "type": "object", + "properties": { + "router_id": { + "type": "string", + "format": "uuid" + }, + "version": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "router_id", + "version" + ] + }, + "SerialPort": { + "description": "A serial port device.", + "type": "object", + "properties": { + "num": { + "description": "The serial port number for this port.", + "allOf": [ + { + "$ref": "#/components/schemas/SerialPortNumber" + } + ] + } + }, + "required": [ + "num" + ], + "additionalProperties": false + }, + "SerialPortNumber": { + "description": "A serial port identifier, which determines what I/O ports a guest can use to access a port.", + "type": "string", + "enum": [ + "com1", + "com2", + "com3", + "com4" + ] + }, + "SledCpuFamily": { + "description": "Identifies the kind of CPU present on a sled, determined by reading CPUID.\n\nThis is intended to broadly support the control plane answering the question \"can I run this instance on that sled?\" given an instance with either no or some CPU platform requirement. It is not enough information for more precise placement questions - for example, is a CPU a high-frequency part or many-core part? We don't include Genoa here, but in that CPU family there are high frequency parts, many-core parts, and large-cache parts. To support those questions (or satisfactorily answer #8730) we would need to collect additional information and send it along.", + "oneOf": [ + { + "description": "The CPU vendor or its family number don't correspond to any of the known family variants.", + "type": "string", + "enum": [ + "unknown" + ] + }, + { + "description": "AMD Milan processors (or very close). Could be an actual Milan in a Gimlet, a close-to-Milan client Zen 3 part, or Zen 4 (for which Milan is the greatest common denominator).", + "type": "string", + "enum": [ + "amd_milan" + ] + }, + { + "description": "AMD Turin processors (or very close). Could be an actual Turin in a Cosmo, or a close-to-Turin client Zen 5 part.", + "type": "string", + "enum": [ + "amd_turin" + ] + }, + { + "description": "AMD Turin Dense processors. There are no \"Turin Dense-like\" CPUs unlike other cases, so this means a bona fide Zen 5c Turin Dense part.", + "type": "string", + "enum": [ + "amd_turin_dense" + ] + } + ] + }, + "SledDiagnosticsQueryOutput": { + "oneOf": [ + { + "type": "object", + "properties": { + "success": { + "type": "object", + "properties": { + "command": { + "description": "The command and its arguments.", + "type": "string" + }, + "exit_code": { + "nullable": true, + "description": "The exit code if one was present when the command exited.", + "type": "integer", + "format": "int32" + }, + "exit_status": { + "description": "The exit status of the command. This will be the exit code (if any) and exit reason such as from a signal.", + "type": "string" + }, + "stdio": { + "description": "Any stdout/stderr produced by the command.", + "type": "string" + } + }, + "required": [ + "command", + "exit_status", + "stdio" + ] + } + }, + "required": [ + "success" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "failure": { + "type": "object", + "properties": { + "error": { + "description": "The reason the command failed to execute.", + "type": "string" + } + }, + "required": [ + "error" + ] + } + }, + "required": [ + "failure" + ], + "additionalProperties": false + } + ] + }, + "SledIdentifiers": { + "description": "Identifiers for a single sled.\n\nThis is intended primarily to be used in timeseries, to identify sled from which metric data originates.", + "type": "object", + "properties": { + "model": { + "description": "Model name of the sled", + "type": "string" + }, + "rack_id": { + "description": "Control plane ID of the rack this sled is a member of", + "type": "string", + "format": "uuid" + }, + "revision": { + "description": "Revision number of the sled", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "serial": { + "description": "Serial number of the sled", + "type": "string" + }, + "sled_id": { + "description": "Control plane ID for the sled itself", + "type": "string", + "format": "uuid" + } + }, + "required": [ + "model", + "rack_id", + "revision", + "serial", + "sled_id" + ] + }, + "SledRole": { + "description": "Describes the role of the sled within the rack.\n\nNote that this may change if the sled is physically moved within the rack.", + "oneOf": [ + { + "description": "The sled is a general compute sled.", + "type": "string", + "enum": [ + "gimlet" + ] + }, + { + "description": "The sled is attached to the network switch, and has additional responsibilities.", + "type": "string", + "enum": [ + "scrimlet" + ] + } + ] + }, + "SledUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::SledUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "SledVmmState": { + "description": "A wrapper type containing a sled's total knowledge of the state of a VMM.", + "type": "object", + "properties": { + "migration_in": { + "nullable": true, + "description": "The current state of any inbound migration to this VMM.", + "allOf": [ + { + "$ref": "#/components/schemas/MigrationRuntimeState" + } + ] + }, + "migration_out": { + "nullable": true, + "description": "The state of any outbound migration from this VMM.", + "allOf": [ + { + "$ref": "#/components/schemas/MigrationRuntimeState" + } + ] + }, + "vmm_state": { + "description": "The most recent state of the sled's VMM process.", + "allOf": [ + { + "$ref": "#/components/schemas/VmmRuntimeState" + } + ] + } + }, + "required": [ + "vmm_state" + ] + }, + "SoftNpuP9": { + "description": "Describes a PCI device that shares host files with the guest using the P9 protocol.\n\nThis is only supported by Propolis servers compiled with the `falcon` feature.", + "type": "object", + "properties": { + "pci_path": { + "description": "The PCI path at which to attach the guest to this port.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + } + }, + "required": [ + "pci_path" + ], + "additionalProperties": false + }, + "SoftNpuPciPort": { + "description": "Describes a SoftNPU PCI device.\n\nThis is only supported by Propolis servers compiled with the `falcon` feature.", + "type": "object", + "properties": { + "pci_path": { + "description": "The PCI path at which to attach the guest to this port.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + } + }, + "required": [ + "pci_path" + ], + "additionalProperties": false + }, + "SoftNpuPort": { + "description": "Describes a port in a SoftNPU emulated ASIC.\n\nThis is only supported by Propolis servers compiled with the `falcon` feature.", + "type": "object", + "properties": { + "backend_id": { + "description": "The name of the port's associated DLPI backend.", + "allOf": [ + { + "$ref": "#/components/schemas/SpecKey" + } + ] + }, + "link_name": { + "description": "The data link name for this port.", + "type": "string" + } + }, + "required": [ + "backend_id", + "link_name" + ], + "additionalProperties": false + }, + "SourceNatConfigGeneric": { + "description": "An IP address and port range used for source NAT, i.e., making outbound network connections from guests or services.", + "type": "object", + "properties": { + "first_port": { + "description": "The first port used for source NAT, inclusive.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "ip": { + "description": "The external address provided to the instance or service.", + "type": "string", + "format": "ip" + }, + "last_port": { + "description": "The last port used for source NAT, also inclusive.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "first_port", + "ip", + "last_port" + ] + }, + "SourceNatConfigV4": { + "description": "An IP address and port range used for source NAT, i.e., making outbound network connections from guests or services.", + "type": "object", + "properties": { + "first_port": { + "description": "The first port used for source NAT, inclusive.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "ip": { + "description": "The external address provided to the instance or service.", + "type": "string", + "format": "ipv4" + }, + "last_port": { + "description": "The last port used for source NAT, also inclusive.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "first_port", + "ip", + "last_port" + ] + }, + "SourceNatConfigV6": { + "description": "An IP address and port range used for source NAT, i.e., making outbound network connections from guests or services.", + "type": "object", + "properties": { + "first_port": { + "description": "The first port used for source NAT, inclusive.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "ip": { + "description": "The external address provided to the instance or service.", + "type": "string", + "format": "ipv6" + }, + "last_port": { + "description": "The last port used for source NAT, also inclusive.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "first_port", + "ip", + "last_port" + ] + }, + "SpecKey": { + "description": "A key identifying a component in an instance spec.", + "oneOf": [ + { + "title": "uuid", + "allOf": [ + { + "type": "string", + "format": "uuid" + } + ] + }, + { + "title": "name", + "allOf": [ + { + "type": "string" + } + ] + } + ] + }, + "StartSledAgentRequest": { + "description": "Configuration information for launching a Sled Agent.", + "type": "object", + "properties": { + "body": { + "$ref": "#/components/schemas/StartSledAgentRequestBody" + }, + "generation": { + "description": "The current generation number of data as stored in CRDB.\n\nThe initial generation is set during RSS time and then only mutated by Nexus. For now, we don't actually anticipate mutating this data, but we leave open the possiblity.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "schema_version": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "body", + "generation", + "schema_version" + ] + }, + "StartSledAgentRequestBody": { + "description": "This is the actual app level data of `StartSledAgentRequest`\n\nWe nest it below the \"header\" of `generation` and `schema_version` so that we can perform partial deserialization of `EarlyNetworkConfig` to only read the header and defer deserialization of the body once we know the schema version. This is possible via the use of [`serde_json::value::RawValue`] in future (post-v1) deserialization paths.", + "type": "object", + "properties": { + "id": { + "description": "Uuid of the Sled Agent to be created.", + "allOf": [ + { + "$ref": "#/components/schemas/SledUuid" + } + ] + }, + "is_lrtq_learner": { + "description": "Is this node an LRTQ learner node?\n\nWe only put the node into learner mode if `use_trust_quorum` is also true.", + "type": "boolean" + }, + "rack_id": { + "description": "Uuid of the rack to which this sled agent belongs.", + "type": "string", + "format": "uuid" + }, + "subnet": { + "description": "Portion of the IP space to be managed by the Sled Agent.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Subnet" + } + ] + }, + "use_trust_quorum": { + "description": "Use trust quorum for key generation", + "type": "boolean" + } + }, + "required": [ + "id", + "is_lrtq_learner", + "rack_id", + "subnet", + "use_trust_quorum" + ] + }, + "StorageLimit": { + "description": "The limit on space allowed for zone bundles, as a percentage of the overall dataset's quota.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "SupportBundleMetadata": { + "description": "Metadata about a support bundle.", + "type": "object", + "properties": { + "state": { + "$ref": "#/components/schemas/SupportBundleState" + }, + "support_bundle_id": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + "required": [ + "state", + "support_bundle_id" + ] + }, + "SupportBundleState": { + "description": "State of a support bundle.", + "type": "string", + "enum": [ + "complete", + "incomplete" + ] + }, + "SupportBundleUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::SupportBundleUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "SvcInMaintenance": { + "description": "Information about an SMF service that is enabled but not running", + "type": "object", + "properties": { + "fmri": { + "type": "string" + }, + "zone": { + "type": "string" + } + }, + "required": [ + "fmri", + "zone" + ] + }, + "SvcsInMaintenanceResult": { + "description": "Lists services in maintenance status if any, and the time the health check for SMF services ran", + "type": "object", + "properties": { + "errors": { + "type": "array", + "items": { + "type": "string" + } + }, + "services": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SvcInMaintenance" + } + }, + "time_of_status": { + "nullable": true, + "type": "string", + "format": "date-time" + } + }, + "required": [ + "errors", + "services" + ] + }, + "SwitchLocation": { + "description": "Identifies switch physical location", + "oneOf": [ + { + "description": "Switch in upper slot", + "type": "string", + "enum": [ + "switch0" + ] + }, + { + "description": "Switch in lower slot", + "type": "string", + "enum": [ + "switch1" + ] + } + ] + }, + "SwitchPorts": { + "description": "A set of switch uplinks.", + "type": "object", + "properties": { + "uplinks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HostPortConfig" + } + } + }, + "required": [ + "uplinks" + ] + }, + "TxEqConfig": { + "description": "Per-port tx-eq overrides. This can be used to fine-tune the transceiver equalization settings to improve signal integrity.", + "type": "object", + "properties": { + "main": { + "nullable": true, + "description": "Main tap", + "type": "integer", + "format": "int32" + }, + "post1": { + "nullable": true, + "description": "Post-cursor tap1", + "type": "integer", + "format": "int32" + }, + "post2": { + "nullable": true, + "description": "Post-cursor tap2", + "type": "integer", + "format": "int32" + }, + "pre1": { + "nullable": true, + "description": "Pre-cursor tap1", + "type": "integer", + "format": "int32" + }, + "pre2": { + "nullable": true, + "description": "Pre-cursor tap2", + "type": "integer", + "format": "int32" + } + } + }, + "UplinkAddressConfig": { + "type": "object", + "properties": { + "address": { + "$ref": "#/components/schemas/IpNet" + }, + "vlan_id": { + "nullable": true, + "description": "The VLAN id (if any) associated with this address.", + "default": null, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "address" + ] + }, + "VirtioDisk": { + "description": "A disk that presents a virtio-block interface to the guest.", + "type": "object", + "properties": { + "backend_id": { + "description": "The name of the disk's backend component.", + "allOf": [ + { + "$ref": "#/components/schemas/SpecKey" + } + ] + }, + "pci_path": { + "description": "The PCI bus/device/function at which this disk should be attached.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + } + }, + "required": [ + "backend_id", + "pci_path" + ], + "additionalProperties": false + }, + "VirtioNetworkBackend": { + "description": "A network backend associated with a virtio-net (viona) VNIC on the host.", + "type": "object", + "properties": { + "vnic_name": { + "description": "The name of the viona VNIC to use as a backend.", + "type": "string" + } + }, + "required": [ + "vnic_name" + ], + "additionalProperties": false + }, + "VirtioNic": { + "description": "A network card that presents a virtio-net interface to the guest.", + "type": "object", + "properties": { + "backend_id": { + "description": "The name of the device's backend.", + "allOf": [ + { + "$ref": "#/components/schemas/SpecKey" + } + ] + }, + "interface_id": { + "description": "A caller-defined correlation identifier for this interface. If Propolis is configured to collect network interface kstats in its Oximeter metrics, the metric series for this interface will be associated with this identifier.", + "type": "string", + "format": "uuid" + }, + "pci_path": { + "description": "The PCI path at which to attach this device.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + } + }, + "required": [ + "backend_id", + "interface_id", + "pci_path" + ], + "additionalProperties": false + }, + "VirtualNetworkInterfaceHost": { + "description": "A mapping from a virtual NIC to a physical host", + "type": "object", + "properties": { + "physical_host_ip": { + "type": "string", + "format": "ipv6" + }, + "virtual_ip": { + "type": "string", + "format": "ip" + }, + "virtual_mac": { + "$ref": "#/components/schemas/MacAddr" + }, + "vni": { + "$ref": "#/components/schemas/Vni" + } + }, + "required": [ + "physical_host_ip", + "virtual_ip", + "virtual_mac", + "vni" + ] + }, + "VmmIssueDiskSnapshotRequestBody": { + "description": "Request body for VMM disk snapshot requests.", + "type": "object", + "properties": { + "snapshot_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "snapshot_id" + ] + }, + "VmmIssueDiskSnapshotRequestResponse": { + "description": "Response for VMM disk snapshot requests.", + "type": "object", + "properties": { + "snapshot_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "snapshot_id" + ] + }, + "VmmPutStateBody": { + "description": "The body of a request to move a previously-ensured instance into a specific runtime state.", + "type": "object", + "properties": { + "state": { + "description": "The state into which the instance should be driven.", + "allOf": [ + { + "$ref": "#/components/schemas/VmmStateRequested" + } + ] + } + }, + "required": [ + "state" + ] + }, + "VmmPutStateResponse": { + "description": "The response sent from a request to move an instance into a specific runtime state.", + "type": "object", + "properties": { + "updated_runtime": { + "nullable": true, + "description": "The current runtime state of the instance after handling the request to change its state. If the instance's state did not change, this field is `None`.", + "allOf": [ + { + "$ref": "#/components/schemas/SledVmmState" + } + ] + } + } + }, + "VmmRuntimeState": { + "description": "The dynamic runtime properties of an individual VMM process.", + "type": "object", + "properties": { + "gen": { + "description": "The generation number for this VMM's state.", + "allOf": [ + { + "$ref": "#/components/schemas/Generation" + } + ] + }, + "state": { + "description": "The last state reported by this VMM.", + "allOf": [ + { + "$ref": "#/components/schemas/VmmState" + } + ] + }, + "time_updated": { + "description": "Timestamp for the VMM's state.", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "gen", + "state", + "time_updated" + ] + }, + "VmmSpec": { + "description": "Specifies the virtual hardware configuration of a new Propolis VMM in the form of a Propolis instance specification.", + "allOf": [ + { + "$ref": "#/components/schemas/InstanceSpecV0" + } + ] + }, + "VmmState": { + "description": "One of the states that a VMM can be in.", + "oneOf": [ + { + "description": "The VMM is initializing and has not started running guest CPUs yet.", + "type": "string", + "enum": [ + "starting" + ] + }, + { + "description": "The VMM has finished initializing and may be running guest CPUs.", + "type": "string", + "enum": [ + "running" + ] + }, + { + "description": "The VMM is shutting down.", + "type": "string", + "enum": [ + "stopping" + ] + }, + { + "description": "The VMM's guest has stopped, and the guest will not run again, but the VMM process may not have released all of its resources yet.", + "type": "string", + "enum": [ + "stopped" + ] + }, + { + "description": "The VMM is being restarted or its guest OS is rebooting.", + "type": "string", + "enum": [ + "rebooting" + ] + }, + { + "description": "The VMM is part of a live migration.", + "type": "string", + "enum": [ + "migrating" + ] + }, + { + "description": "The VMM process reported an internal failure.", + "type": "string", + "enum": [ + "failed" + ] + }, + { + "description": "The VMM process has been destroyed and its resources have been released.", + "type": "string", + "enum": [ + "destroyed" + ] + } + ] + }, + "VmmStateRequested": { + "description": "Requestable running state of an Instance.\n\nA subset of [`omicron_common::api::external::InstanceState`].", + "oneOf": [ + { + "description": "Run this instance by migrating in from a previous running incarnation of the instance.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "migration_target" + ] + }, + "value": { + "$ref": "#/components/schemas/InstanceMigrationTargetParams" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "Start the instance if it is not already running.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "running" + ] + } + }, + "required": [ + "type" + ] + }, + { + "description": "Stop the instance.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "stopped" + ] + } + }, + "required": [ + "type" + ] + }, + { + "description": "Immediately reset the instance, as though it had stopped and immediately began to run again.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "reboot" + ] + } + }, + "required": [ + "type" + ] + } + ] + }, + "VmmUnregisterResponse": { + "description": "The response sent from a request to unregister an instance.", + "type": "object", + "properties": { + "updated_runtime": { + "nullable": true, + "description": "The current state of the instance after handling the request to unregister it. If the instance's state did not change, this field is `None`.", + "allOf": [ + { + "$ref": "#/components/schemas/SledVmmState" + } + ] + } + } + }, + "Vni": { + "description": "A Geneve Virtual Network Identifier", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "VpcFirewallIcmpFilter": { + "type": "object", + "properties": { + "code": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/IcmpParamRange" + } + ] + }, + "icmp_type": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "icmp_type" + ] + }, + "VpcFirewallRuleAction": { + "type": "string", + "enum": [ + "allow", + "deny" + ] + }, + "VpcFirewallRuleDirection": { + "type": "string", + "enum": [ + "inbound", + "outbound" + ] + }, + "VpcFirewallRuleProtocol": { + "description": "The protocols that may be specified in a firewall rule's filter", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "tcp" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "udp" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "icmp" + ] + }, + "value": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/VpcFirewallIcmpFilter" + } + ] + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "VpcFirewallRuleStatus": { + "type": "string", + "enum": [ + "disabled", + "enabled" + ] + }, + "VpcFirewallRulesEnsureBody": { + "description": "Update firewall rules for a VPC", + "type": "object", + "properties": { + "rules": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResolvedVpcFirewallRule" + } + }, + "vni": { + "$ref": "#/components/schemas/Vni" + } + }, + "required": [ + "rules", + "vni" + ] + }, + "ZoneArtifactInventory": { + "description": "Inventory representation of a single zone artifact on a boot disk.\n\nPart of [`ManifestBootInventory`].", + "type": "object", + "properties": { + "expected_hash": { + "description": "The expected digest of the file's contents.", + "type": "string", + "format": "hex string (32 bytes)" + }, + "expected_size": { + "description": "The expected size of the file, in bytes.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "file_name": { + "description": "The name of the zone file on disk, for example `nexus.tar.gz`. Zone files are always \".tar.gz\".", + "type": "string" + }, + "path": { + "description": "The full path to the zone file.", + "type": "string", + "format": "Utf8PathBuf" + }, + "status": { + "description": "The status of the artifact.\n\nThis is `Ok(())` if the artifact is present and matches the expected size and digest, or an error message if it is missing or does not match.", + "x-rust-type": { + "crate": "std", + "parameters": [ + { + "type": "null" + }, + { + "type": "string" + } + ], + "path": "::std::result::Result", + "version": "*" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "ok": { + "type": "string", + "enum": [ + null + ] + } + }, + "required": [ + "ok" + ] + }, + { + "type": "object", + "properties": { + "err": { + "type": "string" + } + }, + "required": [ + "err" + ] + } + ] + } + }, + "required": [ + "expected_hash", + "expected_size", + "file_name", + "path", + "status" + ] + }, + "ZoneBundleCause": { + "description": "The reason or cause for a zone bundle, i.e., why it was created.", + "oneOf": [ + { + "description": "Some other, unspecified reason.", + "type": "string", + "enum": [ + "other" + ] + }, + { + "description": "A zone bundle taken when a sled agent finds a zone that it does not expect to be running.", + "type": "string", + "enum": [ + "unexpected_zone" + ] + }, + { + "description": "An instance zone was terminated.", + "type": "string", + "enum": [ + "terminated_instance" + ] + } + ] + }, + "ZoneBundleId": { + "description": "An identifier for a zone bundle.", + "type": "object", + "properties": { + "bundle_id": { + "description": "The ID for this bundle itself.", + "type": "string", + "format": "uuid" + }, + "zone_name": { + "description": "The name of the zone this bundle is derived from.", + "type": "string" + } + }, + "required": [ + "bundle_id", + "zone_name" + ] + }, + "ZoneBundleMetadata": { + "description": "Metadata about a zone bundle.", + "type": "object", + "properties": { + "cause": { + "description": "The reason or cause a bundle was created.", + "allOf": [ + { + "$ref": "#/components/schemas/ZoneBundleCause" + } + ] + }, + "id": { + "description": "Identifier for this zone bundle", + "allOf": [ + { + "$ref": "#/components/schemas/ZoneBundleId" + } + ] + }, + "time_created": { + "description": "The time at which this zone bundle was created.", + "type": "string", + "format": "date-time" + }, + "version": { + "description": "A version number for this zone bundle.", + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "cause", + "id", + "time_created", + "version" + ] + }, + "ZoneImageResolverInventory": { + "description": "Inventory representation of zone image resolver status and health.", + "type": "object", + "properties": { + "mupdate_override": { + "description": "The mupdate override status.", + "allOf": [ + { + "$ref": "#/components/schemas/MupdateOverrideInventory" + } + ] + }, + "zone_manifest": { + "description": "The zone manifest status.", + "allOf": [ + { + "$ref": "#/components/schemas/ManifestInventory" + } + ] + } + }, + "required": [ + "mupdate_override", + "zone_manifest" + ] + }, + "ZpoolName": { + "title": "The name of a Zpool", + "description": "Zpool names are of the format ox{i,p}_. They are either Internal or External, and should be unique", + "type": "string", + "pattern": "^ox[ip]_[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "ZpoolUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::ZpoolUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "PropolisUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::PropolisUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + } + }, + "responses": { + "Error": { + "description": "Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } +} diff --git a/openapi/sled-agent/sled-agent-latest.json b/openapi/sled-agent/sled-agent-latest.json index b45ee7839bf..f393c0bb571 120000 --- a/openapi/sled-agent/sled-agent-latest.json +++ b/openapi/sled-agent/sled-agent-latest.json @@ -1 +1 @@ -sled-agent-13.0.0-c4af95.json \ No newline at end of file +sled-agent-14.0.0-6ac051.json \ No newline at end of file diff --git a/openapi/wicketd.json b/openapi/wicketd.json index efcccadcaa1..9135a046f01 100644 --- a/openapi/wicketd.json +++ b/openapi/wicketd.json @@ -7557,7 +7557,7 @@ "addr": { "description": "Address of the peer.", "type": "string", - "format": "ipv4" + "format": "ip" }, "allowed_export": { "description": "Apply export policy to this peer with an allow list.", diff --git a/sled-agent/api/src/lib.rs b/sled-agent/api/src/lib.rs index 9f60a4439ab..bb03669dc35 100644 --- a/sled-agent/api/src/lib.rs +++ b/sled-agent/api/src/lib.rs @@ -34,6 +34,7 @@ api_versions!([ // | example for the next person. // v // (next_int, IDENT), + (14, BGP_V6), (13, ADD_TRUST_QUORUM), (12, ADD_SMF_SERVICES_HEALTH_CHECK), (11, ADD_DUAL_STACK_EXTERNAL_IP_CONFIG), @@ -694,6 +695,7 @@ pub trait SledAgentApi { #[endpoint { method = GET, path = "/network-bootstore-config", + versions = VERSION_BGP_V6.., }] async fn read_network_bootstore_config_cache( rqctx: RequestContext, @@ -702,15 +704,58 @@ pub trait SledAgentApi { HttpError, >; + /// This API endpoint is only reading the local sled agent's view of the + /// bootstore. The boostore is a distributed data store that is eventually + /// consistent. Reads from individual nodes may not represent the latest state. + #[endpoint { + method = GET, + path = "/network-bootstore-config", + versions = ..VERSION_BGP_V6, + }] + async fn read_network_bootstore_config_cache_v1( + rqctx: RequestContext, + ) -> Result< + HttpResponseOk, + HttpError, + > { + let result: v1::early_networking::EarlyNetworkConfig = + Self::read_network_bootstore_config_cache(rqctx) + .await? + .0 + .try_into() + .map_err(|e| { + HttpError::for_bad_request( + None, + format!("error getting v1 config: {e}"), + ) + })?; + + Ok(HttpResponseOk(result)) + } + #[endpoint { method = PUT, path = "/network-bootstore-config", + versions = VERSION_BGP_V6.., }] async fn write_network_bootstore_config( rqctx: RequestContext, body: TypedBody, ) -> Result; + #[endpoint { + method = PUT, + path = "/network-bootstore-config", + versions = ..VERSION_BGP_V6, + }] + async fn write_network_bootstore_config_v1( + rqctx: RequestContext, + body: TypedBody, + ) -> Result { + Self::write_network_bootstore_config(rqctx, body.map(|x| x.into())) + .await + } + /// Add a sled to a rack that was already initialized via RSS #[endpoint { method = PUT, diff --git a/sled-agent/tests/integration_tests/early_network.rs b/sled-agent/tests/integration_tests/early_network.rs index 2f1707c4577..afb73ee2b0c 100644 --- a/sled-agent/tests/integration_tests/early_network.rs +++ b/sled-agent/tests/integration_tests/early_network.rs @@ -4,7 +4,7 @@ //! Tests that EarlyNetworkConfig deserializes across versions. -use std::net::Ipv4Addr; +use std::net::{IpAddr, Ipv4Addr}; use std::str::FromStr; use bootstore::schemes::v0 as bootstore; @@ -136,7 +136,7 @@ fn current_config_example() -> (&'static str, EarlyNetworkConfig) { bgp_peers: vec![BgpPeerConfig { asn: 65000, port: "bar".to_owned(), - addr: Ipv4Addr::new(1, 2, 3, 4), + addr: IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), hold_time: Some(20), idle_hold_time: Some(50), delay_open: None, diff --git a/sled-agent/types/src/early_networking.rs b/sled-agent/types/src/early_networking.rs index 55e4ff5f62c..dd7f240b659 100644 --- a/sled-agent/types/src/early_networking.rs +++ b/sled-agent/types/src/early_networking.rs @@ -5,6 +5,3 @@ //! Types for network setup required to bring up the control plane. pub use sled_agent_types_versions::latest::early_networking::*; - -// Re-export back_compat module for bootstore serialization -pub use sled_agent_types_versions::v1::early_networking::back_compat; diff --git a/sled-agent/types/versions/src/bgp_v6/early_networking.rs b/sled-agent/types/versions/src/bgp_v6/early_networking.rs new file mode 100644 index 00000000000..81441d61051 --- /dev/null +++ b/sled-agent/types/versions/src/bgp_v6/early_networking.rs @@ -0,0 +1,232 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Types for network setup required to bring up the control plane. + +use bootstore::schemes::v0 as bootstore; +use omicron_common::api::internal::shared::RackNetworkConfig; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// Network configuration required to bring up the control plane +/// +/// The fields in this structure are those from +/// `RackInitializeRequest` necessary for use beyond RSS. +/// This is just for the initial rack configuration and cold boot purposes. +/// Updates come from Nexus. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +pub struct EarlyNetworkConfig { + // The current generation number of data as stored in CRDB. + // The initial generation is set during RSS time and then only mutated + // by Nexus. + pub generation: u64, + + // Which version of the data structure do we have. This is to help with + // deserialization and conversion in future updates. + pub schema_version: u32, + + // The actual configuration details + pub body: EarlyNetworkConfigBody, +} + +impl From + for EarlyNetworkConfig +{ + fn from(value: crate::v1::early_networking::EarlyNetworkConfig) -> Self { + use omicron_common::api::internal::shared::rack_init::v1; + use omicron_common::api::internal::shared::{ + BfdPeerConfig, BgpConfig, BgpPeerConfig, LldpAdminStatus, + LldpPortConfig, PortConfig, PortFec, PortSpeed, RouteConfig, + SwitchLocation, TxEqConfig, UplinkAddressConfig, + }; + + let rack_network_config = + value.body.rack_network_config.map(|v1_config| { + RackNetworkConfig { + rack_subnet: v1_config.rack_subnet, + infra_ip_first: v1_config.infra_ip_first, + infra_ip_last: v1_config.infra_ip_last, + ports: v1_config + .ports + .into_iter() + .map(|p| PortConfig { + routes: p + .routes + .into_iter() + .map(|r| RouteConfig { + destination: r.destination, + nexthop: r.nexthop, + vlan_id: r.vlan_id, + rib_priority: r.rib_priority, + }) + .collect(), + addresses: p + .addresses + .into_iter() + .map(|a| UplinkAddressConfig { + address: a.address, + vlan_id: a.vlan_id, + }) + .collect(), + switch: match p.switch { + v1::SwitchLocation::Switch0 => { + SwitchLocation::Switch0 + } + v1::SwitchLocation::Switch1 => { + SwitchLocation::Switch1 + } + }, + port: p.port, + uplink_port_speed: match p.uplink_port_speed { + v1::PortSpeed::Speed0G => PortSpeed::Speed0G, + v1::PortSpeed::Speed1G => PortSpeed::Speed1G, + v1::PortSpeed::Speed10G => PortSpeed::Speed10G, + v1::PortSpeed::Speed25G => PortSpeed::Speed25G, + v1::PortSpeed::Speed40G => PortSpeed::Speed40G, + v1::PortSpeed::Speed50G => PortSpeed::Speed50G, + v1::PortSpeed::Speed100G => { + PortSpeed::Speed100G + } + v1::PortSpeed::Speed200G => { + PortSpeed::Speed200G + } + v1::PortSpeed::Speed400G => { + PortSpeed::Speed400G + } + }, + uplink_port_fec: p.uplink_port_fec.map( + |f| match f { + v1::PortFec::Firecode => PortFec::Firecode, + v1::PortFec::None => PortFec::None, + v1::PortFec::Rs => PortFec::Rs, + }, + ), + bgp_peers: p + .bgp_peers + .into_iter() + .map(|peer| BgpPeerConfig { + asn: peer.asn, + port: peer.port, + addr: peer.addr.into(), // Ipv4Addr -> IpAddr + hold_time: peer.hold_time, + idle_hold_time: peer.idle_hold_time, + delay_open: peer.delay_open, + connect_retry: peer.connect_retry, + keepalive: peer.keepalive, + remote_asn: peer.remote_asn, + min_ttl: peer.min_ttl, + md5_auth_key: peer.md5_auth_key, + multi_exit_discriminator: peer + .multi_exit_discriminator, + communities: peer.communities, + local_pref: peer.local_pref, + enforce_first_as: peer.enforce_first_as, + allowed_import: peer.allowed_import, + allowed_export: peer.allowed_export, + vlan_id: peer.vlan_id, + }) + .collect(), + autoneg: p.autoneg, + lldp: p.lldp.map(|l| LldpPortConfig { + status: match l.status { + v1::LldpAdminStatus::Enabled => { + LldpAdminStatus::Enabled + } + v1::LldpAdminStatus::Disabled => { + LldpAdminStatus::Disabled + } + v1::LldpAdminStatus::RxOnly => { + LldpAdminStatus::RxOnly + } + v1::LldpAdminStatus::TxOnly => { + LldpAdminStatus::TxOnly + } + }, + chassis_id: l.chassis_id, + port_id: l.port_id, + port_description: l.port_description, + system_name: l.system_name, + system_description: l.system_description, + management_addrs: l.management_addrs, + }), + tx_eq: p.tx_eq.map(|t| TxEqConfig { + pre1: t.pre1, + pre2: t.pre2, + main: t.main, + post2: t.post2, + post1: t.post1, + }), + }) + .collect(), + bgp: v1_config + .bgp + .into_iter() + .map(|b| BgpConfig { + asn: b.asn, + originate: b.originate, + shaper: b.shaper, + checker: b.checker, + }) + .collect(), + bfd: v1_config + .bfd + .into_iter() + .map(|b| BfdPeerConfig { + local: b.local, + remote: b.remote, + detection_threshold: b.detection_threshold, + required_rx: b.required_rx, + mode: b.mode, + switch: match b.switch { + v1::SwitchLocation::Switch0 => { + SwitchLocation::Switch0 + } + v1::SwitchLocation::Switch1 => { + SwitchLocation::Switch1 + } + }, + }) + .collect(), + } + }); + + EarlyNetworkConfig { + generation: value.generation, + schema_version: value.schema_version, + body: EarlyNetworkConfigBody { + ntp_servers: value.body.ntp_servers, + rack_network_config, + }, + } + } +} + +/// This is the actual configuration of EarlyNetworking. +/// +/// We nest it below the "header" of `generation` and `schema_version` so that +/// we can perform partial deserialization of `EarlyNetworkConfig` to only read +/// the header and defer deserialization of the body once we know the schema +/// version. This is possible via the use of [`serde_json::value::RawValue`] in +/// future (post-v1) deserialization paths. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +pub struct EarlyNetworkConfigBody { + /// The external NTP server addresses. + pub ntp_servers: Vec, + + // Rack network configuration as delivered from RSS or Nexus + pub rack_network_config: Option, +} + +impl From for bootstore::NetworkConfig { + fn from(value: EarlyNetworkConfig) -> Self { + // Can this ever actually fail? + // We literally just deserialized the same data in RSS + let blob = serde_json::to_vec(&value).unwrap(); + + // Yes this is duplicated, but that seems fine. + let generation = value.generation; + + bootstore::NetworkConfig { generation, blob } + } +} diff --git a/sled-agent/types/versions/src/bgp_v6/mod.rs b/sled-agent/types/versions/src/bgp_v6/mod.rs new file mode 100644 index 00000000000..4325a016a9a --- /dev/null +++ b/sled-agent/types/versions/src/bgp_v6/mod.rs @@ -0,0 +1,7 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Version `BGP_v6` of the Sled Agent API. + +pub mod early_networking; diff --git a/sled-agent/types/versions/src/impls/early_networking.rs b/sled-agent/types/versions/src/impls/early_networking.rs index 4f9b88dc034..24fd1838cd7 100644 --- a/sled-agent/types/versions/src/impls/early_networking.rs +++ b/sled-agent/types/versions/src/impls/early_networking.rs @@ -12,9 +12,6 @@ use slog::{Logger, warn}; use crate::latest::early_networking::{ EarlyNetworkConfig, EarlyNetworkConfigBody, }; -// This is an exception to the rule that we only use the latest version, since -// the back_compat module is only defined for v1. -use crate::v1::early_networking::back_compat; impl FromStr for EarlyNetworkConfig { type Err = String; @@ -27,25 +24,14 @@ impl FromStr for EarlyNetworkConfig { body: EarlyNetworkConfigBody, } - let v2_err = match serde_json::from_str::(&value) { - Ok(cfg) => { - return Ok(EarlyNetworkConfig { - generation: cfg.generation, - schema_version: cfg.schema_version, - body: cfg.body, - }); - } - Err(e) => format!("unable to parse EarlyNetworkConfig: {e:?}"), - }; - // If we fail to parse the config as any known version, we return the - // error corresponding to the parse failure of the newest schema. - serde_json::from_str::(&value) - .map(|v1| EarlyNetworkConfig { - generation: v1.generation, - schema_version: Self::schema_version(), - body: v1.body.into(), - }) - .map_err(|_| v2_err) + match serde_json::from_str::(&value) { + Ok(cfg) => Ok(EarlyNetworkConfig { + generation: cfg.generation, + schema_version: cfg.schema_version, + body: cfg.body, + }), + Err(e) => Err(format!("unable to parse EarlyNetworkConfig: {e:?}")), + } } } @@ -62,76 +48,19 @@ impl EarlyNetworkConfig { ) -> Result { // Try to deserialize the latest version of the data structure (v2). If // that succeeds we are done. - let v2_error = - match serde_json::from_slice::(&config.blob) { - Ok(val) => return Ok(val), - Err(error) => { - // Log this error and continue trying to deserialize older - // versions. - warn!( - log, - "Failed to deserialize EarlyNetworkConfig \ - as v2, trying next as v1: {}", - error, - ); - error - } - }; - - match serde_json::from_slice::( - &config.blob, - ) { - Ok(v1) => { - // Convert from v1 to v2 - return Ok(EarlyNetworkConfig { - generation: v1.generation, - schema_version: EarlyNetworkConfig::schema_version(), - body: v1.body.into(), - }); - } + match serde_json::from_slice::(&config.blob) { + Ok(val) => Ok(val), Err(error) => { - // Log this error. + // Log this error and continue trying to deserialize older + // versions. warn!( log, "Failed to deserialize EarlyNetworkConfig \ - as v1, trying next as v0: {}", - error + as v2, trying next as v1: {}", + error, ); + Err(error) } - }; - - match serde_json::from_slice::( - &config.blob, - ) { - Ok(val) => { - // Convert from v0 to v2 - return Ok(EarlyNetworkConfig { - generation: val.generation, - schema_version: 2, - body: EarlyNetworkConfigBody { - ntp_servers: val.ntp_servers, - rack_network_config: val.rack_network_config.map( - |v0_config| { - back_compat::RackNetworkConfigV0::to_v2( - val.rack_subnet, - v0_config, - ) - }, - ), - }, - }); - } - Err(error) => { - // Log this error. - warn!( - log, - "Failed to deserialize EarlyNetworkConfig as v0: {}", error, - ); - } - }; - - // If we fail to parse the config as any known version, we return the - // error corresponding to the parse failure of the newest schema. - Err(v2_error) + } } } diff --git a/sled-agent/types/versions/src/initial/early_networking.rs b/sled-agent/types/versions/src/initial/early_networking.rs index f8501c79633..88a9efc93f2 100644 --- a/sled-agent/types/versions/src/initial/early_networking.rs +++ b/sled-agent/types/versions/src/initial/early_networking.rs @@ -5,7 +5,7 @@ //! Types for network setup required to bring up the control plane. use bootstore::schemes::v0 as bootstore; -use omicron_common::api::internal::shared::RackNetworkConfig; +use omicron_common::api::internal::shared::rack_init::v1::RackNetworkConfig; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -59,441 +59,213 @@ impl From for bootstore::NetworkConfig { } } -/// Structures and routines used to maintain backwards compatibility. The -/// contents of this module should only be used to convert older data into the -/// current format, and not for any ongoing run-time operations. -pub mod back_compat { - use std::net::{Ipv4Addr, Ipv6Addr}; - - use omicron_common::api::{ - external::SwitchLocation, - internal::shared::{ - BfdPeerConfig, BgpConfig, BgpPeerConfig, PortConfig, PortFec, - PortSpeed, RackNetworkConfig, RouteConfig, UplinkAddressConfig, - }, - }; - use oxnet::{IpNet, Ipv4Net, Ipv6Net}; - use schemars::JsonSchema; - use serde::{Deserialize, Serialize}; - - use super::EarlyNetworkConfigBody; - - #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] - pub struct EarlyNetworkConfigBodyV1 { - /// The external NTP server addresses. - pub ntp_servers: Vec, - - // Rack network configuration as delivered from RSS or Nexus - pub rack_network_config: Option, - } - - impl From for EarlyNetworkConfigBody { - fn from(v1: EarlyNetworkConfigBodyV1) -> Self { - EarlyNetworkConfigBody { - ntp_servers: v1.ntp_servers, - rack_network_config: v1 - .rack_network_config - .map(|v1_config| v1_config.into()), - } - } - } - - /// Deprecated, use `RackNetworkConfig` instead. Cannot actually deprecate due to - /// - /// - /// Our first version of `RackNetworkConfig`. If this exists in the bootstore, we - /// upgrade out of it into `RackNetworkConfigV1` or later versions if possible. - #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] - pub struct RackNetworkConfigV0 { - // TODO: #3591 Consider making infra-ip ranges implicit for uplinks - /// First ip address to be used for configuring network infrastructure - pub infra_ip_first: Ipv4Addr, - /// Last ip address to be used for configuring network infrastructure - pub infra_ip_last: Ipv4Addr, - /// Uplinks for connecting the rack to external networks - pub uplinks: Vec, - } - - impl RackNetworkConfigV0 { - /// Convert from `RackNetworkConfigV0` to `RackNetworkConfigV1` - /// - /// We cannot use `From for `RackNetworkConfig` - /// because the `rack_subnet` field does not exist in `RackNetworkConfigV0` - /// and must be passed in from the `EarlyNetworkConfigV0` struct which - /// contains the `RackNetworkConfigV0` struct. - pub fn to_v2( - rack_subnet: Ipv6Addr, - v0: RackNetworkConfigV0, - ) -> RackNetworkConfig { - RackNetworkConfig { - rack_subnet: Ipv6Net::new(rack_subnet, 56).unwrap(), - infra_ip_first: v0.infra_ip_first, - infra_ip_last: v0.infra_ip_last, - ports: v0 - .uplinks - .into_iter() - .map(|uplink| PortConfig::from(uplink)) - .collect(), - bgp: vec![], - bfd: vec![], - } - } - } - - /// Deprecated, use PortConfig instead. Cannot actually deprecate due to - /// - #[derive(Clone, Debug, PartialEq, Deserialize, Serialize, JsonSchema)] - pub struct PortConfigV1 { - /// The set of routes associated with this port. - pub routes: Vec, - /// This port's addresses and optional vlan IDs - pub addresses: Vec, - /// Switch the port belongs to. - pub switch: SwitchLocation, - /// Nmae of the port this config applies to. - pub port: String, - /// Port speed. - pub uplink_port_speed: PortSpeed, - /// Port forward error correction type. - pub uplink_port_fec: PortFec, - /// BGP peers on this port - pub bgp_peers: Vec, - /// Whether or not to set autonegotiation - #[serde(default)] - pub autoneg: bool, - } - - impl From for PortConfig { - fn from(v1: PortConfigV1) -> Self { - PortConfig { - routes: v1.routes.clone(), - addresses: v1 - .addresses - .iter() - .map(|a| UplinkAddressConfig { address: *a, vlan_id: None }) - .collect(), - switch: v1.switch, - port: v1.port, - uplink_port_speed: v1.uplink_port_speed, - uplink_port_fec: Some(v1.uplink_port_fec), - bgp_peers: v1.bgp_peers.clone(), - autoneg: v1.autoneg, - lldp: None, - tx_eq: None, - } - } - } - - /// Deprecated, use PortConfig instead. Cannot actually deprecate due to - /// - #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] - pub struct UplinkConfig { - /// Gateway address - pub gateway_ip: Ipv4Addr, - /// Switch to use for uplink - pub switch: SwitchLocation, - /// Switchport to use for external connectivity - pub uplink_port: String, - /// Speed for the Switchport - pub uplink_port_speed: PortSpeed, - /// Forward Error Correction setting for the uplink port - pub uplink_port_fec: PortFec, - /// IP Address and prefix (e.g., `192.168.0.1/16`) to apply to switchport - /// (must be in infra_ip pool) - pub uplink_cidr: Ipv4Net, - /// VLAN id to use for uplink - pub uplink_vid: Option, - /// RIB Priority - pub rib_priority: Option, - } - - impl From for PortConfig { - fn from(value: UplinkConfig) -> Self { - PortConfig { - routes: vec![RouteConfig { - destination: "0.0.0.0/0".parse().unwrap(), - nexthop: value.gateway_ip.into(), - vlan_id: value.uplink_vid, - rib_priority: value.rib_priority, - }], - addresses: vec![UplinkAddressConfig { - address: value.uplink_cidr.into(), - vlan_id: value.uplink_vid, - }], - switch: value.switch, - port: value.uplink_port, - uplink_port_speed: value.uplink_port_speed, - uplink_port_fec: Some(value.uplink_port_fec), - bgp_peers: vec![], - autoneg: false, - lldp: None, - tx_eq: None, - } - } - } - - /// Deprecated, use `RackNetworkConfig` instead. Cannot actually deprecate due to - /// - /// - /// Our second version of `RackNetworkConfig`. If this exists in the bootstore, - /// we upgrade out of it into `RackNetworkConfigV1` or later versions if - /// possible. - #[derive(Clone, Debug, PartialEq, Deserialize, Serialize, JsonSchema)] - pub struct RackNetworkConfigV1 { - pub rack_subnet: Ipv6Net, - // TODO: #3591 Consider making infra-ip ranges implicit for uplinks - /// First ip address to be used for configuring network infrastructure - pub infra_ip_first: Ipv4Addr, - /// Last ip address to be used for configuring network infrastructure - pub infra_ip_last: Ipv4Addr, - /// Uplinks for connecting the rack to external networks - pub ports: Vec, - /// BGP configurations for connecting the rack to external networks - pub bgp: Vec, - /// BFD configuration for connecting the rack to external networks - #[serde(default)] - pub bfd: Vec, - } +/// Error converting from a newer EarlyNetworkConfig to v1. +#[derive(Debug, thiserror::Error)] +#[error("BgpPeerConfig address {addr} is not IPv4")] +pub struct BgpPeerAddrNotIpv4 { + addr: std::net::IpAddr, +} - impl From for RackNetworkConfig { - fn from(v1: RackNetworkConfigV1) -> Self { - RackNetworkConfig { - rack_subnet: v1.rack_subnet, - infra_ip_first: v1.infra_ip_first, - infra_ip_last: v1.infra_ip_last, - ports: v1 +impl TryFrom + for EarlyNetworkConfig +{ + type Error = BgpPeerAddrNotIpv4; + + fn try_from( + value: crate::v15::early_networking::EarlyNetworkConfig, + ) -> Result { + use omicron_common::api::internal::shared::rack_init::v1; + use omicron_common::api::internal::shared::rack_init::v2; + use std::net::IpAddr; + + let rack_network_config = value + .body + .rack_network_config + .map(|v2_config| { + let ports = v2_config .ports .into_iter() - .map(|ports| PortConfig::from(ports)) - .collect(), - bgp: v1.bgp.clone(), - bfd: v1.bfd.clone(), - } - } - } - - // The second production version of the `EarlyNetworkConfig`. - // - // If this version is in the bootstore than we need to convert it to - // `EarlyNetworkConfig`. - // - // Once we do this for all customers that have initialized racks with the - // old version we can go ahead and remove this type and its conversion code - // altogether. - #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] - pub struct EarlyNetworkConfigV1 { - // The current generation number of data as stored in CRDB. - // The initial generation is set during RSS time and then only mutated - // by Nexus. - pub generation: u64, - - // Which version of the data structure do we have. This is to help with - // deserialization and conversion in future updates. - pub schema_version: u32, - - // The actual configuration details - pub body: EarlyNetworkConfigBodyV1, - } - - // The first production version of the `EarlyNetworkConfig`. - // - // If this version is in the bootstore than we need to convert it to - // `EarlyNetworkConfig`. - // - // Once we do this for all customers that have initialized racks with the - // old version we can go ahead and remove this type and its conversion code - // altogether. - #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] - pub struct EarlyNetworkConfigV0 { - // The current generation number of data as stored in CRDB. - // The initial generation is set during RSS time and then only mutated - // by Nexus. - pub generation: u64, - - pub rack_subnet: Ipv6Addr, - - /// The external NTP server addresses. - pub ntp_servers: Vec, - - // Rack network configuration as delivered from RSS and only existing at - // generation 1 - pub rack_network_config: Option, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use std::net::Ipv4Addr; - use std::net::Ipv6Addr; - - use omicron_common::api::external::SwitchLocation; - use omicron_common::api::internal::shared::PortConfig; - use omicron_common::api::internal::shared::PortFec; - use omicron_common::api::internal::shared::PortSpeed; - use omicron_common::api::internal::shared::RackNetworkConfig; - use omicron_common::api::internal::shared::RouteConfig; - use omicron_common::api::internal::shared::UplinkAddressConfig; - use omicron_test_utils::dev::test_setup_log; - use oxnet::Ipv6Net; - - #[test] - fn serialized_early_network_config_v0_to_v2_conversion() { - let logctx = test_setup_log( - "serialized_early_network_config_v0_to_v2_conversion", - ); - let v0 = back_compat::EarlyNetworkConfigV0 { - generation: 1, - rack_subnet: Ipv6Addr::UNSPECIFIED, - ntp_servers: Vec::new(), - rack_network_config: Some(back_compat::RackNetworkConfigV0 { - infra_ip_first: Ipv4Addr::UNSPECIFIED, - infra_ip_last: Ipv4Addr::UNSPECIFIED, - uplinks: vec![back_compat::UplinkConfig { - gateway_ip: Ipv4Addr::UNSPECIFIED, - switch: SwitchLocation::Switch0, - uplink_port: "Port0".to_string(), - uplink_port_speed: PortSpeed::Speed100G, - uplink_port_fec: PortFec::None, - uplink_cidr: "192.168.0.1/16".parse().unwrap(), - uplink_vid: None, - rib_priority: None, - }], - }), - }; - - let v0_serialized = serde_json::to_vec(&v0).unwrap(); - let bootstore_conf = - bootstore::NetworkConfig { generation: 1, blob: v0_serialized }; - - let v2 = EarlyNetworkConfig::deserialize_bootstore_config( - &logctx.log, - &bootstore_conf, - ) - .unwrap(); - let v0_rack_network_config = v0.rack_network_config.unwrap(); - let uplink = v0_rack_network_config.uplinks[0].clone(); - let expected = EarlyNetworkConfig { - generation: 1, - schema_version: EarlyNetworkConfig::schema_version(), + .map(|p| { + let bgp_peers = p + .bgp_peers + .into_iter() + .map(|peer| { + let addr = match peer.addr { + IpAddr::V4(v4) => Ok(v4), + other => { + Err(BgpPeerAddrNotIpv4 { addr: other }) + } + }?; + Ok(v1::BgpPeerConfig { + asn: peer.asn, + port: peer.port, + addr, + hold_time: peer.hold_time, + idle_hold_time: peer.idle_hold_time, + delay_open: peer.delay_open, + connect_retry: peer.connect_retry, + keepalive: peer.keepalive, + remote_asn: peer.remote_asn, + min_ttl: peer.min_ttl, + md5_auth_key: peer.md5_auth_key, + multi_exit_discriminator: peer + .multi_exit_discriminator, + communities: peer.communities, + local_pref: peer.local_pref, + enforce_first_as: peer.enforce_first_as, + allowed_import: peer.allowed_import, + allowed_export: peer.allowed_export, + vlan_id: peer.vlan_id, + }) + }) + .collect::, _>>()?; + + Ok(v1::PortConfig { + routes: p + .routes + .into_iter() + .map(|r| v1::RouteConfig { + destination: r.destination, + nexthop: r.nexthop, + vlan_id: r.vlan_id, + rib_priority: r.rib_priority, + }) + .collect(), + addresses: p + .addresses + .into_iter() + .map(|a| v1::UplinkAddressConfig { + address: a.address, + vlan_id: a.vlan_id, + }) + .collect(), + switch: match p.switch { + v2::SwitchLocation::Switch0 => { + v1::SwitchLocation::Switch0 + } + v2::SwitchLocation::Switch1 => { + v1::SwitchLocation::Switch1 + } + }, + port: p.port, + uplink_port_speed: match p.uplink_port_speed { + v2::PortSpeed::Speed0G => { + v1::PortSpeed::Speed0G + } + v2::PortSpeed::Speed1G => { + v1::PortSpeed::Speed1G + } + v2::PortSpeed::Speed10G => { + v1::PortSpeed::Speed10G + } + v2::PortSpeed::Speed25G => { + v1::PortSpeed::Speed25G + } + v2::PortSpeed::Speed40G => { + v1::PortSpeed::Speed40G + } + v2::PortSpeed::Speed50G => { + v1::PortSpeed::Speed50G + } + v2::PortSpeed::Speed100G => { + v1::PortSpeed::Speed100G + } + v2::PortSpeed::Speed200G => { + v1::PortSpeed::Speed200G + } + v2::PortSpeed::Speed400G => { + v1::PortSpeed::Speed400G + } + }, + uplink_port_fec: p.uplink_port_fec.map( + |f| match f { + v2::PortFec::Firecode => { + v1::PortFec::Firecode + } + v2::PortFec::None => v1::PortFec::None, + v2::PortFec::Rs => v1::PortFec::Rs, + }, + ), + bgp_peers, + autoneg: p.autoneg, + lldp: p.lldp.map(|l| v1::LldpPortConfig { + status: match l.status { + v2::LldpAdminStatus::Enabled => { + v1::LldpAdminStatus::Enabled + } + v2::LldpAdminStatus::Disabled => { + v1::LldpAdminStatus::Disabled + } + v2::LldpAdminStatus::RxOnly => { + v1::LldpAdminStatus::RxOnly + } + v2::LldpAdminStatus::TxOnly => { + v1::LldpAdminStatus::TxOnly + } + }, + chassis_id: l.chassis_id, + port_id: l.port_id, + port_description: l.port_description, + system_name: l.system_name, + system_description: l.system_description, + management_addrs: l.management_addrs, + }), + tx_eq: p.tx_eq.map(|t| v1::TxEqConfig { + pre1: t.pre1, + pre2: t.pre2, + main: t.main, + post2: t.post2, + post1: t.post1, + }), + }) + }) + .collect::, _>>()?; + + Ok(RackNetworkConfig { + rack_subnet: v2_config.rack_subnet, + infra_ip_first: v2_config.infra_ip_first, + infra_ip_last: v2_config.infra_ip_last, + ports, + bgp: v2_config + .bgp + .into_iter() + .map(|b| v1::BgpConfig { + asn: b.asn, + originate: b.originate, + shaper: b.shaper, + checker: b.checker, + }) + .collect(), + bfd: v2_config + .bfd + .into_iter() + .map(|b| v1::BfdPeerConfig { + local: b.local, + remote: b.remote, + detection_threshold: b.detection_threshold, + required_rx: b.required_rx, + mode: b.mode, + switch: match b.switch { + v2::SwitchLocation::Switch0 => { + v1::SwitchLocation::Switch0 + } + v2::SwitchLocation::Switch1 => { + v1::SwitchLocation::Switch1 + } + }, + }) + .collect(), + }) + }) + .transpose()?; + + Ok(EarlyNetworkConfig { + generation: value.generation, + schema_version: value.schema_version, body: EarlyNetworkConfigBody { - ntp_servers: v0.ntp_servers.clone(), - rack_network_config: Some(RackNetworkConfig { - rack_subnet: Ipv6Net::new(v0.rack_subnet, 56).unwrap(), - infra_ip_first: v0_rack_network_config.infra_ip_first, - infra_ip_last: v0_rack_network_config.infra_ip_last, - ports: vec![PortConfig { - routes: vec![RouteConfig { - destination: "0.0.0.0/0".parse().unwrap(), - nexthop: uplink.gateway_ip.into(), - vlan_id: None, - rib_priority: None, - }], - addresses: vec![UplinkAddressConfig { - address: uplink.uplink_cidr.into(), - vlan_id: None, - }], - switch: uplink.switch, - port: uplink.uplink_port, - uplink_port_speed: uplink.uplink_port_speed, - uplink_port_fec: Some(uplink.uplink_port_fec), - autoneg: false, - bgp_peers: vec![], - lldp: None, - tx_eq: None, - }], - bgp: vec![], - bfd: vec![], - }), - }, - }; - - assert_eq!(expected, v2); - - logctx.cleanup_successful(); - } - - #[test] - fn serialized_early_network_config_v1_to_v2_conversion() { - let logctx = test_setup_log( - "serialized_early_network_config_v1_to_v2_conversion", - ); - - let v1 = back_compat::EarlyNetworkConfigV1 { - generation: 1, - schema_version: 1, - body: back_compat::EarlyNetworkConfigBodyV1 { - ntp_servers: Vec::new(), - rack_network_config: Some(back_compat::RackNetworkConfigV1 { - rack_subnet: Ipv6Net::new(Ipv6Addr::UNSPECIFIED, 56) - .unwrap(), - infra_ip_first: Ipv4Addr::UNSPECIFIED, - infra_ip_last: Ipv4Addr::UNSPECIFIED, - ports: vec![back_compat::PortConfigV1 { - routes: vec![RouteConfig { - destination: "0.0.0.0/0".parse().unwrap(), - nexthop: "192.168.0.2".parse().unwrap(), - vlan_id: None, - rib_priority: None, - }], - addresses: vec!["192.168.0.1/16".parse().unwrap()], - switch: SwitchLocation::Switch0, - port: "Port0".to_string(), - uplink_port_speed: PortSpeed::Speed100G, - uplink_port_fec: PortFec::None, - bgp_peers: Vec::new(), - autoneg: false, - }], - bgp: Vec::new(), - bfd: Vec::new(), - }), + ntp_servers: value.body.ntp_servers, + rack_network_config, }, - }; - - let v1_serialized = serde_json::to_vec(&v1).unwrap(); - let bootstore_conf = - bootstore::NetworkConfig { generation: 1, blob: v1_serialized }; - - let v2 = EarlyNetworkConfig::deserialize_bootstore_config( - &logctx.log, - &bootstore_conf, - ) - .unwrap(); - let v1_rack_network_config = v1.body.rack_network_config.unwrap(); - let port = v1_rack_network_config.ports[0].clone(); - let expected = EarlyNetworkConfig { - generation: 1, - schema_version: EarlyNetworkConfig::schema_version(), - body: EarlyNetworkConfigBody { - ntp_servers: v1.body.ntp_servers.clone(), - rack_network_config: Some(RackNetworkConfig { - rack_subnet: v1_rack_network_config.rack_subnet, - infra_ip_first: v1_rack_network_config.infra_ip_first, - infra_ip_last: v1_rack_network_config.infra_ip_last, - ports: vec![PortConfig { - routes: port.routes.clone(), - addresses: vec![UplinkAddressConfig { - address: port.addresses[0], - vlan_id: None, - }], - switch: port.switch, - port: port.port, - uplink_port_speed: port.uplink_port_speed, - uplink_port_fec: Some(port.uplink_port_fec), - autoneg: false, - bgp_peers: vec![], - lldp: None, - tx_eq: None, - }], - bgp: vec![], - bfd: vec![], - }), - }, - }; - - assert_eq!(expected, v2); - - logctx.cleanup_successful(); + }) } } diff --git a/sled-agent/types/versions/src/initial/rack_init.rs b/sled-agent/types/versions/src/initial/rack_init.rs index 08433f07364..290a2a5e83a 100644 --- a/sled-agent/types/versions/src/initial/rack_init.rs +++ b/sled-agent/types/versions/src/initial/rack_init.rs @@ -13,7 +13,9 @@ use omicron_common::{ }, api::{ external::AllowedSourceIps, - internal::{nexus::Certificate, shared::RackNetworkConfig}, + internal::{ + nexus::Certificate, shared::rack_init::v1::RackNetworkConfig, + }, }, }; use schemars::JsonSchema; diff --git a/sled-agent/types/versions/src/latest.rs b/sled-agent/types/versions/src/latest.rs index 6a96cbf18e5..79700309dc9 100644 --- a/sled-agent/types/versions/src/latest.rs +++ b/sled-agent/types/versions/src/latest.rs @@ -45,8 +45,8 @@ pub mod disk { } pub mod early_networking { - pub use crate::v1::early_networking::EarlyNetworkConfig; - pub use crate::v1::early_networking::EarlyNetworkConfigBody; + pub use crate::v15::early_networking::EarlyNetworkConfig; + pub use crate::v15::early_networking::EarlyNetworkConfigBody; } pub mod firewall_rules { diff --git a/sled-agent/types/versions/src/lib.rs b/sled-agent/types/versions/src/lib.rs index ac9183d2998..bd7fe0ee8a3 100644 --- a/sled-agent/types/versions/src/lib.rs +++ b/sled-agent/types/versions/src/lib.rs @@ -45,6 +45,8 @@ pub mod v12; pub mod v13; #[path = "lockstep_rack_init/mod.rs"] pub mod v14; +#[path = "bgp_v6/mod.rs"] +pub mod v15; #[path = "add_switch_zone_operator_policy/mod.rs"] pub mod v3; #[path = "add_nexus_lockstep_port_to_inventory/mod.rs"] diff --git a/wicket-common/src/rack_setup.rs b/wicket-common/src/rack_setup.rs index 73dfa647fb7..074a1e8f10d 100644 --- a/wicket-common/src/rack_setup.rs +++ b/wicket-common/src/rack_setup.rs @@ -209,7 +209,7 @@ pub struct UserSpecifiedBgpPeerConfig { /// Switch port the peer is reachable on. pub port: String, /// Address of the peer. - pub addr: Ipv4Addr, + pub addr: IpAddr, /// How long to keep a session alive without a keepalive in seconds. /// Defaults to 6 seconds. pub hold_time: Option, From 44d7f589012471c13aeebe7508ec63742dfde9cb Mon Sep 17 00:00:00 2001 From: Ryan Goodfellow Date: Mon, 26 Jan 2026 22:43:46 +0000 Subject: [PATCH 17/20] update bootstore early networking serialization tests --- sled-agent/tests/data/early_network_blobs.txt | 3 +- .../tests/integration_tests/early_network.rs | 214 ++++++++++++++---- 2 files changed, 171 insertions(+), 46 deletions(-) diff --git a/sled-agent/tests/data/early_network_blobs.txt b/sled-agent/tests/data/early_network_blobs.txt index c968d4010b2..07d838f6042 100644 --- a/sled-agent/tests/data/early_network_blobs.txt +++ b/sled-agent/tests/data/early_network_blobs.txt @@ -1,2 +1 @@ -2023-11-30 mupdate failing blob,{"generation":15,"schema_version":1,"body":{"ntp_servers":[],"rack_network_config":{"rack_subnet":"fd00:1122:3344:100::/56","infra_ip_first":"0.0.0.0","infra_ip_last":"0.0.0.0","ports":[{"routes":[],"addresses":[],"switch":"switch1","port":"qsfp0","uplink_port_speed":"speed100_g","uplink_port_fec":"none","bgp_peers":[]},{"routes":[],"addresses":["172.20.15.53/29"],"switch":"switch1","port":"qsfp18","uplink_port_speed":"speed100_g","uplink_port_fec":"rs","bgp_peers":[{"asn":65002,"port":"qsfp18","addr":"172.20.15.51","hold_time":6,"idle_hold_time":6,"delay_open":0,"connect_retry":3,"keepalive":2}]},{"routes":[],"addresses":["172.20.15.45/29"],"switch":"switch0","port":"qsfp18","uplink_port_speed":"speed100_g","uplink_port_fec":"rs","bgp_peers":[{"asn":65002,"port":"qsfp18","addr":"172.20.15.43","hold_time":6,"idle_hold_time":6,"delay_open":0,"connect_retry":3,"keepalive":2}]},{"routes":[],"addresses":[],"switch":"switch0","port":"qsfp0","uplink_port_speed":"speed100_g","uplink_port_fec":"none","bgp_peers":[]}],"bgp":[{"asn":65002,"originate":["172.20.26.0/24"]},{"asn":65002,"originate":["172.20.26.0/24"]}]}}} -2023-12-06 config,{"generation":20,"schema_version":1,"body":{"ntp_servers":["ntp.example.com"],"rack_network_config":{"rack_subnet":"ff01::/32","infra_ip_first":"127.0.0.1","infra_ip_last":"127.1.0.1","ports":[{"routes":[{"destination":"10.1.9.32/16","nexthop":"10.1.9.32"}],"addresses":["2001:db8::/96"],"switch":"switch0","port":"foo","uplink_port_speed":"speed200_g","uplink_port_fec":"firecode","bgp_peers":[{"asn":65000,"port":"bar","addr":"1.2.3.4","hold_time":20,"idle_hold_time":50,"delay_open":null,"connect_retry":30,"keepalive":10}],"autoneg":true}],"bgp":[{"asn":20000,"originate":["192.168.0.0/24"]}]}}} +2026-01-22 r17,{"generation":114,"schema_version":2,"body":{"ntp_servers":[],"rack_network_config":{"rack_subnet":"fd00:1122:3344:100::/56","infra_ip_first":"172.20.15.21","infra_ip_last":"172.20.15.22","ports":[{"routes":[],"addresses":[],"switch":"switch1","port":"qsfp0","uplink_port_speed":"speed100_g","uplink_port_fec":null,"bgp_peers":[],"autoneg":false,"lldp":null,"tx_eq":null},{"routes":[],"addresses":[],"switch":"switch1","port":"qsfp26","uplink_port_speed":"speed100_g","uplink_port_fec":"rs","bgp_peers":[],"autoneg":false,"lldp":{"status":"disabled","chassis_id":null,"port_id":null,"port_description":null,"system_name":null,"system_description":null,"management_addrs":null},"tx_eq":null},{"routes":[],"addresses":[{"address":"172.20.15.53/29","vlan_id":null}],"switch":"switch1","port":"qsfp18","uplink_port_speed":"speed100_g","uplink_port_fec":"rs","bgp_peers":[{"asn":65002,"port":"qsfp18","addr":"172.20.15.51","hold_time":6,"idle_hold_time":3,"delay_open":3,"connect_retry":3,"keepalive":2,"remote_asn":null,"min_ttl":null,"md5_auth_key":null,"multi_exit_discriminator":null,"communities":[],"local_pref":null,"enforce_first_as":false,"allowed_import":{"type":"no_filtering"},"allowed_export":{"type":"allow","value":["172.20.52.0/22","172.20.26.0/24"]},"vlan_id":null}],"autoneg":false,"lldp":{"status":"disabled","chassis_id":null,"port_id":null,"port_description":null,"system_name":null,"system_description":null,"management_addrs":null},"tx_eq":null},{"routes":[],"addresses":[{"address":"172.20.15.45/29","vlan_id":null}],"switch":"switch0","port":"qsfp18","uplink_port_speed":"speed100_g","uplink_port_fec":"rs","bgp_peers":[{"asn":65002,"port":"qsfp18","addr":"172.20.15.43","hold_time":6,"idle_hold_time":0,"delay_open":3,"connect_retry":3,"keepalive":2,"remote_asn":null,"min_ttl":null,"md5_auth_key":null,"multi_exit_discriminator":null,"communities":[],"local_pref":null,"enforce_first_as":false,"allowed_import":{"type":"no_filtering"},"allowed_export":{"type":"allow","value":["172.20.52.0/22","172.20.26.0/24"]},"vlan_id":null}],"autoneg":false,"lldp":{"status":"disabled","chassis_id":null,"port_id":null,"port_description":null,"system_name":null,"system_description":null,"management_addrs":null},"tx_eq":null},{"routes":[],"addresses":[],"switch":"switch0","port":"qsfp0","uplink_port_speed":"speed100_g","uplink_port_fec":null,"bgp_peers":[],"autoneg":false,"lldp":null,"tx_eq":null},{"routes":[],"addresses":[],"switch":"switch0","port":"qsfp26","uplink_port_speed":"speed100_g","uplink_port_fec":"rs","bgp_peers":[],"autoneg":false,"lldp":{"status":"disabled","chassis_id":null,"port_id":null,"port_description":null,"system_name":null,"system_description":null,"management_addrs":null},"tx_eq":null}],"bgp":[{"asn":65002,"originate":["172.20.52.0/22","172.20.26.0/24"],"shaper":null,"checker":null}],"bfd":[]}}} diff --git a/sled-agent/tests/integration_tests/early_network.rs b/sled-agent/tests/integration_tests/early_network.rs index afb73ee2b0c..c7d59a836c0 100644 --- a/sled-agent/tests/integration_tests/early_network.rs +++ b/sled-agent/tests/integration_tests/early_network.rs @@ -8,6 +8,9 @@ use std::net::{IpAddr, Ipv4Addr}; use std::str::FromStr; use bootstore::schemes::v0 as bootstore; +use omicron_common::api::internal::shared::{ + LldpAdminStatus, LldpPortConfig, UplinkAddressConfig, +}; use omicron_common::api::{ external::{ImportExportPolicy, SwitchLocation}, internal::shared::{ @@ -111,55 +114,178 @@ fn early_network_blobs_deserialize() { /// future, older blobs can still be deserialized correctly. fn current_config_example() -> (&'static str, EarlyNetworkConfig) { // NOTE: the description must not contain commas or newlines. - let description = "2023-12-06 config"; + let description = "2026-01-22 r17"; let config = EarlyNetworkConfig { - generation: 20, + generation: 114, schema_version: EarlyNetworkConfig::schema_version(), body: EarlyNetworkConfigBody { - ntp_servers: vec!["ntp.example.com".to_owned()], + ntp_servers: vec![], rack_network_config: Some(RackNetworkConfig { - rack_subnet: "ff01::0/32".parse().unwrap(), - infra_ip_first: Ipv4Addr::new(127, 0, 0, 1), - infra_ip_last: Ipv4Addr::new(127, 1, 0, 1), - ports: vec![PortConfig { - routes: vec![RouteConfig { - destination: "10.1.9.32/16".parse().unwrap(), - nexthop: "10.1.9.32".parse().unwrap(), - vlan_id: None, - rib_priority: None, - }], - addresses: vec!["2001:db8::/96".parse().unwrap()], - switch: SwitchLocation::Switch0, - port: "foo".to_owned(), - uplink_port_speed: PortSpeed::Speed200G, - uplink_port_fec: Some(PortFec::Firecode), - bgp_peers: vec![BgpPeerConfig { - asn: 65000, - port: "bar".to_owned(), - addr: IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), - hold_time: Some(20), - idle_hold_time: Some(50), - delay_open: None, - connect_retry: Some(30), - keepalive: Some(10), - remote_asn: None, - min_ttl: None, - md5_auth_key: None, - multi_exit_discriminator: None, - communities: Vec::new(), - local_pref: None, - enforce_first_as: false, - allowed_export: ImportExportPolicy::NoFiltering, - allowed_import: ImportExportPolicy::NoFiltering, - vlan_id: None, - }], - autoneg: true, - tx_eq: None, - lldp: None, - }], + rack_subnet: "fd00:1122:3344:100::/56".parse().unwrap(), + infra_ip_first: "172.20.15.21".parse().unwrap(), + infra_ip_last: "172.20.15.22".parse().unwrap(), + ports: vec![ + PortConfig { + routes: vec![], + addresses: vec![], + switch: SwitchLocation::Switch1, + port: "qsfp0".to_owned(), + uplink_port_speed: PortSpeed::Speed100G, + uplink_port_fec: None, + bgp_peers: vec![], + autoneg: false, + tx_eq: None, + lldp: None, + }, + PortConfig { + routes: vec![], + addresses: vec![], + switch: SwitchLocation::Switch1, + port: "qsfp26".to_owned(), + uplink_port_speed: PortSpeed::Speed100G, + uplink_port_fec: Some(PortFec::Rs), + bgp_peers: vec![], + autoneg: false, + tx_eq: None, + lldp: Some(LldpPortConfig { + status: LldpAdminStatus::Disabled, + chassis_id: None, + port_id: None, + port_description: None, + system_name: None, + system_description: None, + management_addrs: None, + }), + }, + PortConfig { + routes: vec![], + addresses: vec![UplinkAddressConfig { + address: "172.20.15.53/29".parse().unwrap(), + vlan_id: None, + }], + switch: SwitchLocation::Switch1, + port: "qsfp18".to_owned(), + uplink_port_speed: PortSpeed::Speed100G, + uplink_port_fec: Some(PortFec::Rs), + bgp_peers: vec![BgpPeerConfig { + asn: 65002, + port: "qsfp18".to_owned(), + addr: "172.20.15.51".parse().unwrap(), + hold_time: Some(6), + idle_hold_time: Some(3), + delay_open: Some(3), + connect_retry: Some(3), + keepalive: Some(2), + remote_asn: None, + min_ttl: None, + md5_auth_key: None, + multi_exit_discriminator: None, + communities: Vec::new(), + local_pref: None, + enforce_first_as: false, + allowed_import: ImportExportPolicy::NoFiltering, + allowed_export: ImportExportPolicy::Allow(vec![ + "172.20.52.0/22".parse().unwrap(), + "172.20.26.0/24".parse().unwrap(), + ]), + vlan_id: None, + }], + autoneg: false, + tx_eq: None, + lldp: Some(LldpPortConfig { + status: LldpAdminStatus::Disabled, + chassis_id: None, + port_id: None, + port_description: None, + system_name: None, + system_description: None, + management_addrs: None, + }), + }, + PortConfig { + routes: vec![], + addresses: vec![UplinkAddressConfig { + address: "172.20.15.45/29".parse().unwrap(), + vlan_id: None, + }], + switch: SwitchLocation::Switch0, + port: "qsfp18".to_owned(), + uplink_port_speed: PortSpeed::Speed100G, + uplink_port_fec: Some(PortFec::Rs), + bgp_peers: vec![BgpPeerConfig { + asn: 65002, + port: "qsfp18".to_owned(), + addr: "172.20.15.43".parse().unwrap(), + hold_time: Some(6), + idle_hold_time: Some(0), + delay_open: Some(3), + connect_retry: Some(3), + keepalive: Some(2), + remote_asn: None, + min_ttl: None, + md5_auth_key: None, + multi_exit_discriminator: None, + communities: Vec::new(), + local_pref: None, + enforce_first_as: false, + allowed_import: ImportExportPolicy::NoFiltering, + allowed_export: ImportExportPolicy::Allow(vec![ + "172.20.52.0/22".parse().unwrap(), + "172.20.26.0/24".parse().unwrap(), + ]), + vlan_id: None, + }], + autoneg: false, + tx_eq: None, + lldp: Some(LldpPortConfig { + status: LldpAdminStatus::Disabled, + chassis_id: None, + port_id: None, + port_description: None, + system_name: None, + system_description: None, + management_addrs: None, + }), + }, + PortConfig { + routes: vec![], + addresses: vec![], + switch: SwitchLocation::Switch0, + port: "qsfp0".to_owned(), + uplink_port_speed: PortSpeed::Speed100G, + uplink_port_fec: None, + bgp_peers: vec![], + autoneg: false, + tx_eq: None, + lldp: None, + }, + PortConfig { + routes: vec![], + addresses: vec![], + switch: SwitchLocation::Switch0, + port: "qsfp26".to_owned(), + uplink_port_speed: PortSpeed::Speed100G, + uplink_port_fec: Some(PortFec::Rs), + bgp_peers: vec![], + autoneg: false, + tx_eq: None, + lldp: Some(LldpPortConfig { + status: LldpAdminStatus::Disabled, + chassis_id: None, + port_id: None, + port_description: None, + system_name: None, + system_description: None, + management_addrs: None, + }), + }, + ], bgp: vec![BgpConfig { - asn: 20000, - originate: vec!["192.168.0.0/24".parse().unwrap()], + asn: 65002, + originate: vec![ + "172.20.52.0/22".parse().unwrap(), + "172.20.26.0/24".parse().unwrap(), + ], shaper: None, checker: None, }], From 83dd4f07f88c278137fe17bf67b8ef457d73f13f Mon Sep 17 00:00:00 2001 From: Ryan Goodfellow Date: Tue, 27 Jan 2026 00:53:59 +0000 Subject: [PATCH 18/20] feed clippy --- sled-agent/tests/integration_tests/early_network.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sled-agent/tests/integration_tests/early_network.rs b/sled-agent/tests/integration_tests/early_network.rs index c7d59a836c0..76cb72b0148 100644 --- a/sled-agent/tests/integration_tests/early_network.rs +++ b/sled-agent/tests/integration_tests/early_network.rs @@ -4,7 +4,6 @@ //! Tests that EarlyNetworkConfig deserializes across versions. -use std::net::{IpAddr, Ipv4Addr}; use std::str::FromStr; use bootstore::schemes::v0 as bootstore; @@ -15,7 +14,7 @@ use omicron_common::api::{ external::{ImportExportPolicy, SwitchLocation}, internal::shared::{ BgpConfig, BgpPeerConfig, PortConfig, PortFec, PortSpeed, - RackNetworkConfig, RouteConfig, + RackNetworkConfig, }, }; use omicron_test_utils::dev::test_setup_log; From 579524b067ddb46a7b9cd014b4f136f53e8b3950 Mon Sep 17 00:00:00 2001 From: Levon Tarver Date: Thu, 29 Jan 2026 17:01:26 +0000 Subject: [PATCH 19/20] bump maghemite --- Cargo.lock | 6 +++--- Cargo.toml | 6 +++--- nexus/src/app/bgp.rs | 20 +++++++++++++------- package-manifest.toml | 12 ++++++------ tools/maghemite_ddm_openapi_version | 2 +- tools/maghemite_mg_openapi_version | 2 +- tools/maghemite_mgd_checksums | 4 ++-- 7 files changed, 29 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 851def23e9a..8b60019b2e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2514,7 +2514,7 @@ dependencies = [ [[package]] name = "ddm-admin-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=90110ca7b6d4c074fef651f8ca71bf3f5589a45e#90110ca7b6d4c074fef651f8ca71bf3f5589a45e" +source = "git+https://github.com/oxidecomputer/maghemite?rev=ce0c6cb4bb67a4ec5e208302504c22ddcb0b8d70#ce0c6cb4bb67a4ec5e208302504c22ddcb0b8d70" dependencies = [ "oxnet", "progenitor 0.11.2", @@ -6396,7 +6396,7 @@ dependencies = [ [[package]] name = "mg-admin-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=90110ca7b6d4c074fef651f8ca71bf3f5589a45e#90110ca7b6d4c074fef651f8ca71bf3f5589a45e" +source = "git+https://github.com/oxidecomputer/maghemite?rev=ce0c6cb4bb67a4ec5e208302504c22ddcb0b8d70#ce0c6cb4bb67a4ec5e208302504c22ddcb0b8d70" dependencies = [ "chrono", "colored 3.0.0", @@ -11478,7 +11478,7 @@ dependencies = [ [[package]] name = "rdb-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=90110ca7b6d4c074fef651f8ca71bf3f5589a45e#90110ca7b6d4c074fef651f8ca71bf3f5589a45e" +source = "git+https://github.com/oxidecomputer/maghemite?rev=ce0c6cb4bb67a4ec5e208302504c22ddcb0b8d70#ce0c6cb4bb67a4ec5e208302504c22ddcb0b8d70" dependencies = [ "oxnet", "schemars 0.8.22", diff --git a/Cargo.toml b/Cargo.toml index 072128322f0..6ed365a22be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -572,8 +572,8 @@ ntp-admin-api = { path = "ntp-admin/api" } ntp-admin-client = { path = "clients/ntp-admin-client" } ntp-admin-types = { path = "ntp-admin/types" } ntp-admin-types-versions = { path = "ntp-admin/types/versions" } -mg-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "90110ca7b6d4c074fef651f8ca71bf3f5589a45e" } -ddm-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "90110ca7b6d4c074fef651f8ca71bf3f5589a45e" } +mg-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "ce0c6cb4bb67a4ec5e208302504c22ddcb0b8d70" } +ddm-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "ce0c6cb4bb67a4ec5e208302504c22ddcb0b8d70" } multimap = "0.10.1" nexus-auth = { path = "nexus/auth" } nexus-background-task-interface = { path = "nexus/background-task-interface" } @@ -703,7 +703,7 @@ rats-corim = { git = "https://github.com/oxidecomputer/rats-corim.git", rev = "f raw-cpuid = { git = "https://github.com/oxidecomputer/rust-cpuid.git", rev = "a4cf01df76f35430ff5d39dc2fe470bcb953503b" } rayon = "1.10" rcgen = "0.12.1" -rdb-types = { git = "https://github.com/oxidecomputer/maghemite", rev = "90110ca7b6d4c074fef651f8ca71bf3f5589a45e" } +rdb-types = { git = "https://github.com/oxidecomputer/maghemite", rev = "ce0c6cb4bb67a4ec5e208302504c22ddcb0b8d70" } reconfigurator-cli = { path = "dev-tools/reconfigurator-cli" } reedline = "0.40.0" ref-cast = "1.0" diff --git a/nexus/src/app/bgp.rs b/nexus/src/app/bgp.rs index 0287ba18cf8..c39ce3e28ae 100644 --- a/nexus/src/app/bgp.rs +++ b/nexus/src/app/bgp.rs @@ -123,7 +123,7 @@ impl super::Nexus { for r in &router_info { let asn = r.asn; - let peers = match client.get_neighbors_v3(asn).await { + let peers = match client.get_neighbors_v4(asn).await { Ok(result) => result.into_inner(), Err(e) => { error!( @@ -179,13 +179,16 @@ impl super::Nexus { continue; } }; + for r in &router_info { let asn = r.asn; + let selector = mg_admin_client::types::ExportedSelector { + afi: Some(mg_admin_client::types::Afi::Ipv4), + asn, + peer: None, + }; - let exported = match client - .get_exported(&mg_admin_client::types::AsnSelector { asn }) - .await - { + let exported = match client.get_exported_v2(&selector).await { Ok(result) => result.into_inner(), Err(e) => { error!( @@ -240,7 +243,7 @@ impl super::Nexus { )) })? { let history = match client - .message_history_v2(&MessageHistoryRequest { + .message_history_v3(&MessageHistoryRequest { asn: sel.asn, direction: None, peer: None, @@ -283,7 +286,10 @@ impl super::Nexus { })? { let mut imported: Vec = Vec::new(); match client - .get_rib_imported(Some(&rdb_types::AddressFamily::Ipv4), None) + .get_rib_imported_v2( + Some(&rdb_types::AddressFamily::Ipv4), + None, + ) .await { Ok(result) => { diff --git a/package-manifest.toml b/package-manifest.toml index 08e8e2cc116..9cd1b0df9fd 100644 --- a/package-manifest.toml +++ b/package-manifest.toml @@ -654,10 +654,10 @@ source.repo = "maghemite" # `tools/maghemite_openapi_version`. Failing to do so will cause a failure when # building `ddm-admin-client` (which will instruct you to update # `tools/maghemite_openapi_version`). -source.commit = "90110ca7b6d4c074fef651f8ca71bf3f5589a45e" +source.commit = "ce0c6cb4bb67a4ec5e208302504c22ddcb0b8d70" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/maghemite/image//mg-ddm-gz.sha256.txt -source.sha256 = "834fe0c901568b54b55f8940fc3cf6685c225addd19bb175cc70f1843525958b" +source.sha256 = "6514c79d949c8a554fd870f20505fc1a0005e94a274f9071cd0aa7312df3b234" output.type = "tarball" [package.mg-ddm] @@ -670,10 +670,10 @@ source.repo = "maghemite" # `tools/maghemite_openapi_version`. Failing to do so will cause a failure when # building `ddm-admin-client` (which will instruct you to update # `tools/maghemite_openapi_version`). -source.commit = "90110ca7b6d4c074fef651f8ca71bf3f5589a45e" +source.commit = "ce0c6cb4bb67a4ec5e208302504c22ddcb0b8d70" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/maghemite/image//mg-ddm.sha256.txt -source.sha256 = "12cedd2c1107372f8b5a955be4c9c124fbc90a3194e701d938c654d301da02db" +source.sha256 = "52afcf5c9d45ad9a651594a2b6495f64eeb2c5213e2697c0b11df23ffd88ca16" output.type = "zone" output.intermediate_only = true @@ -685,10 +685,10 @@ source.repo = "maghemite" # `tools/maghemite_openapi_version`. Failing to do so will cause a failure when # building `ddm-admin-client` (which will instruct you to update # `tools/maghemite_openapi_version`). -source.commit = "90110ca7b6d4c074fef651f8ca71bf3f5589a45e" +source.commit = "ce0c6cb4bb67a4ec5e208302504c22ddcb0b8d70" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/maghemite/image//mgd.sha256.txt -source.sha256 = "176f63a305b83455a5a71b0b32405828cf70c04ee2d30e3bfe8243720b2289c2" +source.sha256 = "cb1f6419ba056ff7175329628bb9661647297021278742f1782e7d4e80d1a42e" output.type = "zone" output.intermediate_only = true diff --git a/tools/maghemite_ddm_openapi_version b/tools/maghemite_ddm_openapi_version index ebdfe895cac..416e65d722f 100644 --- a/tools/maghemite_ddm_openapi_version +++ b/tools/maghemite_ddm_openapi_version @@ -1 +1 @@ -COMMIT="90110ca7b6d4c074fef651f8ca71bf3f5589a45e" +COMMIT="ce0c6cb4bb67a4ec5e208302504c22ddcb0b8d70" diff --git a/tools/maghemite_mg_openapi_version b/tools/maghemite_mg_openapi_version index ebdfe895cac..416e65d722f 100644 --- a/tools/maghemite_mg_openapi_version +++ b/tools/maghemite_mg_openapi_version @@ -1 +1 @@ -COMMIT="90110ca7b6d4c074fef651f8ca71bf3f5589a45e" +COMMIT="ce0c6cb4bb67a4ec5e208302504c22ddcb0b8d70" diff --git a/tools/maghemite_mgd_checksums b/tools/maghemite_mgd_checksums index 1b04067a2d1..438450fda83 100644 --- a/tools/maghemite_mgd_checksums +++ b/tools/maghemite_mgd_checksums @@ -1,2 +1,2 @@ -CIDL_SHA256="176f63a305b83455a5a71b0b32405828cf70c04ee2d30e3bfe8243720b2289c2" -MGD_LINUX_SHA256="734c8c690f36d7872e066cd1d74e788d8dee8552f9453ba917d93a7eb666b971" \ No newline at end of file +CIDL_SHA256="cb1f6419ba056ff7175329628bb9661647297021278742f1782e7d4e80d1a42e" +MGD_LINUX_SHA256="819f1cd41cf4a3ebe12b7f8881b31c6a57325e1f234e6ff240769608ec6e4204" From 07ae84b3a89836b2173272d815d4405f14d6d332 Mon Sep 17 00:00:00 2001 From: Levon Tarver Date: Thu, 29 Jan 2026 23:26:38 +0000 Subject: [PATCH 20/20] fixup! Merge branch 'main' into ry/ipv6-all-the-things --- Cargo.lock | 1 - workspace-hack/Cargo.toml | 49 ++++++++++++++++++--------------------- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b1f413f262..9eb5cc62a77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9177,7 +9177,6 @@ dependencies = [ "ipnet", "ipnetwork", "itertools 0.10.5", - "itertools 0.12.1", "itertools 0.13.0", "lalrpop-util", "lazy_static", diff --git a/workspace-hack/Cargo.toml b/workspace-hack/Cargo.toml index cf55eeb2d6c..02ec30c3cf6 100644 --- a/workspace-hack/Cargo.toml +++ b/workspace-hack/Cargo.toml @@ -31,7 +31,7 @@ bstr = { version = "1.10.0" } buf-list = { version = "1.0.3", default-features = false, features = ["tokio1"] } byteorder = { version = "1.5.0" } bytes = { version = "1.10.1", features = ["serde"] } -chrono = { version = "0.4.42", features = ["serde"] } +chrono = { version = "0.4.43", features = ["serde"] } cipher = { version = "0.4.4", default-features = false, features = ["block-padding", "zeroize"] } clap = { version = "4.5.48", features = ["cargo", "derive", "env", "wrap_help"] } clap_builder = { version = "4.5.48", default-features = false, features = ["cargo", "color", "env", "std", "suggestions", "usage", "wrap_help"] } @@ -66,22 +66,21 @@ generic-array = { version = "0.14.7", default-features = false, features = ["mor getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2.15", default-features = false, features = ["js", "rdrand", "std"] } group = { version = "0.13.0", default-features = false, features = ["alloc"] } hashbrown = { version = "0.16.1", default-features = false, features = ["allocator-api2", "inline-more"] } -heck = { version = "0.4.1", features = ["unicode"] } hex = { version = "0.4.3", features = ["serde"] } hickory-proto = { version = "0.25.2", features = ["serde", "text-parsing"] } hmac = { version = "0.12.1", default-features = false, features = ["reset"] } hyper = { version = "1.8.1", features = ["full"] } iddqd = { version = "0.3.16", features = ["daft", "proptest", "schemars08"] } idna = { version = "1.0.3" } -indexmap = { version = "2.12.1", features = ["serde"] } +indexmap = { version = "2.13.0", features = ["serde"] } inout = { version = "0.1.3", default-features = false, features = ["std"] } ipnet = { version = "2.11.0", features = ["serde"] } ipnetwork = { version = "0.21.1", features = ["schemars", "serde"] } itertools-594e8ee84c453af0 = { package = "itertools", version = "0.13.0" } lalrpop-util = { version = "0.19.12" } lazy_static = { version = "1.5.0", default-features = false, features = ["spin_no_std"] } -libc = { version = "0.2.174", features = ["extra_traits"] } -log = { version = "0.4.27", default-features = false, features = ["kv_unstable", "std"] } +libc = { version = "0.2.180", features = ["extra_traits"] } +log = { version = "0.4.29", default-features = false, features = ["kv_unstable", "std"] } managed = { version = "0.8.0", default-features = false, features = ["alloc", "map"] } memchr = { version = "2.7.4" } newtype-uuid = { version = "1.3.2", features = ["proptest1"] } @@ -123,7 +122,7 @@ semver = { version = "1.0.27", features = ["serde"] } serde = { version = "1.0.228", features = ["alloc", "derive", "rc"] } serde_core = { version = "1.0.228", features = ["alloc", "rc"] } serde_json = { version = "1.0.149", features = ["alloc", "raw_value", "unbounded_depth"] } -serde_with = { version = "3.14.0", features = ["hex", "schemars_0_8"] } +serde_with = { version = "3.16.1", features = ["hex", "schemars_0_8"] } sha1 = { version = "0.10.6", features = ["oid"] } sha2 = { version = "0.10.9", features = ["oid"] } sha3 = { version = "0.10.8", features = ["oid"] } @@ -135,9 +134,9 @@ string_cache = { version = "0.8.9" } strum-2f80eeee3b1b6c7e = { package = "strum", version = "0.26.3", features = ["derive"] } strum-754bda37e0fb3874 = { package = "strum", version = "0.27.2", features = ["derive"] } subtle = { version = "2.6.1" } -syn-f595c2ba2a3f28df = { package = "syn", version = "2.0.111", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] } +syn-f595c2ba2a3f28df = { package = "syn", version = "2.0.114", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] } time = { version = "0.3.43", features = ["formatting", "local-offset", "macros", "parsing"] } -tokio = { version = "1.48.0", features = ["full", "test-util"] } +tokio = { version = "1.49.0", features = ["full", "test-util"] } tokio-postgres = { version = "0.7.13", features = ["with-chrono-0_4", "with-serde_json-1", "with-uuid-1"] } tokio-rustls = { version = "0.26.0", default-features = false, features = ["logging", "ring", "tls12"] } tokio-stream = { version = "0.1.17", features = ["net", "sync"] } @@ -173,7 +172,7 @@ buf-list = { version = "1.0.3", default-features = false, features = ["tokio1"] byteorder = { version = "1.5.0" } bytes = { version = "1.10.1", features = ["serde"] } cc = { version = "1.2.30", default-features = false, features = ["parallel"] } -chrono = { version = "0.4.42", features = ["serde"] } +chrono = { version = "0.4.43", features = ["serde"] } cipher = { version = "0.4.4", default-features = false, features = ["block-padding", "zeroize"] } clap = { version = "4.5.48", features = ["cargo", "derive", "env", "wrap_help"] } clap_builder = { version = "4.5.48", default-features = false, features = ["cargo", "color", "env", "std", "suggestions", "usage", "wrap_help"] } @@ -215,15 +214,15 @@ hmac = { version = "0.12.1", default-features = false, features = ["reset"] } hyper = { version = "1.8.1", features = ["full"] } iddqd = { version = "0.3.16", features = ["daft", "proptest", "schemars08"] } idna = { version = "1.0.3" } -indexmap = { version = "2.12.1", features = ["serde"] } +indexmap = { version = "2.13.0", features = ["serde"] } inout = { version = "0.1.3", default-features = false, features = ["std"] } ipnet = { version = "2.11.0", features = ["serde"] } ipnetwork = { version = "0.21.1", features = ["schemars", "serde"] } itertools-594e8ee84c453af0 = { package = "itertools", version = "0.13.0" } lalrpop-util = { version = "0.19.12" } lazy_static = { version = "1.5.0", default-features = false, features = ["spin_no_std"] } -libc = { version = "0.2.174", features = ["extra_traits"] } -log = { version = "0.4.27", default-features = false, features = ["kv_unstable", "std"] } +libc = { version = "0.2.180", features = ["extra_traits"] } +log = { version = "0.4.29", default-features = false, features = ["kv_unstable", "std"] } managed = { version = "0.8.0", default-features = false, features = ["alloc", "map"] } memchr = { version = "2.7.4" } newtype-uuid = { version = "1.3.2", features = ["proptest1"] } @@ -265,8 +264,8 @@ semver = { version = "1.0.27", features = ["serde"] } serde = { version = "1.0.228", features = ["alloc", "derive", "rc"] } serde_core = { version = "1.0.228", features = ["alloc", "rc"] } serde_json = { version = "1.0.149", features = ["alloc", "raw_value", "unbounded_depth"] } -serde_with = { version = "3.14.0", features = ["hex", "schemars_0_8"] } -serde_with_macros = { version = "3.14.0", default-features = false, features = ["schemars_0_8"] } +serde_with = { version = "3.16.1", features = ["hex", "schemars_0_8"] } +serde_with_macros = { version = "3.16.1", default-features = false, features = ["schemars_0_8"] } sha1 = { version = "0.10.6", features = ["oid"] } sha2 = { version = "0.10.9", features = ["oid"] } sha3 = { version = "0.10.8", features = ["oid"] } @@ -279,10 +278,10 @@ strum-2f80eeee3b1b6c7e = { package = "strum", version = "0.26.3", features = ["d strum-754bda37e0fb3874 = { package = "strum", version = "0.27.2", features = ["derive"] } subtle = { version = "2.6.1" } syn-dff4ba8e3ae991db = { package = "syn", version = "1.0.109", features = ["extra-traits", "fold", "full", "visit"] } -syn-f595c2ba2a3f28df = { package = "syn", version = "2.0.111", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] } +syn-f595c2ba2a3f28df = { package = "syn", version = "2.0.114", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] } time = { version = "0.3.43", features = ["formatting", "local-offset", "macros", "parsing"] } time-macros = { version = "0.2.24", default-features = false, features = ["formatting", "parsing"] } -tokio = { version = "1.48.0", features = ["full", "test-util"] } +tokio = { version = "1.49.0", features = ["full", "test-util"] } tokio-postgres = { version = "0.7.13", features = ["with-chrono-0_4", "with-serde_json-1", "with-uuid-1"] } tokio-rustls = { version = "0.26.0", default-features = false, features = ["logging", "ring", "tls12"] } tokio-stream = { version = "0.1.17", features = ["net", "sync"] } @@ -317,7 +316,7 @@ linux-raw-sys = { version = "0.4.14", default-features = false, features = ["elf miniz_oxide = { version = "0.8.5", default-features = false, features = ["with-alloc"] } mio = { version = "1.0.2", features = ["net", "os-ext"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38.37", features = ["event", "fs", "net", "pipe", "process", "stdio", "system", "termios", "time"] } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.0.7", features = ["fs", "stdio", "termios"] } +rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.1.3", features = ["fs", "stdio", "termios"] } zerocopy-ca01ad9e24f5d932 = { package = "zerocopy", version = "0.7.35", features = ["derive", "simd"] } [target.x86_64-unknown-linux-gnu.build-dependencies] @@ -332,7 +331,7 @@ linux-raw-sys = { version = "0.4.14", default-features = false, features = ["elf miniz_oxide = { version = "0.8.5", default-features = false, features = ["with-alloc"] } mio = { version = "1.0.2", features = ["net", "os-ext"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38.37", features = ["event", "fs", "net", "pipe", "process", "stdio", "system", "termios", "time"] } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.0.7", features = ["fs", "stdio", "termios"] } +rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.1.3", features = ["fs", "stdio", "termios"] } zerocopy-ca01ad9e24f5d932 = { package = "zerocopy", version = "0.7.35", features = ["derive", "simd"] } [target.x86_64-apple-darwin.dependencies] @@ -344,7 +343,7 @@ hyper-util = { version = "0.1.19", features = ["full"] } miniz_oxide = { version = "0.8.5", default-features = false, features = ["with-alloc"] } mio = { version = "1.0.2", features = ["net", "os-ext"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38.37", features = ["event", "fs", "net", "pipe", "process", "stdio", "system", "termios", "time"] } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.0.7", features = ["fs", "stdio", "termios"] } +rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.1.3", features = ["fs", "stdio", "termios"] } [target.x86_64-apple-darwin.build-dependencies] bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.9.4", default-features = false, features = ["std"] } @@ -355,7 +354,7 @@ hyper-util = { version = "0.1.19", features = ["full"] } miniz_oxide = { version = "0.8.5", default-features = false, features = ["with-alloc"] } mio = { version = "1.0.2", features = ["net", "os-ext"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38.37", features = ["event", "fs", "net", "pipe", "process", "stdio", "system", "termios", "time"] } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.0.7", features = ["fs", "stdio", "termios"] } +rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.1.3", features = ["fs", "stdio", "termios"] } [target.aarch64-apple-darwin.dependencies] bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.9.4", default-features = false, features = ["std"] } @@ -366,7 +365,7 @@ hyper-util = { version = "0.1.19", features = ["full"] } miniz_oxide = { version = "0.8.5", default-features = false, features = ["with-alloc"] } mio = { version = "1.0.2", features = ["net", "os-ext"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38.37", features = ["event", "fs", "net", "pipe", "process", "stdio", "system", "termios", "time"] } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.0.7", features = ["fs", "stdio", "termios"] } +rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.1.3", features = ["fs", "stdio", "termios"] } [target.aarch64-apple-darwin.build-dependencies] bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.9.4", default-features = false, features = ["std"] } @@ -377,7 +376,7 @@ hyper-util = { version = "0.1.19", features = ["full"] } miniz_oxide = { version = "0.8.5", default-features = false, features = ["with-alloc"] } mio = { version = "1.0.2", features = ["net", "os-ext"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38.37", features = ["event", "fs", "net", "pipe", "process", "stdio", "system", "termios", "time"] } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.0.7", features = ["fs", "stdio", "termios"] } +rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.1.3", features = ["fs", "stdio", "termios"] } [target.x86_64-unknown-illumos.dependencies] bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.9.4", default-features = false, features = ["std"] } @@ -387,13 +386,12 @@ dof-9fbad63c4bcf4a8f = { package = "dof", version = "0.4.0", default-features = getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3.4", default-features = false, features = ["std"] } hyper-rustls = { version = "0.27.7", features = ["http2", "ring", "webpki-tokio"] } hyper-util = { version = "0.1.19", features = ["full"] } -itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12.1" } itertools-93f6ce9d446188ac = { package = "itertools", version = "0.10.5" } miniz_oxide = { version = "0.8.5", default-features = false, features = ["with-alloc"] } mio = { version = "1.0.2", features = ["net", "os-ext"] } nom = { version = "7.1.3" } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38.37", features = ["event", "fs", "net", "pipe", "process", "stdio", "system", "termios", "time"] } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.0.7", features = ["fs", "stdio", "termios"] } +rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.1.3", features = ["fs", "stdio", "termios"] } toml_edit-cdcf2f9584511fe6 = { package = "toml_edit", version = "0.19.15", features = ["serde"] } winnow-3b31131e45eafb45 = { package = "winnow", version = "0.6.26", features = ["simd"] } zerocopy-ca01ad9e24f5d932 = { package = "zerocopy", version = "0.7.35", features = ["derive", "simd"] } @@ -407,13 +405,12 @@ dof-9fbad63c4bcf4a8f = { package = "dof", version = "0.4.0", default-features = getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3.4", default-features = false, features = ["std"] } hyper-rustls = { version = "0.27.7", features = ["http2", "ring", "webpki-tokio"] } hyper-util = { version = "0.1.19", features = ["full"] } -itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12.1" } itertools-93f6ce9d446188ac = { package = "itertools", version = "0.10.5" } miniz_oxide = { version = "0.8.5", default-features = false, features = ["with-alloc"] } mio = { version = "1.0.2", features = ["net", "os-ext"] } nom = { version = "7.1.3" } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38.37", features = ["event", "fs", "net", "pipe", "process", "stdio", "system", "termios", "time"] } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.0.7", features = ["fs", "stdio", "termios"] } +rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.1.3", features = ["fs", "stdio", "termios"] } toml_edit-cdcf2f9584511fe6 = { package = "toml_edit", version = "0.19.15", features = ["serde"] } winnow-3b31131e45eafb45 = { package = "winnow", version = "0.6.26", features = ["simd"] } zerocopy-ca01ad9e24f5d932 = { package = "zerocopy", version = "0.7.35", features = ["derive", "simd"] }