diff --git a/.github/buildomat/jobs/a4x2-deploy.sh b/.github/buildomat/jobs/a4x2-deploy.sh index c6d6b803c1e..86c703be60e 100755 --- a/.github/buildomat/jobs/a4x2-deploy.sh +++ b/.github/buildomat/jobs/a4x2-deploy.sh @@ -207,6 +207,9 @@ NO_COLOR=1 pfexec ./commtest \ --ip-pool-end 198.51.100.70 \ --icmp-loss-tolerance 500 \ --test-duration 200s \ - --packet-rate 10 + --packet-rate 10 \ + --mcast-group-ip 239.100.0.1 \ + --mcast-pool-begin 239.100.0.0 \ + --mcast-pool-end 239.100.0.255 cp connectivity-report.json /out/ diff --git a/dev-tools/omdb/src/bin/omdb/db/multicast.rs b/dev-tools/omdb/src/bin/omdb/db/multicast.rs index 68118b2ca80..7fbe4908bd8 100644 --- a/dev-tools/omdb/src/bin/omdb/db/multicast.rs +++ b/dev-tools/omdb/src/bin/omdb/db/multicast.rs @@ -27,13 +27,16 @@ //! | RANGE | ASM or SSM based on IP range | //! | UNDERLAY_IP | Underlay group IP (blank if none) | //! | SOURCES | Source allowlist, or "-" (any) | -//! | MEMBERS | Comma-separated "instance@sled" list | +//! | MEMBERS | Comma-separated "@sled" entries | //! | VNI | Virtual network ID | //! | CREATED | Creation timestamp | //! //! Note: SOURCES "-" also appears when a group has no members; use MEMBERS //! column or `info` command to distinguish from any-source members. //! +//! Note: MEMBERS uses `"name@sled"` for instances and `"probe:name@sled"` +//! for probes. +//! //! Filters: `--state`, `--pool` //! //! ## `omdb db multicast members` @@ -42,7 +45,8 @@ //! |--------------|------------------------------------------| //! | ID | Member UUID | //! | GROUP_NAME | Parent group name | -//! | PARENT_ID | Instance UUID | +//! | PARENT_KIND | "Instance" or "Probe" | +//! | PARENT_ID | Instance or Probe UUID per PARENT_KIND | //! | STATE | Member state ("Joining"/"Joined"/"Left") | //! | MULTICAST_IP | Group multicast IP | //! | SOURCES | Source allowlist, or "-" (any) | @@ -68,7 +72,8 @@ //! | Column | Description | //! |--------------|--------------------------------| //! | ID | Member UUID | -//! | INSTANCE | Instance name | +//! | PARENT_KIND | "Instance" or "Probe" | +//! | PARENT | Instance or probe name | //! | STATE | Member state | //! | MULTICAST_IP | Group multicast IP | //! | SOURCES | Allowlist, or "-" (any) | @@ -84,13 +89,14 @@ use async_bb8_diesel::AsyncRunQueryDsl; use chrono::{DateTime, Utc}; use clap::{Args, Subcommand, ValueEnum}; use diesel::prelude::*; +use itertools::{Either, Itertools}; use tabled::Tabled; use uuid::Uuid; use nexus_db_model::{ ExternalMulticastGroup, IpPool, IpPoolRange, IpPoolType, - MulticastGroupMember, MulticastGroupMemberState, MulticastGroupState, - UnderlayMulticastGroup, + MulticastGroupMember, MulticastGroupMemberParentKind, + MulticastGroupMemberState, MulticastGroupState, UnderlayMulticastGroup, }; use nexus_db_queries::db::DataStore; use nexus_types::identity::Resource; @@ -247,6 +253,7 @@ struct MulticastGroupRow { struct MulticastMemberRow { id: Uuid, group_name: String, + parent_kind: MulticastGroupMemberParentKind, parent_id: Uuid, state: MulticastGroupMemberState, multicast_ip: std::net::IpAddr, @@ -262,7 +269,8 @@ struct MulticastMemberRow { #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] struct MulticastInfoMemberRow { id: Uuid, - instance: String, + parent_kind: MulticastGroupMemberParentKind, + parent: String, state: MulticastGroupMemberState, multicast_ip: std::net::IpAddr, /// Source IPs for source filtering ("-" = any source) @@ -293,6 +301,7 @@ pub(super) async fn cmd_db_multicast_groups( use nexus_db_schema::schema::ip_pool::dsl as pool_dsl; use nexus_db_schema::schema::multicast_group::dsl; use nexus_db_schema::schema::multicast_group_member::dsl as member_dsl; + use nexus_db_schema::schema::probe::dsl as probe_dsl; use nexus_db_schema::schema::sled::dsl as sled_dsl; use nexus_db_schema::schema::underlay_multicast_group::dsl as underlay_dsl; @@ -401,20 +410,37 @@ pub(super) async fn cmd_db_multicast_groups( map }; - // Batch lookup instance names - let parent_ids: Vec = members.iter().map(|m| m.parent_id).collect(); - let instance_names: HashMap = if parent_ids.is_empty() { - HashMap::new() - } else { - instance_dsl::instance - .filter(instance_dsl::id.eq_any(parent_ids)) + // Batch lookup parent names per kind. `parent_id` is opaque at the + // table level. The discriminator on the member row selects which + // table (`instance` or `probe`) to resolve it against. + let (instance_ids, probe_ids): (Vec, Vec) = + members.iter().partition_map(|member| match member.parent_kind { + MulticastGroupMemberParentKind::Instance => { + Either::Left(member.parent_id) + } + MulticastGroupMemberParentKind::Probe => { + Either::Right(member.parent_id) + } + }); + let mut parent_names: HashMap = HashMap::new(); + if !instance_ids.is_empty() { + let rows: Vec<(Uuid, String)> = instance_dsl::instance + .filter(instance_dsl::id.eq_any(instance_ids)) .select((instance_dsl::id, instance_dsl::name)) - .get_results_async::<(Uuid, String)>(&*conn) + .get_results_async(&*conn) .await - .context("fetching instance names")? - .into_iter() - .collect() - }; + .context("fetching instance names")?; + parent_names.extend(rows); + } + if !probe_ids.is_empty() { + let rows: Vec<(Uuid, String)> = probe_dsl::probe + .filter(probe_dsl::id.eq_any(probe_ids)) + .select((probe_dsl::id, probe_dsl::name)) + .get_results_async(&*conn) + .await + .context("fetching probe names")?; + parent_names.extend(rows); + } // Batch lookup sled serials let sled_ids: Vec = members @@ -434,23 +460,30 @@ pub(super) async fn cmd_db_multicast_groups( .collect() }; - // Build group_id -> formatted members string - let mut members_map: HashMap> = HashMap::new(); - for member in &members { - let inst_name = instance_names - .get(&member.parent_id) - .cloned() - .unwrap_or_else(|| member.parent_id.to_string()); - let sled_serial = member - .sled_id - .and_then(|s| sled_serials.get(&s.into_untyped_uuid()).cloned()) - .unwrap_or_else(|| "-".to_string()); - let formatted = format!("{inst_name}@{sled_serial}"); - members_map - .entry(member.external_group_id) - .or_default() - .push(formatted); - } + // Build group_id -> formatted members string. The parent prefix + // distinguishes instances ("name@sled") from probes ("probe:name@sled"). + let mut members_map: HashMap> = members + .iter() + .map(|member| { + let parent_name = parent_names + .get(&member.parent_id) + .cloned() + .unwrap_or_else(|| member.parent_id.to_string()); + let sled_serial = member + .sled_id + .and_then(|s| sled_serials.get(&s.into_untyped_uuid()).cloned()) + .unwrap_or_else(|| "-".to_string()); + let formatted = match member.parent_kind { + MulticastGroupMemberParentKind::Instance => { + format!("{parent_name}@{sled_serial}") + } + MulticastGroupMemberParentKind::Probe => { + format!("probe:{parent_name}@{sled_serial}") + } + }; + (member.external_group_id, formatted) + }) + .into_group_map(); // Sort each group's members for deterministic output for v in members_map.values_mut() { @@ -611,6 +644,7 @@ pub(super) async fn cmd_db_multicast_members( MulticastMemberRow { id: member.id, group_name, + parent_kind: member.parent_kind, parent_id: member.parent_id, state: member.state, multicast_ip: member.multicast_ip.ip(), @@ -719,6 +753,7 @@ pub(super) async fn cmd_db_multicast_info( use nexus_db_schema::schema::ip_pool::dsl as pool_dsl; use nexus_db_schema::schema::multicast_group::dsl as group_dsl; use nexus_db_schema::schema::multicast_group_member::dsl as member_dsl; + use nexus_db_schema::schema::probe::dsl as probe_dsl; use nexus_db_schema::schema::sled::dsl as sled_dsl; use nexus_db_schema::schema::underlay_multicast_group::dsl as underlay_dsl; @@ -867,17 +902,37 @@ pub(super) async fn cmd_db_multicast_info( } else { println!("\nMEMBERS ({}):", members.len()); - // Batch lookup instance names (parent_id references instances) - let parent_ids: Vec = - members.iter().map(|member| member.parent_id).collect(); - let instances: Vec<(Uuid, String)> = instance_dsl::instance - .filter(instance_dsl::id.eq_any(parent_ids)) - .select((instance_dsl::id, instance_dsl::name)) - .get_results_async(&*conn) - .await - .context("fetching instance names")?; - let instance_map: HashMap = - instances.into_iter().collect(); + // Batch lookup parent names per kind. `parent_id` is opaque at the + // table level. The discriminator on the member row selects which + // table (`instance` or `probe`) to resolve it against. + let (instance_ids, probe_ids): (Vec, Vec) = + members.iter().partition_map(|member| match member.parent_kind { + MulticastGroupMemberParentKind::Instance => { + Either::Left(member.parent_id) + } + MulticastGroupMemberParentKind::Probe => { + Either::Right(member.parent_id) + } + }); + let mut parent_map: HashMap = HashMap::new(); + if !instance_ids.is_empty() { + let rows: Vec<(Uuid, String)> = instance_dsl::instance + .filter(instance_dsl::id.eq_any(instance_ids)) + .select((instance_dsl::id, instance_dsl::name)) + .get_results_async(&*conn) + .await + .context("fetching instance names")?; + parent_map.extend(rows); + } + if !probe_ids.is_empty() { + let rows: Vec<(Uuid, String)> = probe_dsl::probe + .filter(probe_dsl::id.eq_any(probe_ids)) + .select((probe_dsl::id, probe_dsl::name)) + .get_results_async(&*conn) + .await + .context("fetching probe names")?; + parent_map.extend(rows); + } // Batch lookup sled serials let sled_ids: Vec = members @@ -899,7 +954,7 @@ pub(super) async fn cmd_db_multicast_info( let rows: Vec = members .into_iter() .map(|member| { - let instance_name = instance_map + let parent_name = parent_map .get(&member.parent_id) .cloned() .unwrap_or_else(|| member.parent_id.to_string()); @@ -918,7 +973,8 @@ pub(super) async fn cmd_db_multicast_info( .unwrap_or_else(|| "-".to_string()); MulticastInfoMemberRow { id: member.id, - instance: instance_name, + parent_kind: member.parent_kind, + parent: parent_name, state: member.state, multicast_ip: member.multicast_ip.ip(), sources, diff --git a/dev-tools/omdb/tests/successes.out b/dev-tools/omdb/tests/successes.out index b7c5d28cca4..5b424c9336e 100644 --- a/dev-tools/omdb/tests/successes.out +++ b/dev-tools/omdb/tests/successes.out @@ -99,7 +99,7 @@ EXECUTING COMMAND: omdb ["db", "multicast", "members"] termination: Exited(0) --------------------------------------------- stdout: -ID GROUP_NAME PARENT_ID STATE MULTICAST_IP SOURCES SLED_ID CREATED +ID GROUP_NAME PARENT_KIND PARENT_ID STATE MULTICAST_IP SOURCES SLED_ID CREATED --------------------------------------------- stderr: note: using database URL postgresql://root@[::1]:REDACTED_PORT/omicron?sslmode=disable diff --git a/dev-tools/omdb/tests/test_multicast.rs b/dev-tools/omdb/tests/test_multicast.rs index d1d761fd372..0498f1d57c2 100644 --- a/dev-tools/omdb/tests/test_multicast.rs +++ b/dev-tools/omdb/tests/test_multicast.rs @@ -129,7 +129,7 @@ async fn wait_for_member_state( .await .items; if let Some(member) = - members.iter().find(|m| m.instance_id == instance_id) + members.iter().find(|m| m.parent_id == instance_id) { if member.state == expected_state_str { Ok(member.clone()) diff --git a/end-to-end-tests/src/bin/commtest.rs b/end-to-end-tests/src/bin/commtest.rs index cd9d3ada201..ce6575b1409 100644 --- a/end-to-end-tests/src/bin/commtest.rs +++ b/end-to-end-tests/src/bin/commtest.rs @@ -7,9 +7,10 @@ use oxide_client::{ ClientSystemHardwareExt, ClientSystemIpPoolsExt, ClientSystemStatusExt, ClientVpcsExt, types::{ - IpPoolCreate, IpPoolLinkSilo, IpPoolType, IpRange, IpVersion, Name, - NameOrId, PingStatus, PoolSelector, ProbeCreate, ProbeInfo, - ProjectCreate, UsernamePasswordCredentials, + IpPoolCreate, IpPoolLinkSilo, IpPoolType, IpRange, IpVersion, + MulticastGroupIdentifier, MulticastGroupJoinSpec, Name, NameOrId, + PingStatus, PoolSelector, ProbeCreate, ProbeInfo, ProjectCreate, + UsernamePasswordCredentials, }, }; use std::{ @@ -60,6 +61,49 @@ struct RunArgs { /// Last address in the IP pool to use for testing #[arg(long)] ip_pool_end: IpAddr, + + /// Multicast group address to enroll probes into. When present, the + /// run additionally creates a multicast IP pool covering this address + /// (see `--mcast-pool-{begin,end}`), enrolls each probe at + /// `probe_create` time, and verifies that the probe memberships + /// reach "Joined" state via the multicast reconciler. Omit to skip + /// the multicast phase. + #[arg(long, requires_all = ["mcast_pool_begin", "mcast_pool_end"])] + mcast_group_ip: Option, + + /// First address in the multicast IP pool range covering + /// `--mcast-group-ip`. + #[arg(long, requires_all = ["mcast_group_ip", "mcast_pool_end"])] + mcast_pool_begin: Option, + + /// Last address in the multicast IP pool range covering + /// `--mcast-group-ip`. + #[arg(long, requires_all = ["mcast_group_ip", "mcast_pool_begin"])] + mcast_pool_end: Option, +} + +/// Multicast configuration extracted from the CLI args. +/// +/// Either all three fields are present (when `--mcast-group-ip` and the +/// two pool-range args are supplied) or the whole struct is absent. +/// The all-or-nothing invariant is enforced at clap parse time via +/// `requires_all` on the arg attributes. +#[derive(Debug, Clone, Copy)] +struct McastConfig { + group_ip: IpAddr, + pool_begin: IpAddr, + pool_end: IpAddr, +} + +impl RunArgs { + /// Returns the multicast configuration if `--mcast-group-ip` et al. + /// were supplied, or `None` to skip the multicast phase. + fn mcast(&self) -> Option { + let group_ip = self.mcast_group_ip?; + let pool_begin = self.mcast_pool_begin?; + let pool_end = self.mcast_pool_end?; + Some(McastConfig { group_ip, pool_begin, pool_end }) + } } const API_RETRY_ATTEMPTS: usize = 15; @@ -77,7 +121,7 @@ fn main() -> Result<()> { async fn run(cli: &Cli, args: &RunArgs) -> Result<()> { wait_until_oxide_api_is_available(cli).await?; let (sleds, oxide) = rack_prepare(cli, args).await?; - let addrs = launch_probes(sleds, &oxide).await?; + let addrs = launch_probes(args, sleds, &oxide).await?; test_connectivity(args, addrs)?; Ok(()) } @@ -357,6 +401,11 @@ async fn rack_prepare( println!("ip range already exists"); } + if let Some(cfg) = args.mcast() { + ensure_mcast_pool(&oxide, cfg.group_ip, cfg.pool_begin, cfg.pool_end) + .await?; + } + print!("getting sled ids ... "); let sleds = api_retry!(oxide.sled_list().limit(u32::MAX).send().await)? .into_inner() @@ -369,10 +418,124 @@ async fn rack_prepare( Ok((sleds, oxide)) } +const MCAST_POOL_NAME: &str = "mcast"; + +async fn ensure_mcast_pool( + oxide: &oxide_client::Client, + group_ip: IpAddr, + range_begin: IpAddr, + range_end: IpAddr, +) -> Result<()> { + let ip_version = + if group_ip.is_ipv4() { IpVersion::V4 } else { IpVersion::V6 }; + api_retry!(if let Err(e) = + oxide.system_ip_pool_view().pool(MCAST_POOL_NAME).send().await + { + if let Some(reqwest::StatusCode::NOT_FOUND) = e.status() { + print!("multicast ip pool does not exist, creating ... "); + oxide + .system_ip_pool_create() + .body(IpPoolCreate { + name: MCAST_POOL_NAME.parse().unwrap(), + description: "Multicast IP pool".to_string(), + ip_version, + pool_type: IpPoolType::Multicast, + }) + .send() + .await?; + println!("done"); + Ok(()) + } else { + Err(e) + } + } else { + println!("multicast ip pool already exists"); + Ok(()) + })?; + + // The silo link is idempotent on re-runs: a 4xx from the API on the + // re-link is treated as already-linked. + if let Err(e) = oxide + .system_ip_pool_silo_link() + .pool(MCAST_POOL_NAME) + .body(IpPoolLinkSilo { + silo: NameOrId::Name("recovery".parse().unwrap()), + is_default: false, + }) + .send() + .await + { + match e.status() { + Some(s) if s.is_client_error() => { + println!("multicast pool already linked to silo"); + } + _ => Err(e)?, + } + } + + let pool = api_retry!( + oxide + .system_ip_pool_range_list() + .limit(u32::MAX) + .pool(Name::try_from(MCAST_POOL_NAME).unwrap()) + .send() + .await + )? + .into_inner() + .items; + + let range = try_create_ip_range(range_begin, range_end)?; + let range_exists = + pool.iter().any(|pool_range| match (&range, &pool_range.range) { + (IpRange::V4(r1), IpRange::V4(r2)) => { + r1.first == r2.first && r1.last == r2.last + } + (IpRange::V6(r1), IpRange::V6(r2)) => { + r1.first == r2.first && r1.last == r2.last + } + (_, _) => false, + }); + if !range_exists { + print!("multicast ip range does not exist, creating ... "); + api_retry!( + oxide + .system_ip_pool_range_add() + .pool(Name::try_from(MCAST_POOL_NAME).unwrap()) + .body(range.clone()) + .send() + .await + )?; + println!("done"); + } else { + println!("multicast ip range already exists"); + } + Ok(()) +} + async fn launch_probes( + args: &RunArgs, sleds: Vec, oxide: &oxide_client::Client, ) -> Result> { + // When multicast is configured, the group IP is passed as the group + // identifier so the first probe creation auto-creates the group out + // of the multicast pool. Subsequent probes resolve to the same + // group and attach as new members. + // + // Note: This is empty when running unicast-only. + let multicast_groups: Vec = args + .mcast() + .map(|cfg| { + vec![MulticastGroupJoinSpec { + group: MulticastGroupIdentifier(cfg.group_ip.to_string()), + ip_version: None, + source_ips: None, + }] + }) + .unwrap_or_default(); + // Probes are created sequentially so the first one drives the group + // auto-create unambiguously, while later probes find the existing group + // and add themselves as members. for (i, sled) in sleds.into_iter().enumerate() { println!("checking if probe{i} exists"); api_retry!(if let Err(e) = oxide @@ -394,6 +557,7 @@ async fn launch_probes( }, name: format!("probe{i}").parse().unwrap(), sled, + multicast_groups: multicast_groups.clone(), }) .send() .await?; diff --git a/nexus/db-model/src/multicast_group.rs b/nexus/db-model/src/multicast_group.rs index c9224b624e1..c34d69fb280 100644 --- a/nexus/db-model/src/multicast_group.rs +++ b/nexus/db-model/src/multicast_group.rs @@ -93,7 +93,7 @@ use nexus_db_schema::schema::{ }; use nexus_types::external_api::multicast as multicast_types; use omicron_common::api::external::{self, IdentityMetadata}; -use omicron_uuid_kinds::SledKind; +use omicron_uuid_kinds::{GenericUuid, InstanceUuid, ProbeUuid, SledKind}; use crate::typed_uuid::DbTypedUuid; use crate::{Generation, Name, SqlU8, Vni, impl_enum_type}; @@ -121,6 +121,16 @@ impl_enum_type!( Left => b"left" ); +impl_enum_type!( + MulticastGroupMemberParentKindEnum: + + #[derive(Clone, Copy, Debug, PartialEq, Eq, AsExpression, FromSqlRow, Serialize, Deserialize, JsonSchema)] + pub enum MulticastGroupMemberParentKind; + + Instance => b"instance" + Probe => b"probe" +); + impl std::fmt::Display for MulticastGroupState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match self { @@ -142,6 +152,15 @@ impl std::fmt::Display for MulticastGroupMemberState { } } +impl std::fmt::Display for MulticastGroupMemberParentKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + MulticastGroupMemberParentKind::Instance => "Instance", + MulticastGroupMemberParentKind::Probe => "Probe", + }) + } +} + /// Type alias for lookup resource naming convention. /// /// This alias maps the generic name [MulticastGroup] to [ExternalMulticastGroup], @@ -209,6 +228,71 @@ pub struct ExternalMulticastGroup { pub underlay_salt: Option, } +/// A reference to the resource that owns a multicast group member (its +/// "parent"), distinguishing between the parent kinds the control +/// plane supports. +/// +/// Multicast group members can have either an instance or a probe (used +/// for testing network connectivity) as their parent. The two kinds resolve +/// their hosting sled differently. Instances look up their `sled_id` through +/// the `vmm` table via `instance.active_propolis_id`, while probes store `sled` +/// directly on the probe row. The parent kind also determines which sled-agent +/// endpoints the reconciler invokes for join/leave actions. +/// +/// The discriminator is persisted on the member row as `parent_kind` +/// (see [`MulticastGroupMemberParentKind`]). +/// +/// Note: `parent_kind` is an enum column rather than two booleans +/// (`is_instance` / `is_probe`) the way [`ExternalIp`] discriminates +/// (`is_service` / `is_probe`). The enum approach matches +/// [`NetworkInterfaceKind`] (Instance / Service / Probe) and makes +/// invalid combinations unrepresentable. +/// +/// [`ExternalIp`]: crate::ExternalIp +/// [`NetworkInterfaceKind`]: crate::NetworkInterfaceKind +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum MemberParentRef { + /// Member is owned by an instance. The hosting sled is resolved via + /// `instance` joined with `vmm` on `active_propolis_id`. + Instance(InstanceUuid), + /// Member is owned by a probe. The hosting sled is read directly + /// from the `probe` row. + Probe(ProbeUuid), +} + +impl MemberParentRef { + /// Returns the underlying parent UUID without distinguishing kind. + pub fn as_uuid(&self) -> Uuid { + match self { + MemberParentRef::Instance(id) => *id.as_untyped_uuid(), + MemberParentRef::Probe(id) => *id.as_untyped_uuid(), + } + } + + /// Returns the persisted [`MulticastGroupMemberParentKind`] discriminator + /// for this parent. + pub fn kind(&self) -> MulticastGroupMemberParentKind { + match self { + MemberParentRef::Instance(_) => { + MulticastGroupMemberParentKind::Instance + } + MemberParentRef::Probe(_) => MulticastGroupMemberParentKind::Probe, + } + } +} + +impl From for MemberParentRef { + fn from(id: InstanceUuid) -> Self { + MemberParentRef::Instance(id) + } +} + +impl From for MemberParentRef { + fn from(id: ProbeUuid) -> Self { + MemberParentRef::Probe(id) + } +} + /// Values used to create a [MulticastGroupMember] in the database. /// /// This struct is used for database insertions and omits fields that are @@ -229,6 +313,7 @@ pub struct MulticastGroupMemberValues { pub multicast_ip: IpNetwork, /// Source IPs for source-filtered multicast (optional for ASM, required for SSM). pub source_ips: Vec, + pub parent_kind: MulticastGroupMemberParentKind, // version_added and version_removed are omitted - database assigns these // via DEFAULT nextval() } @@ -257,7 +342,7 @@ pub struct MulticastGroupMember { pub time_deleted: Option>, /// External multicast group this member belongs to. pub external_group_id: Uuid, - /// Parent instance or service that receives multicast traffic. + /// Parent instance or probe that receives multicast traffic. pub parent_id: Uuid, /// Sled hosting the parent. pub sled_id: Option>, @@ -272,6 +357,8 @@ pub struct MulticastGroupMember { pub multicast_ip: IpNetwork, /// Source IPs for source-filtered multicast (optional for ASM, required for SSM). pub source_ips: Vec, + /// Discriminator for `parent_id`. + pub parent_kind: MulticastGroupMemberParentKind, } // Conversions to external API views @@ -280,6 +367,15 @@ impl TryFrom for multicast_types::MulticastGroupMember { type Error = external::Error; fn try_from(member: MulticastGroupMember) -> Result { + let parent = member.parent_ref(); + let kind = match parent { + MemberParentRef::Instance(_) => { + multicast_types::MulticastGroupMemberParentKind::Instance + } + MemberParentRef::Probe(_) => { + multicast_types::MulticastGroupMemberParentKind::Probe + } + }; Ok(multicast_types::MulticastGroupMember { identity: IdentityMetadata { id: member.id, @@ -294,7 +390,8 @@ impl TryFrom for multicast_types::MulticastGroupMember { }, multicast_group_id: member.external_group_id, multicast_ip: member.multicast_ip.ip(), - instance_id: member.parent_id, + kind, + parent_id: parent.as_uuid(), source_ips: member .source_ips .into_iter() @@ -354,7 +451,7 @@ impl MulticastGroupMember { id: Uuid, external_group_id: Uuid, multicast_ip: IpNetwork, - parent_id: Uuid, + parent: MemberParentRef, sled_id: Option>, source_ips: Vec, ) -> Self { @@ -365,7 +462,8 @@ impl MulticastGroupMember { time_deleted: None, external_group_id, multicast_ip, - parent_id, + parent_id: parent.as_uuid(), + parent_kind: parent.kind(), sled_id, state: MulticastGroupMemberState::Joining, source_ips, @@ -374,6 +472,48 @@ impl MulticastGroupMember { version_removed: None, } } + + /// Reconstruct the typed [`MemberParentRef`] from the persisted + /// `(parent_id, parent_kind)` pair. The inverse of the split done in + /// [`MulticastGroupMember::new`]. + pub fn parent_ref(&self) -> MemberParentRef { + match self.parent_kind { + MulticastGroupMemberParentKind::Instance => { + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + self.parent_id, + )) + } + MulticastGroupMemberParentKind::Probe => MemberParentRef::Probe( + ProbeUuid::from_untyped_uuid(self.parent_id), + ), + } + } + + /// Typed accessor for the parent when it is an instance. + /// + /// # Returns + /// + /// `Some(InstanceUuid)` if `parent_kind` is + /// [`MulticastGroupMemberParentKind::Instance`], `None` otherwise. + pub fn instance_id(&self) -> Option { + match self.parent_ref() { + MemberParentRef::Instance(id) => Some(id), + MemberParentRef::Probe(_) => None, + } + } + + /// Typed accessor for the parent when it is a probe. + /// + /// # Returns + /// + /// `Some(ProbeUuid)` if `parent_kind` is + /// [`MulticastGroupMemberParentKind::Probe`], `None` otherwise. + pub fn probe_id(&self) -> Option { + match self.parent_ref() { + MemberParentRef::Probe(id) => Some(id), + MemberParentRef::Instance(_) => None, + } + } } /// Database representation of an underlay multicast group. diff --git a/nexus/db-model/src/schema_versions.rs b/nexus/db-model/src/schema_versions.rs index 5caf71e9e28..94d45d2f391 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(261, 0, 0); +pub const SCHEMA_VERSION: Version = Version::new(262, 0, 0); /// List of all past database schema versions, in *reverse* order /// @@ -28,6 +28,7 @@ pub 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(262, "multicast-member-parent-kind"), KnownVersion::new(261, "remove-add-zones-with-mupdate-override"), KnownVersion::new(260, "ereport-trim-serial-trailing-nulls"), KnownVersion::new(259, "vmm-failure-reason"), diff --git a/nexus/db-queries/src/db/datastore/multicast/groups.rs b/nexus/db-queries/src/db/datastore/multicast/groups.rs index 4f78463b849..800720c3ee2 100644 --- a/nexus/db-queries/src/db/datastore/multicast/groups.rs +++ b/nexus/db-queries/src/db/datastore/multicast/groups.rs @@ -932,7 +932,7 @@ mod tests { use crate::db::datastore::LookupType; use crate::db::model::{ IncompleteIpPoolResource, IpPool, IpPoolReservationType, - IpPoolResourceType, IpVersion, + IpPoolResourceType, IpVersion, MemberParentRef, }; use crate::db::pub_test_utils::helpers::create_instance_with_vmm; use crate::db::pub_test_utils::{TestDatabase, multicast}; @@ -1969,10 +1969,12 @@ mod tests { .await; datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, group_id, - InstanceUuid::from_untyped_uuid(*instance.as_untyped_uuid()), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + *instance.as_untyped_uuid(), + )), Some(&[] as &[IpAddr]), ) .await diff --git a/nexus/db-queries/src/db/datastore/multicast/members.rs b/nexus/db-queries/src/db/datastore/multicast/members.rs index f65c40cc753..de2fd892466 100644 --- a/nexus/db-queries/src/db/datastore/multicast/members.rs +++ b/nexus/db-queries/src/db/datastore/multicast/members.rs @@ -27,7 +27,8 @@ use crate::context::OpContext; use crate::db::datastore::DataStore; use crate::db::datastore::multicast::ops; use crate::db::model::{ - DbTypedUuid, MulticastGroupMember, MulticastGroupMemberState, + DbTypedUuid, MemberParentRef, MulticastGroupMember, + MulticastGroupMemberParentKind, MulticastGroupMemberState, }; use crate::db::pagination::paginated; @@ -75,14 +76,17 @@ impl DataStore { .await } - /// Attach an instance to a multicast group as a member. + /// Attach a parent (instance or probe) to a multicast group as a member. /// - /// Uses an atomic CTE to validate group/instance and perform upsert in a - /// single database round-trip. The CTE is TOCTOU-safe, i.e., group state - /// and instance existence are validated atomically with the upsert. + /// Uses an atomic CTE to validate group and parent existence and perform + /// the member upsert in a single database round-trip. The CTE is + /// TOCTOU-safe, i.e., group state and parent existence are validated + /// atomically with the upsert. The parent kind is persisted on the + /// member row so the reconciler can dispatch on it without re-querying + /// the parent tables. /// /// Creates a member record in "Joining" state. The RPW reconciler - /// programs the dataplane when the instance starts. + /// programs the dataplane when the parent comes up. /// /// Handles reactivation of "Left" members and preserves "Joined" state for /// idempotency. @@ -95,11 +99,11 @@ impl DataStore { /// Atomically enforces the per-group source IP union cap /// ([`omicron_common::address::MAX_SOURCE_IPS_PER_GROUP`]) when a /// non-empty source list is being applied. - pub async fn multicast_group_member_attach_to_instance( + pub async fn multicast_group_member_attach( &self, opctx: &OpContext, group_id: MulticastGroupUuid, - instance_id: InstanceUuid, + parent: MemberParentRef, source_ips: Option<&[IpAddr]>, ) -> CreateResult { let conn = self.pool_connection_authorized(opctx).await?; @@ -109,12 +113,12 @@ impl DataStore { .map(|ips| ips.iter().copied().map(IpNetwork::from).collect()); // Execute atomic CTE that validates group (not "Deleting"), validates - // instance, gets `sled_id`, performs upsert, and returns full member - // record + // the parent (instance or probe), gets `sled_id`, performs upsert, + // and returns the full member record. let attach_result = ops::member_attach::AttachMemberToGroupStatement::new( group_id.into_untyped_uuid(), - instance_id.into_untyped_uuid(), + parent, Uuid::new_v4(), source_networks, ) @@ -136,8 +140,7 @@ impl DataStore { ) -> DeleteResult { use nexus_db_schema::schema::multicast_group_member::dsl; - // Delete all members for this group, including soft-deleted ones - // We use a targeted query to leverage existing indexes + // Delete all members for this group, including soft-deleted ones. diesel::delete(dsl::multicast_group_member) .filter(dsl::external_group_id.eq(group_id.into_untyped_uuid())) .execute_async(&*self.pool_connection_authorized(opctx).await?) @@ -146,35 +149,37 @@ impl DataStore { .map(|_x| ()) } - /// Delete a specific multicast group member by group and instance ID. + /// Delete a multicast member by `(group, parent)` pair. /// - /// This performs a hard delete of the specific member (both active and soft-deleted) - /// for the given (group, instance) pair. Used during saga undo operations to - /// clean up only the member created by that saga, not affecting other instances' - /// memberships in the same group. - pub async fn multicast_group_member_delete_by_group_and_instance( + /// Hard-deletes the row (active or soft-deleted) for the given group and + /// parent (instance or probe). Used during saga undo to clean up only the + /// member created by that saga, leaving other parents' memberships in the + /// same group intact. + pub async fn multicast_group_member_delete_by_group_and_parent( &self, opctx: &OpContext, group_id: MulticastGroupUuid, - instance_id: InstanceUuid, + parent: MemberParentRef, ) -> DeleteResult { use nexus_db_schema::schema::multicast_group_member::dsl; diesel::delete(dsl::multicast_group_member) .filter(dsl::external_group_id.eq(group_id.into_untyped_uuid())) - .filter(dsl::parent_id.eq(instance_id.into_untyped_uuid())) + .filter(dsl::parent_id.eq(parent.as_uuid())) + .filter(dsl::parent_kind.eq(parent.kind())) .execute_async(&*self.pool_connection_authorized(opctx).await?) .await .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) .map(|_x| ()) } - /// Set the state of a multicast group member. - pub async fn multicast_group_member_set_state( + /// Set the state of a multicast group member identified by its + /// `(group, parent)` pair, regardless of parent kind. + pub async fn multicast_group_member_set_state_for_parent( &self, opctx: &OpContext, external_group_id: MulticastGroupUuid, - parent_id: InstanceUuid, + parent: MemberParentRef, new_state: MulticastGroupMemberState, ) -> UpdateResult<()> { use nexus_db_schema::schema::multicast_group_member::dsl; @@ -184,7 +189,8 @@ impl DataStore { dsl::external_group_id .eq(external_group_id.into_untyped_uuid()), ) - .filter(dsl::parent_id.eq(parent_id.into_untyped_uuid())) + .filter(dsl::parent_id.eq(parent.as_uuid())) + .filter(dsl::parent_kind.eq(parent.kind())) .filter(dsl::time_deleted.is_null()) .set((dsl::state.eq(new_state), dsl::time_modified.eq(Utc::now()))) .execute_async(&*self.pool_connection_authorized(opctx).await?) @@ -209,18 +215,20 @@ impl DataStore { Ok(()) } - /// Conditionally set the state of a multicast group member if the current - /// state matches `expected_state`. + /// Conditionally set the state of a multicast group member if the + /// current state matches `expected_state`, regardless of parent kind. /// - /// Used by RPW reconciler. + /// Used by the RPW reconciler. + /// + /// # Returns /// - /// Returns `Ok(true)` if updated, `Ok(false)` if no row matched the filters - /// (member not found, soft-deleted, or state mismatch). - pub async fn multicast_group_member_set_state_if_current( + /// `Ok(true)` if the row was updated; `Ok(false)` if no row matched + /// the filters (member not found, soft-deleted, or state mismatch). + pub async fn multicast_group_member_set_state_if_current_for_parent( &self, opctx: &OpContext, external_group_id: MulticastGroupUuid, - parent_id: InstanceUuid, + parent: MemberParentRef, expected_state: MulticastGroupMemberState, new_state: MulticastGroupMemberState, ) -> UpdateResult { @@ -231,7 +239,8 @@ impl DataStore { dsl::external_group_id .eq(external_group_id.into_untyped_uuid()), ) - .filter(dsl::parent_id.eq(parent_id.into_untyped_uuid())) + .filter(dsl::parent_id.eq(parent.as_uuid())) + .filter(dsl::parent_kind.eq(parent.kind())) .filter(dsl::time_deleted.is_null()) .filter(dsl::state.eq(expected_state)) .set((dsl::state.eq(new_state), dsl::time_modified.eq(Utc::now()))) @@ -242,16 +251,20 @@ impl DataStore { Ok(rows_updated > 0) } - /// Atomically transition from "Left" → "Joining" and set sled_id. + /// Atomically transition from "Left" → "Joining" and set sled_id, + /// regardless of parent kind. /// - /// Used by RPW reconciler. + /// Used by the RPW reconciler. + /// + /// # Returns /// - /// Returns Ok(true) if updated, Ok(false) if state was not "Left" or row missing. - pub async fn multicast_group_member_left_to_joining_if_current( + /// `Ok(true)` if the row was updated; `Ok(false)` if the row was + /// missing or its state was not "Left". + pub async fn multicast_group_member_left_to_joining_if_current_for_parent( &self, opctx: &OpContext, external_group_id: MulticastGroupUuid, - parent_id: InstanceUuid, + parent: MemberParentRef, sled_id: DbTypedUuid, ) -> UpdateResult { use nexus_db_schema::schema::multicast_group_member::dsl; @@ -261,7 +274,8 @@ impl DataStore { dsl::external_group_id .eq(external_group_id.into_untyped_uuid()), ) - .filter(dsl::parent_id.eq(parent_id.into_untyped_uuid())) + .filter(dsl::parent_id.eq(parent.as_uuid())) + .filter(dsl::parent_kind.eq(parent.kind())) .filter(dsl::time_deleted.is_null()) .filter(dsl::state.eq(MulticastGroupMemberState::Left)) .set(( @@ -277,16 +291,19 @@ impl DataStore { } /// Atomically transition to "Left" and clear sled_id if current state - /// matches `expected_state`. + /// matches `expected_state`, regardless of parent kind. /// - /// Used by RPW reconciler. + /// Used by the RPW reconciler. /// - /// Returns Ok(true) if updated, Ok(false) if state did not match or row missing. - pub async fn multicast_group_member_to_left_if_current( + /// # Returns + /// + /// `Ok(true)` if the row was updated; `Ok(false)` if the row was + /// missing or its state did not match `expected_state`. + pub async fn multicast_group_member_to_left_if_current_for_parent( &self, opctx: &OpContext, external_group_id: MulticastGroupUuid, - parent_id: InstanceUuid, + parent: MemberParentRef, expected_state: MulticastGroupMemberState, ) -> UpdateResult { use nexus_db_schema::schema::multicast_group_member::dsl; @@ -296,7 +313,8 @@ impl DataStore { dsl::external_group_id .eq(external_group_id.into_untyped_uuid()), ) - .filter(dsl::parent_id.eq(parent_id.into_untyped_uuid())) + .filter(dsl::parent_id.eq(parent.as_uuid())) + .filter(dsl::parent_kind.eq(parent.kind())) .filter(dsl::time_deleted.is_null()) .filter(dsl::state.eq(expected_state)) .set(( @@ -332,20 +350,21 @@ impl DataStore { .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) } - /// List multicast group memberships for a specific instance. + /// List multicast group memberships for a parent (instance or probe). /// /// Only returns active (non-deleted) memberships. - pub async fn multicast_group_members_list_by_instance( + pub async fn multicast_group_members_list_by_parent( &self, opctx: &OpContext, - instance_id: InstanceUuid, + parent: MemberParentRef, pagparams: &DataPageParams<'_, Uuid>, ) -> ListResultVec { use nexus_db_schema::schema::multicast_group_member::dsl; paginated(dsl::multicast_group_member, dsl::id, pagparams) .filter(dsl::time_deleted.is_null()) - .filter(dsl::parent_id.eq(instance_id.into_untyped_uuid())) + .filter(dsl::parent_id.eq(parent.as_uuid())) + .filter(dsl::parent_kind.eq(parent.kind())) .select(MulticastGroupMember::as_select()) .get_results_async(&*self.pool_connection_authorized(opctx).await?) .await @@ -429,7 +448,7 @@ impl DataStore { /// /// # Returns /// - /// Returns the reconciliation result indicating what action was taken. + /// The reconciliation result indicating what action was taken. /// /// # Example Usage (from RPW reconciler) /// @@ -443,10 +462,10 @@ impl DataStore { /// /// let result = self /// .datastore - /// .multicast_group_member_reconcile_joining( + /// .multicast_group_member_reconcile_joining_for_parent( /// opctx, /// MulticastGroupUuid::from_untyped_uuid(group.id()), - /// InstanceUuid::from_untyped_uuid(member.parent_id), + /// MemberParentRef::Instance(InstanceUuid::from_untyped_uuid(member.parent_id)), /// instance_valid, /// current_sled_id, /// ) @@ -461,12 +480,12 @@ impl DataStore { /// ``` /// /// See [`ops::member_reconcile::reconcile_joining_member`] for atomic CTE implementation. - pub async fn multicast_group_member_reconcile_joining( + pub async fn multicast_group_member_reconcile_joining_for_parent( &self, opctx: &OpContext, group_id: MulticastGroupUuid, - instance_id: InstanceUuid, - instance_valid: bool, + parent: MemberParentRef, + parent_valid: bool, current_sled_id: Option>, ) -> Result { @@ -475,40 +494,34 @@ impl DataStore { ops::member_reconcile::reconcile_joining_member( &conn, group_id.into_untyped_uuid(), - instance_id.into_untyped_uuid(), - instance_valid, + parent, + parent_valid, current_sled_id, ) .await .map_err(external::Error::from) } - /// Detach all multicast group memberships for an instance. - /// - /// Transitions all non-Left members to "Left" state and clears sled_id. - /// Used by instance lifecycle operations (stop, delete) to signal RPW - /// that dataplane cleanup is needed. + /// Detach a parent (instance or probe) from all of its multicast group + /// memberships. /// - /// Note: This does not set `time_deleted`. For soft deletion of memberships, - /// use [`Self::multicast_group_members_mark_for_removal`]. - /// - /// See also [`Self::multicast_group_member_detach_by_group_and_instance`] - /// for detaching a specific group membership. - pub async fn multicast_group_members_detach_by_instance( + /// This is polymorphic over parent kind. Transitions non-"Left" members to + /// "Left" and clears `sled_id`. The dataplane cleanup is then driven + /// by the reconciler. + pub async fn multicast_group_members_detach_by_parent( &self, opctx: &OpContext, - instance_id: InstanceUuid, + parent: MemberParentRef, ) -> Result<(), external::Error> { use nexus_db_schema::schema::multicast_group_member::dsl; let now = Utc::now(); - // Transition members from "Joined/Joining" to "Left" state and clear - // `sled_id` diesel::update(dsl::multicast_group_member) - .filter(dsl::parent_id.eq(instance_id.into_untyped_uuid())) + .filter(dsl::parent_id.eq(parent.as_uuid())) + .filter(dsl::parent_kind.eq(parent.kind())) .filter(dsl::time_deleted.is_null()) - .filter(dsl::state.ne(MulticastGroupMemberState::Left)) // Only update non-Left members + .filter(dsl::state.ne(MulticastGroupMemberState::Left)) .set(( dsl::state.eq(MulticastGroupMemberState::Left), dsl::sled_id.eq(Option::>::None), @@ -520,18 +533,19 @@ impl DataStore { .map(|_| ()) } - /// Get a specific multicast group member by group ID and instance ID. - pub async fn multicast_group_member_get_by_group_and_instance( + /// Get a multicast member by `(group, parent)` pair. + pub async fn multicast_group_member_get_by_group_and_parent( &self, opctx: &OpContext, group_id: MulticastGroupUuid, - instance_id: InstanceUuid, + parent: MemberParentRef, ) -> Result, external::Error> { use nexus_db_schema::schema::multicast_group_member::dsl; let member = dsl::multicast_group_member .filter(dsl::external_group_id.eq(group_id.into_untyped_uuid())) - .filter(dsl::parent_id.eq(instance_id.into_untyped_uuid())) + .filter(dsl::parent_id.eq(parent.as_uuid())) + .filter(dsl::parent_kind.eq(parent.kind())) .filter(dsl::time_deleted.is_null()) .select(MulticastGroupMember::as_select()) .first_async(&*self.pool_connection_authorized(opctx).await?) @@ -571,34 +585,37 @@ impl DataStore { Ok(member) } - /// Detach a specific multicast group member by group ID and instance ID. + /// Detach a multicast member by (group, parent) pair. /// - /// This transitions member to "Left" state, clears `sled_id`, and sets `time_deleted` - /// (marking for permanent removal). Used by the HTTP API for explicit detach operations. - /// Distinct from instance stop which only transitions to "Left" without `time_deleted`. + /// Transitions the member to "Left" state, clears `sled_id`, and sets + /// `time_deleted` (marking for permanent removal). Used by the HTTP API + /// for explicit detach operations, distinct from lifecycle-driven detach + /// (e.g., instance stop) which only transitions to "Left" without setting + /// `time_deleted`. /// - /// See [`Self::multicast_group_members_detach_by_instance`] for detaching all - /// memberships of an instance (used during instance stop). - pub async fn multicast_group_member_detach_by_group_and_instance( + /// See [`Self::multicast_group_members_detach_by_parent`] for detaching + /// all memberships of a parent (used during the instance stop event). + pub async fn multicast_group_member_detach_by_group_and_parent( &self, opctx: &OpContext, group_id: MulticastGroupUuid, - instance_id: InstanceUuid, + parent: MemberParentRef, ) -> Result { use nexus_db_schema::schema::multicast_group_member::dsl; let now = Utc::now(); - // Mark member for removal (set time_deleted and state to "Left"), similar - // to soft instance deletion + // Mark member for removal (set time_deleted and state to "Left"), + // similar to soft deletion of the parent resource. let updated_rows = diesel::update(dsl::multicast_group_member) .filter(dsl::external_group_id.eq(group_id.into_untyped_uuid())) - .filter(dsl::parent_id.eq(instance_id.into_untyped_uuid())) + .filter(dsl::parent_id.eq(parent.as_uuid())) + .filter(dsl::parent_kind.eq(parent.kind())) .filter(dsl::time_deleted.is_null()) .set(( dsl::state.eq(MulticastGroupMemberState::Left), dsl::sled_id.eq(Option::>::None), - dsl::time_deleted.eq(Some(now)), // Mark for deletion + dsl::time_deleted.eq(Some(now)), dsl::time_modified.eq(now), )) .execute_async(&*self.pool_connection_authorized(opctx).await?) @@ -620,7 +637,7 @@ impl DataStore { /// /// Note: This does not update members already in "Left" state. For instance /// stops, first transition memberships to "Left" and clear their `sled_id` - /// via [`Self::multicast_group_members_detach_by_instance`]. + /// via [`Self::multicast_group_members_detach_by_parent`]. pub async fn multicast_group_member_update_sled_id( &self, opctx: &OpContext, @@ -644,6 +661,9 @@ impl DataStore { diesel::update(dsl::multicast_group_member) .filter(dsl::parent_id.eq(instance_id.into_untyped_uuid())) + .filter( + dsl::parent_kind.eq(MulticastGroupMemberParentKind::Instance), + ) .filter(dsl::time_deleted.is_null()) // Only update members not in "Left" state .filter(dsl::state.ne(MulticastGroupMemberState::Left)) @@ -679,6 +699,9 @@ impl DataStore { let rows_updated = diesel::update(dsl::multicast_group_member) .filter(dsl::parent_id.eq(instance_id.into_untyped_uuid())) + .filter( + dsl::parent_kind.eq(MulticastGroupMemberParentKind::Instance), + ) .filter(dsl::time_deleted.is_null()) .filter(dsl::state.ne(MulticastGroupMemberState::Left)) .filter(dsl::sled_id.eq(expected_sled_id)) // CAS condition @@ -729,6 +752,9 @@ impl DataStore { // - "Joining" (sled_id=NULL) → "Joining" + set sled_id (first-time start) diesel::update(dsl::multicast_group_member) .filter(dsl::parent_id.eq(instance_id.into_untyped_uuid())) + .filter( + dsl::parent_kind.eq(MulticastGroupMemberParentKind::Instance), + ) .filter(dsl::time_deleted.is_null()) .filter( dsl::state.eq(MulticastGroupMemberState::Left).or(dsl::state @@ -746,7 +772,8 @@ impl DataStore { .map(|_| ()) } - /// Permanently mark all multicast memberships for deletion when instance is deleted. + /// Permanently mark all multicast memberships for deletion when the + /// parent (instance or probe) is deleted. /// /// Sets members to "Left" state with `time_deleted` timestamp, indicating /// permanent removal (not temporary like instance stop). This distinguishes @@ -758,19 +785,20 @@ impl DataStore { /// - RPW reconciler will remove DPD configuration /// - Cleanup task will eventually hard-delete the database rows /// - /// Compare with [`Self::multicast_group_members_detach_by_instance`] which leaves - /// `time_deleted=NULL` for reactivation on instance restart. - pub async fn multicast_group_members_mark_for_removal( + /// Compare with [`Self::multicast_group_members_detach_by_parent`] which + /// leaves `time_deleted=NULL` for reactivation on parent restart. + pub async fn multicast_group_members_mark_for_removal_by_parent( &self, opctx: &OpContext, - instance_id: InstanceUuid, + parent: MemberParentRef, ) -> Result<(), external::Error> { use nexus_db_schema::schema::multicast_group_member::dsl; let now = Utc::now(); diesel::update(dsl::multicast_group_member) - .filter(dsl::parent_id.eq(instance_id.into_untyped_uuid())) + .filter(dsl::parent_id.eq(parent.as_uuid())) + .filter(dsl::parent_kind.eq(parent.kind())) .filter(dsl::time_deleted.is_null()) .set(( dsl::state.eq(MulticastGroupMemberState::Left), // Transition to Left state @@ -865,9 +893,11 @@ mod tests { use omicron_common::api::external::DataPageParams; use omicron_common::api::external::IdentityMetadataCreateParams; use omicron_test_utils::dev; - use omicron_uuid_kinds::SledUuid; + use omicron_uuid_kinds::{ProbeUuid, SledUuid}; - use crate::db::model::MulticastGroupMemberValues; + use crate::db::model::{ + MulticastGroupMemberParentKind, MulticastGroupMemberValues, + }; use crate::db::pub_test_utils::helpers::{ SledUpdateBuilder, attach_instance_to_vmm, create_instance_with_vmm, create_stopped_instance_record, create_vmm_for_instance, @@ -901,9 +931,9 @@ mod tests { } #[tokio::test] - async fn test_multicast_group_member_attach_to_instance() { + async fn test_multicast_group_member_attach_instance_parent() { let logctx = dev::test_setup_log( - "test_multicast_group_member_attach_to_instance", + "test_multicast_group_member_attach_instance_parent", ); let db = TestDatabase::new_with_datastore(&logctx.log).await; let (opctx, datastore) = (db.opctx(), db.datastore()); @@ -956,10 +986,12 @@ mod tests { // Attaching to "Creating" group should succeed (implicit lifecycle model) // Members start in "Joining" and wait for RPW to activate the group let creating_member = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(creating_group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), Some(NO_SOURCE_IPS), ) .await @@ -968,10 +1000,12 @@ mod tests { // Attach to active group should also succeed let member = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(active_group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), Some(NO_SOURCE_IPS), ) .await @@ -992,10 +1026,12 @@ mod tests { // Second attach to same group with member in "Joining" state should be // idempotent let member2 = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(active_group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), Some(NO_SOURCE_IPS), ) .await @@ -1009,19 +1045,23 @@ mod tests { // Transition member to "Joined" state and capture time_modified datastore - .multicast_group_member_set_state( + .multicast_group_member_set_state_for_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(active_group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), MulticastGroupMemberState::Joined, ) .await .expect("Should transition member to 'Joined'"); let member_joined = datastore - .multicast_group_member_get_by_group_and_instance( + .multicast_group_member_get_by_group_and_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(active_group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), ) .await .expect("Should refetch member after Joined") @@ -1030,10 +1070,12 @@ mod tests { // Attach to member in "Joined" state should be idempotent let member3 = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(active_group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), Some(NO_SOURCE_IPS), ) .await @@ -1047,10 +1089,12 @@ mod tests { // Transition member to "Left" state (simulating instance stop) datastore - .multicast_group_member_set_state( + .multicast_group_member_set_state_for_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(active_group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), MulticastGroupMemberState::Left, ) .await @@ -1066,10 +1110,12 @@ mod tests { .await .expect("Should clear sled_id for stopped instance"); let member_left = datastore - .multicast_group_member_get_by_group_and_instance( + .multicast_group_member_get_by_group_and_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(active_group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), ) .await .expect("Should get member after Left") @@ -1080,10 +1126,12 @@ mod tests { let reactivation_sources: Vec = vec!["10.0.0.1".parse().unwrap(), "10.0.0.2".parse().unwrap()]; let reactivated_member = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(active_group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), Some(reactivation_sources.as_slice()), ) .await @@ -1115,6 +1163,125 @@ mod tests { logctx.cleanup_successful(); } + #[tokio::test] + async fn test_multicast_group_member_attach_probe_parent() { + let logctx = dev::test_setup_log( + "test_multicast_group_member_attach_probe_parent", + ); + let db = TestDatabase::new_with_datastore(&logctx.log).await; + let (opctx, datastore) = (db.opctx(), db.datastore()); + + let setup = multicast::create_test_setup( + &opctx, + &datastore, + "attach-probe-test-pool", + "test-project-attach-probe", + ) + .await; + + let active_group = multicast::create_test_group_with_state( + &opctx, + &datastore, + "active-probe-group", + "224.10.1.7", + true, + ) + .await; + + // Insert a probe row directly. `multicast_group_member_attach` only + // reads `probe.id`, `probe.sled`, and `probe.time_deleted`. + let probe_id = Uuid::new_v4(); + let probe = nexus_db_model::Probe { + identity: nexus_db_model::ProbeIdentity::new( + probe_id, + IdentityMetadataCreateParams { + name: "attach-test-probe".parse().unwrap(), + description: "attach probe test fixture".to_string(), + }, + ), + project_id: setup.project_id, + sled: setup.sled_id.into(), + }; + { + use nexus_db_schema::schema::probe::dsl as probe_dsl; + let conn = datastore + .pool_connection_authorized(&opctx) + .await + .expect("Get connection"); + diesel::insert_into(probe_dsl::probe) + .values(probe.clone()) + .execute_async(&*conn) + .await + .expect("Should insert probe row"); + } + let parent = + MemberParentRef::Probe(ProbeUuid::from_untyped_uuid(probe_id)); + + // First attach: probe enters "Joining" with `sled_id` read directly + // from the probe row (not via VMM lookup). + let member = datastore + .multicast_group_member_attach( + &opctx, + MulticastGroupUuid::from_untyped_uuid(active_group.id()), + parent, + Some(NO_SOURCE_IPS), + ) + .await + .expect("Should attach probe to active group"); + + assert_eq!(member.state, MulticastGroupMemberState::Joining); + assert_eq!(member.sled_id, Some(setup.sled_id.into())); + assert_eq!(member.external_group_id, active_group.id()); + assert_eq!(member.multicast_ip, active_group.multicast_ip); + assert_eq!(member.parent_id, probe_id); + assert_eq!( + member.parent_kind, + MulticastGroupMemberParentKind::Probe, + "parent_kind must persist as Probe" + ); + assert!(member.time_deleted.is_none()); + assert!(member.time_created <= member.time_modified); + assert_eq!(member.source_ips, Vec::::new()); + assert!(member.version_removed.is_none()); + + // Verify the persisted row round-trips through the lookup path and + // reconstructs as `MemberParentRef::Probe`. + let fetched = datastore + .multicast_group_member_get_by_group_and_parent( + &opctx, + MulticastGroupUuid::from_untyped_uuid(active_group.id()), + parent, + ) + .await + .expect("Should fetch probe member") + .expect("Probe member should exist"); + assert_eq!(fetched.id, member.id); + assert!(matches!(fetched.parent_ref(), MemberParentRef::Probe(_))); + + let time_after_first_attach = member.time_modified; + + // Re-attach while still "Joining" should be idempotent: same row, no + // `time_modified` advance. + let member2 = datastore + .multicast_group_member_attach( + &opctx, + MulticastGroupUuid::from_untyped_uuid(active_group.id()), + parent, + Some(NO_SOURCE_IPS), + ) + .await + .expect("Should handle duplicate probe attach idempotently"); + + assert_eq!(member.id, member2.id); + assert_eq!( + member2.time_modified, time_after_first_attach, + "Idempotent probe attach must not update time_modified" + ); + + db.terminate().await; + logctx.cleanup_successful(); + } + #[tokio::test] async fn test_multicast_group_members_detach_by_instance() { let logctx = dev::test_setup_log( @@ -1202,30 +1369,36 @@ mod tests { // Add instance1 to both groups and instance2 to only group1 let member1_1 = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group1.id()), - InstanceUuid::from_untyped_uuid(*instance1_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + *instance1_id, + )), Some(NO_SOURCE_IPS), ) .await .expect("Should add instance1 to group1"); let member1_2 = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group2.id()), - InstanceUuid::from_untyped_uuid(*instance1_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + *instance1_id, + )), Some(NO_SOURCE_IPS), ) .await .expect("Should add instance1 to group2"); let member2_1 = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group1.id()), - InstanceUuid::from_untyped_uuid(*instance2_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + *instance2_id, + )), Some(NO_SOURCE_IPS), ) .await @@ -1238,9 +1411,11 @@ mod tests { // Detach all memberships for instance1 (transitions to "Left", does not set time_deleted) datastore - .multicast_group_members_detach_by_instance( + .multicast_group_members_detach_by_parent( &opctx, - InstanceUuid::from_untyped_uuid(*instance1_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + *instance1_id, + )), ) .await .expect("Should detach all memberships for instance1"); @@ -1302,9 +1477,11 @@ mod tests { // Test idempotency: detaching again should be idempotent datastore - .multicast_group_members_detach_by_instance( + .multicast_group_members_detach_by_parent( &opctx, - InstanceUuid::from_untyped_uuid(*instance1_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + *instance1_id, + )), ) .await .expect("Should handle detaching instance1 again"); @@ -1313,6 +1490,164 @@ mod tests { logctx.cleanup_successful(); } + #[tokio::test] + async fn test_multicast_group_members_detach_by_parent_probe() { + let logctx = dev::test_setup_log( + "test_multicast_group_members_detach_by_parent_probe", + ); + let db = TestDatabase::new_with_datastore(&logctx.log).await; + let (opctx, datastore) = (db.opctx(), db.datastore()); + + let setup = multicast::create_test_setup( + &opctx, + &datastore, + "detach-probe-pool", + "test-project-detach-probe", + ) + .await; + + let group = multicast::create_test_group_with_state( + &opctx, + &datastore, + "detach-probe-group", + "224.10.1.8", + true, + ) + .await; + + // Create one instance-parented member. + let (instance, _vmm) = create_instance_with_vmm( + &opctx, + &datastore, + &setup.authz_project, + "detach-probe-coexist-instance", + setup.sled_id, + ) + .await; + let instance_id = *instance.as_untyped_uuid(); + let instance_parent = MemberParentRef::Instance( + InstanceUuid::from_untyped_uuid(instance_id), + ); + + // Insert a probe row directly. + let probe_id = Uuid::new_v4(); + let probe = nexus_db_model::Probe { + identity: nexus_db_model::ProbeIdentity::new( + probe_id, + IdentityMetadataCreateParams { + name: "detach-test-probe".parse().unwrap(), + description: "detach probe test fixture".to_string(), + }, + ), + project_id: setup.project_id, + sled: setup.sled_id.into(), + }; + { + use nexus_db_schema::schema::probe::dsl as probe_dsl; + let conn = datastore + .pool_connection_authorized(&opctx) + .await + .expect("Get connection"); + diesel::insert_into(probe_dsl::probe) + .values(probe.clone()) + .execute_async(&*conn) + .await + .expect("Should insert probe row"); + } + let probe_parent = + MemberParentRef::Probe(ProbeUuid::from_untyped_uuid(probe_id)); + + // Attach both to the same group and drive both to "Joined" so we can + // confirm `detach_by_parent` only touches the targeted parent. + let instance_member = datastore + .multicast_group_member_attach( + &opctx, + MulticastGroupUuid::from_untyped_uuid(group.id()), + instance_parent, + Some(NO_SOURCE_IPS), + ) + .await + .expect("Should attach instance to group"); + let probe_member = datastore + .multicast_group_member_attach( + &opctx, + MulticastGroupUuid::from_untyped_uuid(group.id()), + probe_parent, + Some(NO_SOURCE_IPS), + ) + .await + .expect("Should attach probe to group"); + + datastore + .multicast_group_member_set_state_for_parent( + &opctx, + MulticastGroupUuid::from_untyped_uuid(group.id()), + instance_parent, + MulticastGroupMemberState::Joined, + ) + .await + .expect("Should mark instance member Joined"); + datastore + .multicast_group_member_set_state_for_parent( + &opctx, + MulticastGroupUuid::from_untyped_uuid(group.id()), + probe_parent, + MulticastGroupMemberState::Joined, + ) + .await + .expect("Should mark probe member Joined"); + + // Detach by probe parent only. + datastore + .multicast_group_members_detach_by_parent(&opctx, probe_parent) + .await + .expect("Should detach probe memberships"); + + // Probe row transitions to "Left" with cleared `sled_id`, no + // `time_deleted` (membership row remains for the reconciler). + let probe_after = datastore + .multicast_group_member_get_by_id(&opctx, probe_member.id, false) + .await + .expect("Should fetch probe member after detach") + .expect("Probe member row should still exist"); + assert_eq!(probe_after.state, MulticastGroupMemberState::Left); + assert!( + probe_after.sled_id.is_none(), + "Probe detach must clear sled_id" + ); + assert!( + probe_after.time_deleted.is_none(), + "detach_by_parent should not set time_deleted" + ); + assert_eq!( + probe_after.parent_kind, + MulticastGroupMemberParentKind::Probe, + ); + + // Instance row in the same group is untouched, so still "Joined" with its + // `sled_id` intact. + let instance_after = datastore + .multicast_group_member_get_by_id(&opctx, instance_member.id, false) + .await + .expect("Should fetch instance member after probe detach") + .expect("Instance member row should still exist"); + assert_eq!(instance_after.state, MulticastGroupMemberState::Joined); + assert_eq!(instance_after.sled_id, Some(setup.sled_id.into())); + assert_eq!( + instance_after.parent_kind, + MulticastGroupMemberParentKind::Instance, + ); + + // Detaching the probe again is a noop. + datastore + .multicast_group_members_detach_by_parent(&opctx, probe_parent) + .await + .expect("Repeat probe detach should be idempotent"); + + db.terminate().await; + logctx.cleanup_successful(); + } + #[tokio::test] async fn test_multicast_group_member_operations_with_parent_id() { let logctx = dev::test_setup_log( @@ -1368,10 +1703,12 @@ mod tests { // Add member using parent_id (instance_id) let member = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(*instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + *instance_id, + )), Some(NO_SOURCE_IPS), ) .await @@ -1389,9 +1726,11 @@ mod tests { direction: dropshot::PaginationOrder::Ascending, }; let member_memberships = datastore - .multicast_group_members_list_by_instance( + .multicast_group_members_list_by_parent( &opctx, - InstanceUuid::from_untyped_uuid(*instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + *instance_id, + )), pagparams, ) .await @@ -1457,10 +1796,10 @@ mod tests { // Add member first time - should succeed let member1 = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - instance_id, + MemberParentRef::Instance(instance_id), Some(NO_SOURCE_IPS), ) .await @@ -1468,10 +1807,10 @@ mod tests { // Try to add same instance again - should return existing member let member2 = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - instance_id, + MemberParentRef::Instance(instance_id), Some(NO_SOURCE_IPS), ) .await @@ -1529,10 +1868,12 @@ mod tests { // Create member record in "Joining" state (no sled_id initially) let member = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(test_instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + test_instance_id, + )), Some(NO_SOURCE_IPS), ) .await @@ -1553,10 +1894,12 @@ mod tests { // Verify sled_id was updated let updated_member = datastore - .multicast_group_member_get_by_group_and_instance( + .multicast_group_member_get_by_group_and_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(test_instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + test_instance_id, + )), ) .await .expect("Should fetch updated member") @@ -1576,10 +1919,12 @@ mod tests { // Verify sled_id was updated to new sled let migrated_member = datastore - .multicast_group_member_get_by_group_and_instance( + .multicast_group_member_get_by_group_and_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(test_instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + test_instance_id, + )), ) .await .expect("Should fetch migrated member") @@ -1589,19 +1934,23 @@ mod tests { // Instance stop - Clear sled_id (set to NULL) datastore - .multicast_group_members_detach_by_instance( + .multicast_group_members_detach_by_parent( &opctx, - InstanceUuid::from_untyped_uuid(test_instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + test_instance_id, + )), ) .await .expect("Should clear sled_id for instance stop"); // Verify sled_id was cleared let stopped_member = datastore - .multicast_group_member_get_by_group_and_instance( + .multicast_group_member_get_by_group_and_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(test_instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + test_instance_id, + )), ) .await .expect("Should fetch stopped member") @@ -1611,9 +1960,11 @@ mod tests { // Idempotency - Clearing again should be idempotent datastore - .multicast_group_members_detach_by_instance( + .multicast_group_members_detach_by_parent( &opctx, - InstanceUuid::from_untyped_uuid(test_instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + test_instance_id, + )), ) .await .expect("Should handle clearing sled_id again"); @@ -1622,8 +1973,8 @@ mod tests { logctx.cleanup_successful(); } - #[tokio::test] /// Datastore-only verification of member state transitions. + #[tokio::test] async fn test_multicast_group_member_state_transitions_datastore() { let logctx = dev::test_setup_log( "test_multicast_group_member_state_transitions_datastore", @@ -1662,10 +2013,12 @@ mod tests { // Create member record directly in "Joining" state datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(test_instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + test_instance_id, + )), Some(NO_SOURCE_IPS), ) .await @@ -1673,10 +2026,12 @@ mod tests { // Complete the attach operation datastore - .multicast_group_member_set_state( + .multicast_group_member_set_state_for_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(test_instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + test_instance_id, + )), MulticastGroupMemberState::Joined, ) .await @@ -1684,10 +2039,12 @@ mod tests { // Complete the operation and leave datastore - .multicast_group_member_set_state( + .multicast_group_member_set_state_for_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(test_instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + test_instance_id, + )), MulticastGroupMemberState::Left, ) .await @@ -1697,6 +2054,186 @@ mod tests { logctx.cleanup_successful(); } + /// Datastore-only verification of probe-parented member state transitions. + /// + /// Walks "Joining" -> "Joined" -> "Left" via the polymorphic + /// `_for_parent` operations and asserts probe-side transitions never + /// write `sled_id`. + #[tokio::test] + async fn test_multicast_group_member_state_transitions_probe_parent() { + let logctx = dev::test_setup_log( + "test_multicast_group_member_state_transitions_probe_parent", + ); + let db = TestDatabase::new_with_datastore(&logctx.log).await; + let (opctx, datastore) = (db.opctx(), db.datastore()); + + let setup = multicast::create_test_setup_with_range( + &opctx, + &datastore, + "state-probe-pool", + "test-project-probe-state", + (224, 2, 2, 1), + (224, 2, 2, 254), + ) + .await; + let group = multicast::create_test_group_with_state( + &opctx, + &datastore, + "state-probe-group", + "224.2.2.5", + true, + ) + .await; + + // Insert a probe row directly. + let probe_id = Uuid::new_v4(); + let probe = nexus_db_model::Probe { + identity: nexus_db_model::ProbeIdentity::new( + probe_id, + IdentityMetadataCreateParams { + name: "state-test-probe".parse().unwrap(), + description: "state probe test fixture".to_string(), + }, + ), + project_id: setup.project_id, + sled: setup.sled_id.into(), + }; + { + use nexus_db_schema::schema::probe::dsl as probe_dsl; + let conn = datastore + .pool_connection_authorized(&opctx) + .await + .expect("Get connection"); + diesel::insert_into(probe_dsl::probe) + .values(probe.clone()) + .execute_async(&*conn) + .await + .expect("Should insert probe row"); + } + let parent = + MemberParentRef::Probe(ProbeUuid::from_untyped_uuid(probe_id)); + + // Attach: row lands in "Joining" with `sled_id` pulled from the + // probe row by the CTE. + let attached = datastore + .multicast_group_member_attach( + &opctx, + MulticastGroupUuid::from_untyped_uuid(group.id()), + parent, + Some(NO_SOURCE_IPS), + ) + .await + .expect("Should attach probe member"); + assert_eq!(attached.state, MulticastGroupMemberState::Joining); + assert_eq!(attached.sled_id, Some(setup.sled_id.into())); + let sled_id_after_attach = attached.sled_id; + + // Joining -> Joined via the polymorphic setter. + datastore + .multicast_group_member_set_state_for_parent( + &opctx, + MulticastGroupUuid::from_untyped_uuid(group.id()), + parent, + MulticastGroupMemberState::Joined, + ) + .await + .expect("Should transition probe to Joined"); + let joined = datastore + .multicast_group_member_get_by_group_and_parent( + &opctx, + MulticastGroupUuid::from_untyped_uuid(group.id()), + parent, + ) + .await + .expect("Fetch after Joined") + .expect("Probe member should exist"); + assert_eq!(joined.state, MulticastGroupMemberState::Joined); + assert_eq!( + joined.sled_id, sled_id_after_attach, + "Joined transition must not rewrite sled_id" + ); + + // "Joined" -> "Left" via the conditional + // `multicast_group_member_to_left_if_current_for_parent` used by + // the RPW reconciler. `sled_id` is cleared as part of "Left" + // semantics. This is the only path on the probe side that mutates + // `sled_id`, and it does so by clearing (not by writing a new + // value). + let transitioned = datastore + .multicast_group_member_to_left_if_current_for_parent( + &opctx, + MulticastGroupUuid::from_untyped_uuid(group.id()), + parent, + MulticastGroupMemberState::Joined, + ) + .await + .expect("Conditional transition should succeed"); + + assert!(transitioned, "Joined -> Left must update one row"); + + let left = datastore + .multicast_group_member_get_by_group_and_parent( + &opctx, + MulticastGroupUuid::from_untyped_uuid(group.id()), + parent, + ) + .await + .expect("Fetch after Left") + .expect("Probe member should exist"); + assert_eq!(left.state, MulticastGroupMemberState::Left); + assert!( + left.sled_id.is_none(), + "Left transition clears sled_id; probe path never writes a new sled" + ); + + // Conditional re-transition with the wrong `expected_state` must + // not affect the row (idempotency / TOCTOU guard). + let no_op = datastore + .multicast_group_member_to_left_if_current_for_parent( + &opctx, + MulticastGroupUuid::from_untyped_uuid(group.id()), + parent, + MulticastGroupMemberState::Joined, + ) + .await + .expect("Conditional with wrong expected_state should not error"); + assert!(!no_op, "No row should match a stale expected_state"); + + // "Left" -> "Joining" via the polymorphic + // `multicast_group_member_left_to_joining_if_current_for_parent`. + // It re-populates `sled_id` (RPW reconciler-driven rejoin). Verify + // the value matches the parameter we pass in, i.e. probe-side + // state transitions never synthesize a sled from elsewhere. + let rejoined = datastore + .multicast_group_member_left_to_joining_if_current_for_parent( + &opctx, + MulticastGroupUuid::from_untyped_uuid(group.id()), + parent, + setup.sled_id.into(), + ) + .await + .expect("Should rejoin probe"); + assert!(rejoined, "Left -> Joining must update one row"); + let rejoined_row = datastore + .multicast_group_member_get_by_group_and_parent( + &opctx, + MulticastGroupUuid::from_untyped_uuid(group.id()), + parent, + ) + .await + .expect("Fetch after rejoin") + .expect("Probe member should exist"); + assert_eq!(rejoined_row.state, MulticastGroupMemberState::Joining); + assert_eq!(rejoined_row.sled_id, Some(setup.sled_id.into())); + assert_eq!( + rejoined_row.parent_kind, + MulticastGroupMemberParentKind::Probe, + ); + + db.terminate().await; + logctx.cleanup_successful(); + } + #[tokio::test] async fn test_multicast_group_members_complete_delete() { let logctx = @@ -1769,6 +2306,7 @@ mod tests { external_group_id: group.id(), multicast_ip: group.multicast_ip, parent_id: instance1_id, + parent_kind: MulticastGroupMemberParentKind::Instance, sled_id: Some(setup.sled_id.into()), state: MulticastGroupMemberState::Left, source_ips: vec![], @@ -1789,6 +2327,7 @@ mod tests { external_group_id: group.id(), multicast_ip: group.multicast_ip, parent_id: instance2_id, + parent_kind: MulticastGroupMemberParentKind::Instance, sled_id: Some(setup.sled_id.into()), state: MulticastGroupMemberState::Left, source_ips: vec![], @@ -1809,6 +2348,7 @@ mod tests { external_group_id: group.id(), multicast_ip: group.multicast_ip, parent_id: instance3_id, + parent_kind: MulticastGroupMemberParentKind::Instance, sled_id: Some(setup.sled_id.into()), state: MulticastGroupMemberState::Joined, source_ips: vec![], @@ -1834,10 +2374,12 @@ mod tests { // Verify member1 was deleted by trying to find it directly let member1_result = datastore - .multicast_group_member_get_by_group_and_instance( + .multicast_group_member_get_by_group_and_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(member1.parent_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + member1.parent_id, + )), ) .await .expect("Should query for member1"); @@ -1845,10 +2387,12 @@ mod tests { // Verify member2 still exists let member2_result = datastore - .multicast_group_member_get_by_group_and_instance( + .multicast_group_member_get_by_group_and_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(member2.parent_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + member2.parent_id, + )), ) .await .expect("Should query for member2"); @@ -1994,10 +2538,12 @@ mod tests { // Try to add member to non-existent group let result = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(fake_group_id), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), Some(NO_SOURCE_IPS), ) .await; @@ -2005,10 +2551,12 @@ mod tests { // Try to set state for non-existent member let result = datastore - .multicast_group_member_set_state( + .multicast_group_member_set_state_for_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(fake_group_id), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), MulticastGroupMemberState::Joined, ) .await; @@ -2019,10 +2567,12 @@ mod tests { // Try to get member from non-existent group let result = datastore - .multicast_group_member_get_by_group_and_instance( + .multicast_group_member_get_by_group_and_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(fake_group_id), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), ) .await .expect("Query should succeed"); @@ -2041,10 +2591,12 @@ mod tests { // Try to attach non-existent instance to group let result = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(fake_instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + fake_instance_id, + )), Some(NO_SOURCE_IPS), ) .await; @@ -2052,10 +2604,12 @@ mod tests { // Successfully create a member for further testing datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), Some(NO_SOURCE_IPS), ) .await @@ -2063,20 +2617,24 @@ mod tests { // Invalid state transitions should be handled gracefully datastore - .multicast_group_member_set_state( + .multicast_group_member_set_state_for_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), MulticastGroupMemberState::Left, ) .await .expect("Should allow transition to 'Left'"); datastore - .multicast_group_member_set_state( + .multicast_group_member_set_state_for_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), MulticastGroupMemberState::Joined, ) .await @@ -2084,17 +2642,21 @@ mod tests { // Test idempotent operations work correctly datastore - .multicast_group_members_detach_by_instance( + .multicast_group_members_detach_by_parent( &opctx, - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), ) .await .expect("First detach should succeed"); datastore - .multicast_group_members_detach_by_instance( + .multicast_group_members_detach_by_parent( &opctx, - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), ) .await .expect("Second detach should be idempotent"); @@ -2156,10 +2718,10 @@ mod tests { // Add member in "Joining" state (typical after instance create) let member = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - instance_id, + MemberParentRef::Instance(instance_id), Some(NO_SOURCE_IPS), ) .await @@ -2181,10 +2743,10 @@ mod tests { // Verify member is still "Joining" but now has sled_id let updated_member = datastore - .multicast_group_member_get_by_group_and_instance( + .multicast_group_member_get_by_group_and_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - instance_id, + MemberParentRef::Instance(instance_id), ) .await .expect("Should find updated member") @@ -2196,16 +2758,19 @@ mod tests { // Simulate instance stop by transitioning to "Left" state datastore - .multicast_group_members_detach_by_instance(&opctx, instance_id) + .multicast_group_members_detach_by_parent( + &opctx, + MemberParentRef::Instance(instance_id), + ) .await .expect("Should stop instance"); // Verify member is "Left" with no sled_id let stopped_member = datastore - .multicast_group_member_get_by_group_and_instance( + .multicast_group_member_get_by_group_and_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - instance_id, + MemberParentRef::Instance(instance_id), ) .await .expect("Should find stopped member") @@ -2226,10 +2791,10 @@ mod tests { // Verify member is back to "Joining" with new sled_id let restarted_member = datastore - .multicast_group_member_get_by_group_and_instance( + .multicast_group_member_get_by_group_and_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - instance_id, + MemberParentRef::Instance(instance_id), ) .await .expect("Should find restarted member") @@ -2242,10 +2807,10 @@ mod tests { // Test that starting instance with "Joined" members works correctly // First transition to "Joined" state (simulate RPW reconciler) datastore - .multicast_group_member_set_state( + .multicast_group_member_set_state_for_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - instance_id, + MemberParentRef::Instance(instance_id), MulticastGroupMemberState::Joined, ) .await @@ -2253,10 +2818,10 @@ mod tests { // Verify member is now "Joined" let joined_member = datastore - .multicast_group_member_get_by_group_and_instance( + .multicast_group_member_get_by_group_and_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - instance_id, + MemberParentRef::Instance(instance_id), ) .await .expect("Should find joined member") @@ -2277,10 +2842,10 @@ mod tests { // Verify "Joined" member remains unchanged (no state transition) let unchanged_member = datastore - .multicast_group_member_get_by_group_and_instance( + .multicast_group_member_get_by_group_and_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - instance_id, + MemberParentRef::Instance(instance_id), ) .await .expect("Should find unchanged member") @@ -2365,20 +2930,20 @@ mod tests { // Add instance1 to both groups let member1_1 = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group1.id()), - instance1_id, + MemberParentRef::Instance(instance1_id), Some(NO_SOURCE_IPS), ) .await .expect("Should add instance1 to group1"); let member1_2 = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group2.id()), - instance1_id, + MemberParentRef::Instance(instance1_id), Some(NO_SOURCE_IPS), ) .await @@ -2386,10 +2951,10 @@ mod tests { // Add instance2 to only group1 let member2_1 = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group1.id()), - instance2_id, + MemberParentRef::Instance(instance2_id), Some(NO_SOURCE_IPS), ) .await @@ -2402,7 +2967,10 @@ mod tests { // Mark all memberships for instance1 for removal datastore - .multicast_group_members_mark_for_removal(&opctx, instance1_id) + .multicast_group_members_mark_for_removal_by_parent( + &opctx, + MemberParentRef::Instance(instance1_id), + ) .await .expect("Should mark instance1 memberships for removal"); @@ -2450,16 +3018,19 @@ mod tests { // Test idempotency - marking again should be safe datastore - .multicast_group_members_mark_for_removal(&opctx, instance1_id) + .multicast_group_members_mark_for_removal_by_parent( + &opctx, + MemberParentRef::Instance(instance1_id), + ) .await .expect("Should handle duplicate mark for removal"); // Test marking instance with no memberships (should be noop) let non_member_instance = InstanceUuid::new_v4(); datastore - .multicast_group_members_mark_for_removal( + .multicast_group_members_mark_for_removal_by_parent( &opctx, - non_member_instance, + MemberParentRef::Instance(non_member_instance), ) .await .expect("Should handle marking instance with no memberships"); @@ -2539,20 +3110,20 @@ mod tests { // Add members to group1 let member1_1 = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group1.id()), - instance1_id, + MemberParentRef::Instance(instance1_id), Some(NO_SOURCE_IPS), ) .await .expect("Should add instance1 to group1"); let member1_2 = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group1.id()), - instance2_id, + MemberParentRef::Instance(instance2_id), Some(NO_SOURCE_IPS), ) .await @@ -2560,20 +3131,20 @@ mod tests { // Add members to group2 let member2_1 = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group2.id()), - instance1_id, + MemberParentRef::Instance(instance1_id), Some(NO_SOURCE_IPS), ) .await .expect("Should add instance1 to group2"); let member2_2 = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group2.id()), - instance3_id, + MemberParentRef::Instance(instance3_id), Some(NO_SOURCE_IPS), ) .await @@ -2740,10 +3311,12 @@ mod tests { let handle1 = tokio::spawn(async move { datastore1 - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx1, MulticastGroupUuid::from_untyped_uuid(group_id), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), Some(NO_SOURCE_IPS), ) .await @@ -2751,10 +3324,12 @@ mod tests { let handle2 = tokio::spawn(async move { datastore2 - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx2, MulticastGroupUuid::from_untyped_uuid(group_id), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), Some(NO_SOURCE_IPS), ) .await @@ -2805,10 +3380,12 @@ mod tests { // Attach to non-existent group let fake_group_id = Uuid::new_v4(); let result = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(fake_group_id), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), Some(NO_SOURCE_IPS), ) .await; @@ -2831,10 +3408,12 @@ mod tests { // Attach non-existent instance let fake_instance_id = Uuid::new_v4(); let result = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(fake_instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + fake_instance_id, + )), Some(NO_SOURCE_IPS), ) .await; @@ -2888,10 +3467,12 @@ mod tests { // Attaching to "Creating" group should succeed (not just "Active") let member = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(creating_group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), Some(NO_SOURCE_IPS), ) .await @@ -2922,10 +3503,12 @@ mod tests { // Attaching to soft-deleted ("Deleting") group should fail let res = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(deleting_group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), Some(NO_SOURCE_IPS), ) .await; @@ -2975,10 +3558,12 @@ mod tests { // First attach with source IPs let initial_sources: Vec = vec!["192.168.1.1".parse().unwrap()]; let member1 = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), Some(initial_sources.as_slice()), ) .await @@ -2987,10 +3572,12 @@ mod tests { // Second attach (idempotent, should not update sources) let member2 = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), None, // None preserves existing sources ) .await @@ -2998,10 +3585,12 @@ mod tests { assert_eq!(member1.id, member2.id, "Should return same member ID"); let member_after_second = datastore - .multicast_group_member_get_by_group_and_instance( + .multicast_group_member_get_by_group_and_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), ) .await .expect("Should fetch member after second attach") @@ -3021,10 +3610,12 @@ mod tests { // Third attach (still idempotent) let member3 = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), Some(NO_SOURCE_IPS), ) .await @@ -3032,10 +3623,12 @@ mod tests { assert_eq!(member1.id, member3.id, "Should return same member ID"); let member_after_third = datastore - .multicast_group_member_get_by_group_and_instance( + .multicast_group_member_get_by_group_and_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), ) .await .expect("Should fetch member after third attach") @@ -3088,23 +3681,28 @@ mod tests { let original_sources: Vec = vec!["10.1.1.1".parse().unwrap(), "10.1.1.2".parse().unwrap()]; let member = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, group_id, - instance, + MemberParentRef::Instance(instance), Some(original_sources.as_slice()), ) .await .expect("Should attach"); datastore - .multicast_group_members_detach_by_instance(&opctx, instance) + .multicast_group_members_detach_by_parent( + &opctx, + MemberParentRef::Instance(instance), + ) .await .expect("Should detach"); let reactivated = datastore - .multicast_group_member_attach_to_instance( - &opctx, group_id, instance, + .multicast_group_member_attach( + &opctx, + group_id, + MemberParentRef::Instance(instance), None, // Preserve existing sources ) .await @@ -3136,27 +3734,30 @@ mod tests { let original_sources: Vec = vec!["10.0.0.1".parse().unwrap(), "10.0.0.2".parse().unwrap()]; let member = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, group_id, - instance, + MemberParentRef::Instance(instance), Some(original_sources.as_slice()), ) .await .expect("Should attach"); datastore - .multicast_group_members_detach_by_instance(&opctx, instance) + .multicast_group_members_detach_by_parent( + &opctx, + MemberParentRef::Instance(instance), + ) .await .expect("Should detach"); let replacement_sources: Vec = vec!["10.0.0.3".parse().unwrap(), "10.0.0.4".parse().unwrap()]; let reactivated = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, group_id, - instance, + MemberParentRef::Instance(instance), Some(replacement_sources.as_slice()), ) .await @@ -3188,25 +3789,28 @@ mod tests { let original_sources: Vec = vec!["10.5.5.1".parse().unwrap(), "10.5.5.2".parse().unwrap()]; let member = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, group_id, - instance, + MemberParentRef::Instance(instance), Some(original_sources.as_slice()), ) .await .expect("Should attach"); datastore - .multicast_group_members_detach_by_instance(&opctx, instance) + .multicast_group_members_detach_by_parent( + &opctx, + MemberParentRef::Instance(instance), + ) .await .expect("Should detach"); let reactivated = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, group_id, - instance, + MemberParentRef::Instance(instance), Some(NO_SOURCE_IPS), // Clear sources ) .await @@ -3266,10 +3870,12 @@ mod tests { // Create member let member1 = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), Some(NO_SOURCE_IPS), ) .await @@ -3277,20 +3883,24 @@ mod tests { // Transition through states: "Joining" -> "Joined" -> "Left" datastore - .multicast_group_member_set_state( + .multicast_group_member_set_state_for_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), MulticastGroupMemberState::Joined, ) .await .expect("Transition to Joined should succeed"); datastore - .multicast_group_member_set_state( + .multicast_group_member_set_state_for_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), MulticastGroupMemberState::Left, ) .await @@ -3301,10 +3911,12 @@ mod tests { // state 'Left' with time_deleted=NULL. In this case, ON CONFLICT // updates the row (Left → Joining) instead of inserting a new one. let member2 = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), Some(NO_SOURCE_IPS), ) .await @@ -3320,9 +3932,11 @@ mod tests { direction: dropshot::PaginationOrder::Ascending, }; let members = datastore - .multicast_group_members_list_by_instance( + .multicast_group_members_list_by_parent( &opctx, - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), pagparams, ) .await @@ -3355,10 +3969,12 @@ mod tests { // Attempt to attach non-existent instance to non-existent group let result = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(fake_group_id), - InstanceUuid::from_untyped_uuid(fake_instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + fake_instance_id, + )), Some(NO_SOURCE_IPS), ) .await; @@ -3411,10 +4027,10 @@ mod tests { // Attach stopped instance should succeed let attached_member = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - instance_id, + MemberParentRef::Instance(instance_id), Some(NO_SOURCE_IPS), ) .await @@ -3422,10 +4038,10 @@ mod tests { // Verify member created with sled_id = NULL (no active VMM) let member = datastore - .multicast_group_member_get_by_group_and_instance( + .multicast_group_member_get_by_group_and_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - instance_id, + MemberParentRef::Instance(instance_id), ) .await .expect("Should get member") @@ -3480,10 +4096,10 @@ mod tests { let member1_sources: Vec = vec!["10.0.0.1".parse().unwrap(), "10.0.0.2".parse().unwrap()]; datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, group_id, - instance1, + MemberParentRef::Instance(instance1), Some(member1_sources.as_slice()), ) .await @@ -3514,10 +4130,10 @@ mod tests { let member2_sources: Vec = vec!["10.0.0.2".parse().unwrap(), "10.0.0.3".parse().unwrap()]; datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, group_id, - instance2, + MemberParentRef::Instance(instance2), Some(member2_sources.as_slice()), ) .await @@ -3546,10 +4162,10 @@ mod tests { .await; datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, group_id, - instance3, + MemberParentRef::Instance(instance3), Some(NO_SOURCE_IPS), ) .await diff --git a/nexus/db-queries/src/db/datastore/multicast/ops/member_attach.rs b/nexus/db-queries/src/db/datastore/multicast/ops/member_attach.rs index 89d941be552..e737a66ba72 100644 --- a/nexus/db-queries/src/db/datastore/multicast/ops/member_attach.rs +++ b/nexus/db-queries/src/db/datastore/multicast/ops/member_attach.rs @@ -37,7 +37,9 @@ use ipnetwork::IpNetwork; use uuid::Uuid; use nexus_db_lookup::DbConnection; -use nexus_db_model::{MulticastGroupMember, MulticastGroupMemberState}; +use nexus_db_model::{ + MemberParentRef, MulticastGroupMember, MulticastGroupMemberState, +}; use omicron_common::address::MAX_SOURCE_IPS_PER_GROUP; use omicron_common::api::external; @@ -49,6 +51,7 @@ use crate::db::true_or_cast_error::matches_sentinel; const GROUP_NOT_FOUND_SENTINEL: &str = "group-not-found"; const INSTANCE_NOT_FOUND_SENTINEL: &str = "instance-not-found"; const UNION_EXCEEDED_SENTINEL: &str = "source-union-exceeded"; +const PROBE_NOT_FOUND_SENTINEL: &str = "probe-not-found"; /// Result of attaching an instance to a multicast group. #[derive(Debug, Clone, PartialEq)] @@ -57,7 +60,7 @@ pub(crate) struct AttachMemberResult { pub member: MulticastGroupMember, } -/// Errors from attaching an instance to a multicast group. +/// Errors from attaching a parent to a multicast group. #[derive(Debug)] pub(crate) enum AttachMemberError { /// Multicast group doesn't exist or is being deleted @@ -67,6 +70,8 @@ pub(crate) enum AttachMemberError { /// Attaching this member would push the group's source IP union past /// the per-group cap. SourceUnionExceeded { cap: usize }, + /// Probe doesn't exist or has been deleted + ProbeNotFound, /// Database constraint violation (unique index, etc.) ConstraintViolation(String), /// Other database error @@ -77,13 +82,14 @@ impl AttachMemberError { /// Construct an [`AttachMemberError`] from a database error. /// /// This catches the sentinel errors that indicate validation failures - /// (group not found, instance not found, source union cap) as well as + /// (group / instance / probe not found, source union cap) as well as /// constraint violations. fn from_diesel(err: DieselError) -> Self { // Check for sentinel errors first let sentinels = [ GROUP_NOT_FOUND_SENTINEL, INSTANCE_NOT_FOUND_SENTINEL, + PROBE_NOT_FOUND_SENTINEL, UNION_EXCEEDED_SENTINEL, ]; if let Some(sentinel) = matches_sentinel(&err, &sentinels) { @@ -92,6 +98,7 @@ impl AttachMemberError { INSTANCE_NOT_FOUND_SENTINEL => { AttachMemberError::InstanceNotFound } + PROBE_NOT_FOUND_SENTINEL => AttachMemberError::ProbeNotFound, UNION_EXCEEDED_SENTINEL => { AttachMemberError::SourceUnionExceeded { cap: MAX_SOURCE_IPS_PER_GROUP, @@ -130,6 +137,11 @@ impl From for external::Error { "Instance does not exist or has been deleted", ) } + AttachMemberError::ProbeNotFound => { + external::Error::invalid_request( + "Probe does not exist or has been deleted", + ) + } AttachMemberError::SourceUnionExceeded { cap } => { external::Error::invalid_request(format!( "attaching this member would exceed the per-group \ @@ -174,7 +186,10 @@ impl From for external::Error { #[must_use = "Queries must be executed"] pub(crate) struct AttachMemberToGroupStatement { group_id: Uuid, - instance_id: Uuid, + parent: MemberParentRef, + /// Cached `parent.as_uuid()` so each CTE arm can borrow it instead + /// of rederiving it per bind. + parent_uuid: Uuid, new_member_id: Uuid, time_created: DateTime, time_modified: DateTime, @@ -190,25 +205,30 @@ impl AttachMemberToGroupStatement { /// # Arguments /// /// - `group_id`: Multicast group to attach to - /// - `instance_id`: Instance being attached as member + /// - `parent`: Typed reference to the parent being attached as a + /// member. The variant determines how the CTE resolves the parent's + /// hosting sled. /// - `new_member_id`: UUID for new member row (if creating) /// - `source_ips`: Source IPs for filtering (`None` preserves existing on reactivation) /// - /// CTEs atomically validate group is not in a "Deleting" state, that the - /// instance exists, retrieves the current `sled_id` from VMM table, and - /// (when a non-empty source list is being applied) verifies that the - /// resulting per-group source IP union stays within - /// [`MAX_SOURCE_IPS_PER_GROUP`]. + /// CTEs atomically validate group is not in a "Deleting" state, that + /// the parent exists, and retrieve the current `sled_id`. For instance + /// parents the lookup joins `instance` and `vmm` on + /// `active_propolis_id`. For probe parents the lookup reads `sled` + /// directly from the `probe` row. When a non-empty source list is + /// being applied, the CTEs also verify that the resulting per-group + /// source IP union stays within [`MAX_SOURCE_IPS_PER_GROUP`]. pub fn new( group_id: Uuid, - instance_id: Uuid, + parent: MemberParentRef, new_member_id: Uuid, source_ips: Option>, ) -> Self { let now = Utc::now(); Self { group_id, - instance_id, + parent, + parent_uuid: parent.as_uuid(), new_member_id, time_created: now, time_modified: now, @@ -272,43 +292,78 @@ impl AttachMemberToGroupStatement { Ok(()) } - /// Generates the `instance_sled` CTE (validates instance and gets sled_id). - /// - /// Joins instance and VMM tables via active_propolis_id to get current `sled_id`. - /// Returns one row with (`instance_id`, `sled_id`) if instance exists and not deleted. - fn push_instance_sled_cte<'a>( + /// Generates the `parent_sled` CTE that validates the parent exists + /// and produces a single `(id, sled_id)` row, dispatching on parent + /// kind so instance parents resolve through the `instance`/`vmm` join + /// while probe parents read `sled` directly from the `probe` row. + fn push_parent_sled_cte<'a>( &'a self, mut out: AstPass<'_, 'a, Pg>, ) -> QueryResult<()> { - out.push_sql( - "SELECT instance.id, vmm.sled_id \ - FROM instance \ - LEFT JOIN vmm ON instance.active_propolis_id = vmm.id \ - WHERE instance.id = ", - ); - out.push_bind_param::(&self.instance_id)?; - out.push_sql(" AND instance.time_deleted IS NULL"); + match self.parent { + MemberParentRef::Instance(_) => { + out.push_sql( + "SELECT instance.id, vmm.sled_id \ + FROM instance \ + LEFT JOIN vmm ON instance.active_propolis_id = vmm.id \ + WHERE instance.id = ", + ); + out.push_bind_param::( + &self.parent_uuid, + )?; + out.push_sql(" AND instance.time_deleted IS NULL"); + } + MemberParentRef::Probe(_) => { + out.push_sql( + "SELECT probe.id, probe.sled AS sled_id \ + FROM probe \ + WHERE probe.id = ", + ); + out.push_bind_param::( + &self.parent_uuid, + )?; + out.push_sql(" AND probe.time_deleted IS NULL"); + } + } Ok(()) } /// Generates the `validation` CTE that triggers sentinel errors on failure. /// /// Uses CAST to trigger a predictable error when validation fails: + /// - If parent not found → CAST(parent-kind sentinel AS BOOL) fails /// - If group not found → CAST('group-not-found' AS BOOL) fails - /// - If instance not found → CAST('instance-not-found' AS BOOL) fails /// - If the resulting source IP union would exceed the per-group cap /// → CAST('source-union-exceeded' AS BOOL) fails (only checked when a /// non-empty source list is being applied) /// - If all valid → CAST('TRUE' AS BOOL) succeeds /// + /// The parent-kind sentinel is selected from the [`MemberParentRef`] + /// variant, so the decoded error matches the parent the caller passed + /// (instance vs. probe). + /// /// This follows the pattern used in `network_interface.rs` and `external_ip.rs`. fn push_validation_cte<'a>( &'a self, mut out: AstPass<'_, 'a, Pg>, ) -> QueryResult<()> { + let parent_sentinel = match self.parent { + MemberParentRef::Instance(_) => INSTANCE_NOT_FOUND_SENTINEL, + MemberParentRef::Probe(_) => PROBE_NOT_FOUND_SENTINEL, + }; + + // SELECT CAST( + // CASE + // WHEN NOT EXISTS (SELECT 1 FROM parent_sled) THEN + // WHEN NOT EXISTS (SELECT 1 FROM valid_group) THEN 'group-not-found' + // ELSE 'TRUE' + // END AS BOOL + // ) AS validated + // + // Parent is checked first so the more specific error surfaces up front. out.push_sql("SELECT CAST(CASE "); - out.push_sql("WHEN NOT EXISTS (SELECT 1 FROM instance_sled) THEN '"); - out.push_sql(INSTANCE_NOT_FOUND_SENTINEL); + out.push_sql("WHEN NOT EXISTS (SELECT 1 FROM parent_sled) THEN '"); + out.push_sql(parent_sentinel); out.push_sql("' "); out.push_sql("WHEN NOT EXISTS (SELECT 1 FROM valid_group) THEN '"); out.push_sql(GROUP_NOT_FOUND_SENTINEL); @@ -351,7 +406,7 @@ impl AttachMemberToGroupStatement { ); out.push_bind_param::(&self.group_id)?; out.push_sql(" AND parent_id != "); - out.push_bind_param::(&self.instance_id)?; + out.push_bind_param::(&self.parent_uuid)?; out.push_sql(" AND time_deleted IS NULL "); out.push_sql("UNION ALL SELECT unnest("); out.push_bind_param::, _>( @@ -363,10 +418,10 @@ impl AttachMemberToGroupStatement { /// Generates the `upserted_member` CTE (performs unconditional upsert). /// - /// SELECT joins with both `valid_group` and `instance_sled` CTEs to: + /// SELECT joins with both `valid_group` and `parent_sled` CTEs to: /// 1. Ensure group exists and is attachable (FROM valid_group) /// 2. Retrieve group's multicast_ip (FROM valid_group) - /// 3. Retrieve instance's current sled_id (CROSS JOIN instance_sled) + /// 3. Retrieve the parent's current sled_id (CROSS JOIN parent_sled) /// /// ON CONFLICT clause uses partial unique index (only rows with time_deleted IS NULL): /// - Conflict only for members with time_deleted=NULL (active or stopped) @@ -380,13 +435,15 @@ impl AttachMemberToGroupStatement { &'a self, mut out: AstPass<'_, 'a, Pg>, ) -> QueryResult<()> { - // Column order matches schema: id, time_created, time_modified, time_deleted, - // external_group_id, parent_id, sled_id, state, version_added, version_removed, - // multicast_ip, source_ips + // Schema column order: id, time_created, time_modified, time_deleted, + // external_group_id, parent_id, sled_id, state, version_added, + // version_removed, multicast_ip, source_ips, parent_kind. The INSERT + // names columns explicitly, so the order below is independent of the + // table layout. out.push_sql( "INSERT INTO multicast_group_member (\ id, time_created, time_modified, external_group_id, \ - parent_id, sled_id, state, multicast_ip, source_ips) SELECT ", + parent_id, parent_kind, sled_id, state, multicast_ip, source_ips) SELECT ", ); out.push_bind_param::(&self.new_member_id)?; out.push_sql(", "); @@ -396,8 +453,12 @@ impl AttachMemberToGroupStatement { out.push_sql(", "); out.push_bind_param::(&self.group_id)?; out.push_sql(", "); - out.push_bind_param::(&self.instance_id)?; - out.push_sql(", instance_sled.sled_id, "); + out.push_bind_param::(&self.parent_uuid)?; + out.push_sql(", "); + out.push_sql(super::member_parent_kind_as_sql_literal( + self.parent.kind(), + )); + out.push_sql(", parent_sled.sled_id, "); out.push_sql(super::member_state_as_sql_literal( MulticastGroupMemberState::Joining, )); @@ -405,10 +466,10 @@ impl AttachMemberToGroupStatement { out.push_bind_param::, _>( &self.source_ips_for_insert, )?; - out.push_sql(" FROM valid_group CROSS JOIN instance_sled "); + out.push_sql(" FROM valid_group CROSS JOIN parent_sled "); // ON CONFLICT: only update "Left" members, preserve other states - out.push_sql("ON CONFLICT (external_group_id, parent_id) WHERE time_deleted IS NULL DO UPDATE SET state = CASE WHEN multicast_group_member.state = "); + out.push_sql("ON CONFLICT (external_group_id, parent_kind, parent_id) WHERE time_deleted IS NULL DO UPDATE SET state = CASE WHEN multicast_group_member.state = "); out.push_sql(super::member_state_as_sql_literal( MulticastGroupMemberState::Left, )); @@ -449,12 +510,12 @@ impl AttachMemberToGroupStatement { // Return all columns so caller gets full member record // Column order must match schema: id, time_created, time_modified, time_deleted, - // external_group_id, parent_id, sled_id, state, version_added, version_removed, - // multicast_ip, source_ips + // external_group_id, parent_id, sled_id, state, version_added, + // version_removed, multicast_ip, source_ips, parent_kind out.push_sql( " RETURNING id, time_created, time_modified, time_deleted, \ - external_group_id, parent_id, sled_id, state, version_added, \ - version_removed, multicast_ip, source_ips", + external_group_id, parent_id, sled_id, state, \ + version_added, version_removed, multicast_ip, source_ips, parent_kind", ); Ok(()) } @@ -468,12 +529,12 @@ impl AttachMemberToGroupStatement { mut out: AstPass<'_, 'a, Pg>, ) -> QueryResult<()> { // Column order must match schema: id, time_created, time_modified, time_deleted, - // external_group_id, parent_id, sled_id, state, version_added, version_removed, - // multicast_ip, source_ips + // external_group_id, parent_id, sled_id, state, version_added, + // version_removed, multicast_ip, source_ips, parent_kind out.push_sql( "SELECT id, time_created, time_modified, time_deleted, \ - external_group_id, parent_id, sled_id, state, version_added, \ - version_removed, multicast_ip, source_ips \ + external_group_id, parent_id, sled_id, state, \ + version_added, version_removed, multicast_ip, source_ips, parent_kind \ FROM upserted_member", ); Ok(()) @@ -489,9 +550,9 @@ impl QueryFragment for AttachMemberToGroupStatement { self.push_valid_group_cte(out.reborrow())?; out.push_sql("), "); - // CTE: Validate instance exists and get sled_id - out.push_sql("instance_sled AS ("); - self.push_instance_sled_cte(out.reborrow())?; + // CTE: Validate parent (instance or probe) exists and get sled_id + out.push_sql("parent_sled AS ("); + self.push_parent_sled_cte(out.reborrow())?; out.push_sql("), "); // CTE: Compute the prospective per-group source IP union size when diff --git a/nexus/db-queries/src/db/datastore/multicast/ops/member_reconcile.rs b/nexus/db-queries/src/db/datastore/multicast/ops/member_reconcile.rs index 5a8b9766429..5aaeda2017e 100644 --- a/nexus/db-queries/src/db/datastore/multicast/ops/member_reconcile.rs +++ b/nexus/db-queries/src/db/datastore/multicast/ops/member_reconcile.rs @@ -19,7 +19,7 @@ //! - Conditional logic based on instance_valid and sled_id changes //! //! Other states ("Joined", "Left") have simpler transitions using direct datastore -//! methods (e.g., `multicast_group_member_to_left_if_current`). +//! methods (e.g., `multicast_group_member_to_left_if_current_for_parent`). //! //! ## Operations //! @@ -41,7 +41,8 @@ use uuid::Uuid; use nexus_db_lookup::DbConnection; use nexus_db_model::{ - DbTypedUuid, MulticastGroupMember, MulticastGroupMemberState, + DbTypedUuid, MemberParentRef, MulticastGroupMember, + MulticastGroupMemberState, }; use nexus_db_schema::schema::multicast_group_member::dsl; use omicron_common::api::external::Error as ExternalError; @@ -114,13 +115,14 @@ impl From for ExternalError { pub async fn reconcile_joining_member( conn: &async_bb8_diesel::Connection, group_id: Uuid, - instance_id: Uuid, - instance_valid: bool, + parent: MemberParentRef, + parent_valid: bool, current_sled_id: Option>, ) -> Result { let member_opt: Option = dsl::multicast_group_member .filter(dsl::external_group_id.eq(group_id)) - .filter(dsl::parent_id.eq(instance_id)) + .filter(dsl::parent_id.eq(parent.as_uuid())) + .filter(dsl::parent_kind.eq(parent.kind())) .filter(dsl::time_deleted.is_null()) .filter(dsl::state.eq(MulticastGroupMemberState::Joining)) .select(MulticastGroupMember::as_select()) @@ -139,9 +141,9 @@ pub async fn reconcile_joining_member( let prior_sled_id = member.sled_id; - // Determine what action to take based on instance validity - if !instance_valid { - // Instance is invalid - transition to "Left" + // Determine what action to take based on parent validity + if !parent_valid { + // Parent is invalid - transition to "Left" let updated = diesel::update(dsl::multicast_group_member) .filter(dsl::id.eq(member.id)) .filter(dsl::state.eq(MulticastGroupMemberState::Joining)) @@ -179,7 +181,7 @@ pub async fn reconcile_joining_member( }) } } else if prior_sled_id != current_sled_id { - // Instance is valid but sled_id needs updating + // Parent is valid but sled_id needs updating let updated = diesel::update(dsl::multicast_group_member) .filter(dsl::id.eq(member.id)) .filter(dsl::state.eq(MulticastGroupMemberState::Joining)) @@ -234,9 +236,7 @@ mod tests { use nexus_types::identity::Resource; use omicron_test_utils::dev; - use omicron_uuid_kinds::{ - GenericUuid, InstanceUuid, MulticastGroupUuid, SledUuid, - }; + use omicron_uuid_kinds::{GenericUuid, MulticastGroupUuid, SledUuid}; use crate::db::pub_test_utils::helpers::{ SledUpdateBuilder, create_instance_with_vmm, @@ -268,7 +268,7 @@ mod tests { ) .await; - let (instance, _vmm) = create_instance_with_vmm( + let (instance_id, _vmm) = create_instance_with_vmm( &opctx, &datastore, &setup.authz_project, @@ -276,14 +276,13 @@ mod tests { setup.sled_id, ) .await; - let instance_id = *instance.as_untyped_uuid(); // Attach instance to create member in Joining state datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(instance_id), Some(NO_SOURCE_IPS), ) .await @@ -294,7 +293,7 @@ mod tests { let result = reconcile_joining_member( &conn, group.id(), - instance_id, + MemberParentRef::Instance(instance_id), false, // instance_valid=false Some(setup.sled_id.into()), ) @@ -307,10 +306,10 @@ mod tests { // Verify database state let member = datastore - .multicast_group_member_get_by_group_and_instance( + .multicast_group_member_get_by_group_and_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(instance_id), ) .await .expect("Should get member") @@ -356,7 +355,7 @@ mod tests { ) .await; - let (instance, _vmm) = create_instance_with_vmm( + let (instance_id, _vmm) = create_instance_with_vmm( &opctx, &datastore, &setup.authz_project, @@ -364,14 +363,13 @@ mod tests { setup.sled_id, ) .await; - let instance_id = *instance.as_untyped_uuid(); // Attach instance datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(instance_id), Some(NO_SOURCE_IPS), ) .await @@ -382,7 +380,7 @@ mod tests { let result = reconcile_joining_member( &conn, group.id(), - instance_id, + MemberParentRef::Instance(instance_id), true, // instance_valid=true Some(sled_id_new.into()), ) @@ -404,10 +402,10 @@ mod tests { // Verify database state let member = datastore - .multicast_group_member_get_by_group_and_instance( + .multicast_group_member_get_by_group_and_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(instance_id), ) .await .expect("Should get member") @@ -444,7 +442,7 @@ mod tests { ) .await; - let (instance, _vmm) = create_instance_with_vmm( + let (instance_id, _vmm) = create_instance_with_vmm( &opctx, &datastore, &setup.authz_project, @@ -452,24 +450,23 @@ mod tests { setup.sled_id, ) .await; - let instance_id = *instance.as_untyped_uuid(); // Attach instance datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(instance_id), Some(NO_SOURCE_IPS), ) .await .expect("Should attach instance"); let member_before = datastore - .multicast_group_member_get_by_group_and_instance( + .multicast_group_member_get_by_group_and_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(instance_id), ) .await .expect("Should get member") @@ -481,7 +478,7 @@ mod tests { let result = reconcile_joining_member( &conn, group.id(), - instance_id, + MemberParentRef::Instance(instance_id), true, // instance_valid=true Some(setup.sled_id.into()), ) @@ -497,10 +494,10 @@ mod tests { // Verify time_modified unchanged (no database update) let member_after = datastore - .multicast_group_member_get_by_group_and_instance( + .multicast_group_member_get_by_group_and_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(instance_id), ) .await .expect("Should get member") @@ -537,7 +534,7 @@ mod tests { .await; // Create instance but don't attach it - let (instance, _vmm) = create_instance_with_vmm( + let (instance_id, _vmm) = create_instance_with_vmm( &opctx, &datastore, &setup.authz_project, @@ -545,14 +542,13 @@ mod tests { setup.sled_id, ) .await; - let instance_id = *instance.as_untyped_uuid(); // Reconcile non-existent member let conn = datastore.pool_connection_authorized(&opctx).await.unwrap(); let result = reconcile_joining_member( &conn, group.id(), - instance_id, + MemberParentRef::Instance(instance_id), true, Some(setup.sled_id.into()), ) @@ -592,7 +588,7 @@ mod tests { ) .await; - let (instance, _vmm) = create_instance_with_vmm( + let (instance_id, _vmm) = create_instance_with_vmm( &opctx, &datastore, &setup.authz_project, @@ -600,14 +596,13 @@ mod tests { setup.sled_id, ) .await; - let instance_id = *instance.as_untyped_uuid(); // Attach instance datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(instance_id), Some(NO_SOURCE_IPS), ) .await @@ -615,10 +610,10 @@ mod tests { // Transition member to Joined state before reconciliation datastore - .multicast_group_member_set_state_if_current( + .multicast_group_member_set_state_if_current_for_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(instance_id), MulticastGroupMemberState::Joining, MulticastGroupMemberState::Joined, ) @@ -630,7 +625,7 @@ mod tests { let result = reconcile_joining_member( &conn, group.id(), - instance_id, + MemberParentRef::Instance(instance_id), false, // Would transition to Left if still Joining Some(setup.sled_id.into()), ) @@ -642,10 +637,10 @@ mod tests { // Verify member is still in Joined state let member = datastore - .multicast_group_member_get_by_group_and_instance( + .multicast_group_member_get_by_group_and_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(instance_id), ) .await .expect("Should get member") @@ -692,7 +687,7 @@ mod tests { ) .await; - let (instance, _vmm) = create_instance_with_vmm( + let (instance_id, _vmm) = create_instance_with_vmm( &opctx, &datastore, &setup.authz_project, @@ -700,14 +695,13 @@ mod tests { sled_id_a, ) .await; - let instance_id = *instance.as_untyped_uuid(); // Attach instance (starts on sled_a) datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(instance_id), Some(NO_SOURCE_IPS), ) .await @@ -718,7 +712,7 @@ mod tests { let result = reconcile_joining_member( &conn, group.id(), - instance_id, + MemberParentRef::Instance(instance_id), true, Some(sled_id_b.into()), ) @@ -740,10 +734,10 @@ mod tests { // Verify member remains in Joining state with new sled_id let member = datastore - .multicast_group_member_get_by_group_and_instance( + .multicast_group_member_get_by_group_and_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(instance_id), ) .await .expect("Should get member") diff --git a/nexus/db-queries/src/db/datastore/multicast/ops/mod.rs b/nexus/db-queries/src/db/datastore/multicast/ops/mod.rs index 9d46ddb552d..bf8f45b3f80 100644 --- a/nexus/db-queries/src/db/datastore/multicast/ops/mod.rs +++ b/nexus/db-queries/src/db/datastore/multicast/ops/mod.rs @@ -20,7 +20,10 @@ //! Helper functions convert state enums to SQL literals with compile-time //! safety (ensures SQL strings match enum definitions). -use nexus_db_model::{MulticastGroupMemberState, MulticastGroupState}; +use nexus_db_model::{ + MulticastGroupMemberParentKind, MulticastGroupMemberState, + MulticastGroupState, +}; pub mod member_attach; pub mod member_reconcile; @@ -53,3 +56,16 @@ pub(super) const fn member_state_as_sql_literal( MulticastGroupMemberState::Left => "'left'", } } + +/// Returns SQL literal for a member parent kind (e.g., "'instance'"). +/// +/// Compile-time safety: kind names in SQL must match enum definition. +/// Returned string includes single quotes for direct SQL interpolation. +pub(super) const fn member_parent_kind_as_sql_literal( + kind: MulticastGroupMemberParentKind, +) -> &'static str { + match kind { + MulticastGroupMemberParentKind::Instance => "'instance'", + MulticastGroupMemberParentKind::Probe => "'probe'", + } +} diff --git a/nexus/db-queries/src/db/datastore/probe.rs b/nexus/db-queries/src/db/datastore/probe.rs index 4b81ceaa14d..c6f37babcc4 100644 --- a/nexus/db-queries/src/db/datastore/probe.rs +++ b/nexus/db-queries/src/db/datastore/probe.rs @@ -12,14 +12,18 @@ use crate::db::pagination::Paginator; use crate::db::pagination::paginated; use async_bb8_diesel::AsyncRunQueryDsl; use chrono::Utc; +use diesel::BoolExpressionMethods as _; use diesel::JoinOnDsl as _; use diesel::NullableExpressionMethods as _; +use diesel::OptionalExtension as _; use diesel::{ExpressionMethods, QueryDsl, SelectableHelper}; use nexus_db_errors::ErrorHandler; use nexus_db_errors::public_error_from_diesel; use nexus_db_lookup::LookupPath; use nexus_db_model::IncompleteNetworkInterface; use nexus_db_model::IpVersion; +use nexus_db_model::MulticastGroupMemberParentKind; +use nexus_db_model::MulticastGroupMemberState; use nexus_db_model::Probe; use nexus_db_model::VpcSubnet; use nexus_types::external_api::instance::PrivateIpStackCreate; @@ -41,6 +45,7 @@ use omicron_uuid_kinds::SledUuid; use ref_cast::RefCast; use sled_agent_client::types::ProbeCreate; use sled_agent_types::inventory::NetworkInterface; +use std::collections::HashMap; use uuid::Uuid; impl super::DataStore { @@ -161,6 +166,42 @@ impl super::DataStore { }) } + /// Look up the hosting sled for a probe. + /// + /// Returns `Some(sled_id)` when the probe exists and is not + /// soft-deleted, `None` otherwise. Probes carry `sled` directly on + /// the row, so a single scalar lookup is sufficient (no VMM + /// indirection like instances). + /// + /// This is used by the multicast RPW reconciler to advance probe-typed + /// members through the membership state machine without having to re-run + /// authz on each pass. + pub async fn probe_get_sled_for_multicast( + &self, + opctx: &OpContext, + probe_id: ProbeUuid, + ) -> Result, omicron_common::api::external::Error> { + use nexus_db_schema::schema::probe::dsl; + + let sled = dsl::probe + .filter(dsl::id.eq(probe_id.into_untyped_uuid())) + .filter(dsl::time_deleted.is_null()) + .select(dsl::sled) + .first_async::( + &*self.pool_connection_authorized(opctx).await?, + ) + .await + .optional() + .map_err(|e| { + nexus_db_errors::public_error_from_diesel( + e, + nexus_db_errors::ErrorHandler::Server, + ) + })?; + + Ok(sled.map(SledUuid::from_untyped_uuid)) + } + /// Get information about a particular probe given its name or id. pub async fn probe_get( &self, @@ -328,6 +369,8 @@ impl super::DataStore { external_ip, network_interface, probe, vpc, vpc_subnet, }; + let conn = self.pool_connection_authorized(opctx).await?; + // TODO-correctness: This inner join below assumes exactly one external // IP for each probe. That's true today because of how we specify the // create-params in the external API. That has one IP Pool, and we @@ -338,89 +381,144 @@ impl super::DataStore { // In that case, we'd get rows like: // // probe_id, vpc stuff, vpc_subnet stuff, [eip0, eip1, ...] - paginated(probe::dsl::probe, probe::dsl::id, pagparams) - .inner_join( - external_ip::dsl::external_ip - .on(external_ip::dsl::parent_id - .eq(probe::dsl::id.nullable())), - ) - .inner_join( - network_interface::dsl::network_interface - .on(network_interface::dsl::parent_id.eq(probe::dsl::id)), - ) - .inner_join( - vpc_subnet::dsl::vpc_subnet - .on(vpc_subnet::dsl::id - .eq(network_interface::dsl::subnet_id)), - ) - .inner_join( - vpc::dsl::vpc.on(vpc::dsl::id.eq(vpc_subnet::dsl::vpc_id)), + let mut probes: Vec = + paginated(probe::dsl::probe, probe::dsl::id, pagparams) + .inner_join(external_ip::dsl::external_ip.on( + external_ip::dsl::parent_id.eq(probe::dsl::id.nullable()), + )) + .inner_join( + network_interface::dsl::network_interface + .on(network_interface::dsl::parent_id + .eq(probe::dsl::id)), + ) + .inner_join(vpc_subnet::dsl::vpc_subnet.on( + vpc_subnet::dsl::id.eq(network_interface::dsl::subnet_id), + )) + .inner_join( + vpc::dsl::vpc.on(vpc::dsl::id.eq(vpc_subnet::dsl::vpc_id)), + ) + .filter(probe::dsl::sled.eq(sled_id.into_untyped_uuid())) + .filter(probe::dsl::time_deleted.is_null()) + .filter(external_ip::dsl::time_deleted.is_null()) + .filter(network_interface::dsl::time_deleted.is_null()) + .filter(vpc_subnet::dsl::time_deleted.is_null()) + .filter(vpc::dsl::time_deleted.is_null()) + .select(( + probe::dsl::id, + external_ip::dsl::ip, + external_ip::dsl::first_port, + external_ip::dsl::last_port, + external_ip::dsl::kind, + nexus_db_model::NetworkInterface::as_select(), + vpc_subnet::dsl::ipv4_block, + vpc_subnet::dsl::ipv6_block, + vpc::dsl::vni, + )) + .get_results_async::<( + Uuid, + ipnetwork::IpNetwork, + nexus_db_model::SqlU16, + nexus_db_model::SqlU16, + nexus_db_model::IpKind, + nexus_db_model::NetworkInterface, + nexus_db_model::Ipv4Net, + nexus_db_model::Ipv6Net, + nexus_db_model::Vni, + )>(&*conn) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) + .and_then(|rows| { + rows.into_iter() + .map( + |( + probe_id, + ip, + first_port, + last_port, + kind, + nic, + ipv4_block, + ipv6_block, + vni, + )| { + let kind = db_ip_kind_to_sled(kind); + let external_ips = vec![ + sled_agent_client::types::ExternalIp { + first_port: first_port.into(), + ip: ip.ip(), + kind, + last_port: last_port.into(), + }, + ]; + let interface = NetworkInterface { + vni: vni.0, + ..nic.into_internal( + *ipv4_block, + *ipv6_block, + )? + }; + Ok(ProbeCreate { + id: ProbeUuid::from_untyped_uuid(probe_id), + external_ips, + interface, + multicast_groups: Vec::new(), + }) + }, + ) + .collect::>() + })?; + + if probes.is_empty() { + return Ok(probes); + } + + // Backfill `multicast_groups` for this page with a single batched + // lookup against `multicast_group_member`, avoiding per-probe N+1 + // queries. Membership rows carry `multicast_ip` and `source_ips` + // directly, so no join against `multicast_group` is required to + // build the sled-agent wire type. + use nexus_db_schema::schema::multicast_group_member::dsl as mgm; + + let probe_ids: Vec = + probes.iter().map(|p| p.id.into_untyped_uuid()).collect(); + + let memberships: Vec<( + Uuid, + ipnetwork::IpNetwork, + Vec, + )> = mgm::multicast_group_member + .filter(mgm::parent_id.eq_any(probe_ids)) + .filter(mgm::parent_kind.eq(MulticastGroupMemberParentKind::Probe)) + .filter(mgm::time_deleted.is_null()) + .filter( + mgm::state + .eq(MulticastGroupMemberState::Joining) + .or(mgm::state.eq(MulticastGroupMemberState::Joined)), ) - .filter(probe::dsl::sled.eq(sled_id.into_untyped_uuid())) - .filter(probe::dsl::time_deleted.is_null()) - .filter(external_ip::dsl::time_deleted.is_null()) - .filter(network_interface::dsl::time_deleted.is_null()) - .filter(vpc_subnet::dsl::time_deleted.is_null()) - .filter(vpc::dsl::time_deleted.is_null()) - .select(( - probe::dsl::id, - external_ip::dsl::ip, - external_ip::dsl::first_port, - external_ip::dsl::last_port, - external_ip::dsl::kind, - nexus_db_model::NetworkInterface::as_select(), - vpc_subnet::dsl::ipv4_block, - vpc_subnet::dsl::ipv6_block, - vpc::dsl::vni, - )) - .get_results_async::<( - Uuid, - ipnetwork::IpNetwork, - nexus_db_model::SqlU16, - nexus_db_model::SqlU16, - nexus_db_model::IpKind, - nexus_db_model::NetworkInterface, - nexus_db_model::Ipv4Net, - nexus_db_model::Ipv6Net, - nexus_db_model::Vni, - )>(&*self.pool_connection_authorized(opctx).await?) + .select((mgm::parent_id, mgm::multicast_ip, mgm::source_ips)) + .get_results_async(&*conn) .await - .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) - .and_then(|rows| { - rows.into_iter() - .map( - |( - probe_id, - ip, - first_port, - last_port, - kind, - nic, - ipv4_block, - ipv6_block, - vni, - )| { - let kind = db_ip_kind_to_sled(kind); - let external_ips = - vec![sled_agent_client::types::ExternalIp { - first_port: first_port.into(), - ip: ip.ip(), - kind, - last_port: last_port.into(), - }]; - let interface = NetworkInterface { - vni: vni.0, - ..nic.into_internal(*ipv4_block, *ipv6_block)? - }; - Ok(ProbeCreate { - id: ProbeUuid::from_untyped_uuid(probe_id), - external_ips, - interface, - }) - }, - ) - .collect::>() - }) + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?; + + let mut by_probe: HashMap< + Uuid, + Vec, + > = HashMap::new(); + for (parent_id, group_ip, source_ips) in memberships { + by_probe.entry(parent_id).or_default().push( + sled_agent_client::types::InstanceMulticastMembership { + group_ip: group_ip.ip(), + sources: source_ips.into_iter().map(|s| s.ip()).collect(), + }, + ); + } + + for probe in probes.iter_mut() { + probe.multicast_groups = + by_probe.remove(probe.id.as_untyped_uuid()).unwrap_or_default(); + } + + Ok(probes) } /// Remove a probe from the data store. diff --git a/nexus/db-schema/src/enums.rs b/nexus/db-schema/src/enums.rs index cddaf8f4d36..c25d68a8dc4 100644 --- a/nexus/db-schema/src/enums.rs +++ b/nexus/db-schema/src/enums.rs @@ -81,6 +81,7 @@ define_enums! { MigrationStateEnum => "migration_state", MulticastGroupStateEnum => "multicast_group_state", MulticastGroupMemberStateEnum => "multicast_group_member_state", + MulticastGroupMemberParentKindEnum => "multicast_group_member_parent_kind", NetworkInterfaceKindEnum => "network_interface_kind", OximeterReadModeEnum => "oximeter_read_mode", PhysicalDiskKindEnum => "physical_disk_kind", diff --git a/nexus/db-schema/src/schema.rs b/nexus/db-schema/src/schema.rs index 1099c520092..a6e42450b8c 100644 --- a/nexus/db-schema/src/schema.rs +++ b/nexus/db-schema/src/schema.rs @@ -3014,6 +3014,7 @@ table! { version_removed -> Nullable, multicast_ip -> Inet, source_ips -> Array, + parent_kind -> crate::enums::MulticastGroupMemberParentKindEnum, } } diff --git a/nexus/external-api/src/lib.rs b/nexus/external-api/src/lib.rs index 4d069bf609f..566086143a3 100644 --- a/nexus/external-api/src/lib.rs +++ b/nexus/external-api/src/lib.rs @@ -83,6 +83,7 @@ api_versions!([ // | date-based version should be at the top of the list. // v // (next_yyyy_mm_dd_nn, IDENT), + (2026_05_26_00, PROBE_MULTICAST), (2026_05_22_00, MULTICAST_SOURCE_LIMITS), (2026_05_20_00, ADD_CONTACT_SUPPORT_TO_UPDATE_STATUS), (2026_05_08_00, MANUAL_DISK_ADOPTION), @@ -3175,7 +3176,7 @@ pub trait NexusExternalApi { method = GET, path = "/v1/multicast-groups/{multicast_group}/members", tags = ["experimental"], - versions = VERSION_MULTICAST_IMPLICIT_LIFECYCLE_UPDATES.., + versions = VERSION_PROBE_MULTICAST.., }] async fn multicast_group_member_list( rqctx: RequestContext, @@ -3186,6 +3187,41 @@ pub trait NexusExternalApi { HttpError, >; + /// List members of multicast group + #[endpoint { + method = GET, + path = "/v1/multicast-groups/{multicast_group}/members", + tags = ["experimental"], + operation_id = "multicast_group_member_list", + versions = VERSION_MULTICAST_IMPLICIT_LIFECYCLE_UPDATES..VERSION_PROBE_MULTICAST, + }] + async fn multicast_group_member_list_v2026_01_08_00( + rqctx: RequestContext, + path_params: Path, + query_params: Query, + ) -> Result< + HttpResponseOk< + ResultsPage, + >, + HttpError, + > { + let HttpResponseOk(page) = + Self::multicast_group_member_list(rqctx, path_params, query_params) + .await?; + // Drop probe-parented rows that have no representation in this + // version's type. A single-row fetch would surface the mismatch + // as 406, but list pages stay readable for the instance subset. + let items = page + .items + .into_iter() + .filter_map(|m| { + v2026_01_08_00::multicast::MulticastGroupMember::try_from(m) + .ok() + }) + .collect(); + Ok(HttpResponseOk(ResultsPage { items, next_page: page.next_page })) + } + /// List members of multicast group /// /// The group can be specified by name or UUID. @@ -3207,14 +3243,19 @@ pub trait NexusExternalApi { HttpError, > { let path = path_params.map(Into::into); - Self::multicast_group_member_list(rqctx, path, query_params).await.map( - |HttpResponseOk(page)| { - HttpResponseOk(ResultsPage { - items: page.items.into_iter().map(|m| m.into()).collect(), - next_page: page.next_page, - }) - }, - ) + let HttpResponseOk(page) = + Self::multicast_group_member_list(rqctx, path, query_params) + .await?; + let items = page + .items + .into_iter() + .filter_map(|m| { + v2026_01_08_00::multicast::MulticastGroupMember::try_from(m) + .ok() + .map(Into::into) + }) + .collect(); + Ok(HttpResponseOk(ResultsPage { items, next_page: page.next_page })) } /// Add instance to multicast group @@ -5879,7 +5920,7 @@ pub trait NexusExternalApi { method = GET, path = "/v1/instances/{instance}/multicast-groups", tags = ["experimental"], - versions = VERSION_MULTICAST_IMPLICIT_LIFECYCLE_UPDATES.., + versions = VERSION_PROBE_MULTICAST.., }] async fn instance_multicast_group_list( rqctx: RequestContext, @@ -5892,6 +5933,43 @@ pub trait NexusExternalApi { HttpError, >; + /// List multicast groups for an instance + #[endpoint { + method = GET, + path = "/v1/instances/{instance}/multicast-groups", + tags = ["experimental"], + operation_id = "instance_multicast_group_list", + versions = VERSION_MULTICAST_IMPLICIT_LIFECYCLE_UPDATES..VERSION_PROBE_MULTICAST, + }] + async fn instance_multicast_group_list_v2026_01_08_00( + rqctx: RequestContext, + query_params: Query< + PaginatedById, + >, + path_params: Path, + ) -> Result< + HttpResponseOk< + ResultsPage, + >, + HttpError, + > { + let HttpResponseOk(page) = Self::instance_multicast_group_list( + rqctx, + query_params, + path_params, + ) + .await?; + let items = page + .items + .into_iter() + .filter_map(|m| { + v2026_01_08_00::multicast::MulticastGroupMember::try_from(m) + .ok() + }) + .collect(); + Ok(HttpResponseOk(ResultsPage { items, next_page: page.next_page })) + } + /// List multicast groups for an instance #[endpoint { method = GET, @@ -5920,14 +5998,22 @@ pub trait NexusExternalApi { )?) .map_err(|e| HttpError::for_bad_request(None, e.to_string())) })?; - Self::instance_multicast_group_list(rqctx, query_params, path_params) - .await - .map(|HttpResponseOk(page)| { - HttpResponseOk(ResultsPage { - next_page: page.next_page, - items: page.items.into_iter().map(Into::into).collect(), - }) + let HttpResponseOk(page) = Self::instance_multicast_group_list( + rqctx, + query_params, + path_params, + ) + .await?; + let items = page + .items + .into_iter() + .filter_map(|m| { + v2026_01_08_00::multicast::MulticastGroupMember::try_from(m) + .ok() + .map(Into::into) }) + .collect(); + Ok(HttpResponseOk(ResultsPage { items, next_page: page.next_page })) } /// Join multicast group by name, IP address, or UUID @@ -5946,7 +6032,7 @@ pub trait NexusExternalApi { method = PUT, path = "/v1/instances/{instance}/multicast-groups/{multicast_group}", tags = ["experimental"], - versions = VERSION_MULTICAST_SOURCE_LIMITS.., + versions = VERSION_PROBE_MULTICAST.., }] async fn instance_multicast_group_join( rqctx: RequestContext, @@ -5958,6 +6044,28 @@ pub trait NexusExternalApi { HttpError, >; + /// Join multicast group by name, IP address, or UUID + #[endpoint { + method = PUT, + path = "/v1/instances/{instance}/multicast-groups/{multicast_group}", + tags = ["experimental"], + operation_id = "instance_multicast_group_join", + versions = VERSION_MULTICAST_SOURCE_LIMITS..VERSION_PROBE_MULTICAST, + }] + async fn instance_multicast_group_join_v2026_05_22_00( + rqctx: RequestContext, + path_params: Path< + v2026_01_08_00::multicast::InstanceMulticastGroupPath, + >, + query_params: Query, + body_params: TypedBody< + v2026_01_08_00::multicast::InstanceMulticastGroupJoin, + >, + ) -> Result< + HttpResponseCreated, + HttpError, + >; + /// Join multicast group by name, IP address, or UUID /// /// Groups can be referenced by name, IP address, or UUID. If the group @@ -7834,7 +7942,7 @@ pub trait NexusExternalApi { method = POST, path = "/experimental/v1/probes", tags = ["experimental"], // system/probes: only one tag is allowed - versions = VERSION_POOL_SELECTION_ENUMS.., + versions = VERSION_PROBE_MULTICAST.., }] async fn probe_create( rqctx: RequestContext, @@ -7842,6 +7950,22 @@ pub trait NexusExternalApi { new_probe: TypedBody, ) -> Result, HttpError>; + /// Create instrumentation probe + #[endpoint { + operation_id = "probe_create", + method = POST, + path = "/experimental/v1/probes", + tags = ["experimental"], // system/probes: only one tag is allowed + versions = VERSION_POOL_SELECTION_ENUMS..VERSION_PROBE_MULTICAST, + }] + async fn probe_create_v2026_01_05_00( + rqctx: RequestContext, + query_params: Query, + new_probe: TypedBody, + ) -> Result, HttpError> { + Self::probe_create(rqctx, query_params, new_probe.map(Into::into)).await + } + /// Create instrumentation probe #[endpoint { operation_id = "probe_create", @@ -7855,7 +7979,12 @@ pub trait NexusExternalApi { query_params: Query, new_probe: TypedBody, ) -> Result, HttpError> { - Self::probe_create(rqctx, query_params, new_probe.map(Into::into)).await + Self::probe_create_v2026_01_05_00( + rqctx, + query_params, + new_probe.map(Into::into), + ) + .await } /// Delete instrumentation probe diff --git a/nexus/src/app/background/tasks/multicast/members.rs b/nexus/src/app/background/tasks/multicast/members.rs index 9fb973fd5d9..15fee6bf51c 100644 --- a/nexus/src/app/background/tasks/multicast/members.rs +++ b/nexus/src/app/background/tasks/multicast/members.rs @@ -115,7 +115,7 @@ use uuid::Uuid; use dpd_client::types::{BackplaneLink, Direction, LinkId, PortId, Rear}; use nexus_db_model::{ - DbTypedUuid, MulticastGroup, MulticastGroupMember, + DbTypedUuid, MemberParentRef, MulticastGroup, MulticastGroupMember, MulticastGroupMemberState, MulticastGroupState, Sled, }; use nexus_db_queries::context::OpContext; @@ -126,8 +126,8 @@ use nexus_types::deployment::SledFilter; use nexus_types::identity::{Asset, Resource}; use omicron_common::api::external::{DataPageParams, InstanceState}; use omicron_uuid_kinds::{ - GenericUuid, InstanceUuid, MulticastGroupUuid, PropolisUuid, SledKind, - SledUuid, + GenericUuid, InstanceUuid, MulticastGroupUuid, ProbeUuid, PropolisUuid, + SledKind, SledUuid, }; use super::{MulticastGroupReconciler, StateTransition, SwitchBackplanePort}; @@ -135,12 +135,14 @@ use crate::app::multicast::dataplane::MulticastDataplaneClient; use crate::app::multicast::sled::MulticastSledClient; use crate::app::multicast::switch_zone::MulticastSwitchZoneClient; -/// Pre-fetched instance state for multicast reconciliation. +/// Runtime state for a multicast group member's parent (instance or +/// probe), as observed during a reconciliation pass. +/// +/// Instance state is pre-fetched per pass into [`InstanceStateMap`]. +/// Probe state is fetched per row since probes are rare. #[derive(Clone, Copy, Debug, Default)] -struct InstanceMulticastState { - /// Whether the instance is in a state that can receive multicast traffic. +struct ParentRuntimeState { valid: bool, - /// Current sled hosting the VMM, if any. sled_id: Option, } @@ -159,7 +161,7 @@ struct MemberReconcileCtx<'a> { } /// Maps instance_id to pre-fetched multicast-relevant state. -type InstanceStateMap = HashMap; +type InstanceStateMap = HashMap; type MemberPortKey = (PortId, LinkId); /// Sled-to-port mapping for a single reconciliation pass. @@ -225,59 +227,6 @@ struct SledIdUpdate { new: Option>, } -/// Trait for processing different types of multicast group members. -trait MemberStateProcessor { - /// Process a member in "Joining" state. - async fn process_joining( - &self, - reconciler: &MulticastGroupReconciler, - ctx: &MemberReconcileCtx<'_>, - ) -> Result; - - /// Process a member in "Joined" state. - async fn process_joined( - &self, - reconciler: &MulticastGroupReconciler, - ctx: &MemberReconcileCtx<'_>, - ) -> Result; - - /// Process a member in "Left" state. - async fn process_left( - &self, - reconciler: &MulticastGroupReconciler, - ctx: &MemberReconcileCtx<'_>, - ) -> Result; -} - -/// Processor for instance-based multicast group members. -struct InstanceMemberProcessor; - -impl MemberStateProcessor for InstanceMemberProcessor { - async fn process_joining( - &self, - reconciler: &MulticastGroupReconciler, - ctx: &MemberReconcileCtx<'_>, - ) -> Result { - reconciler.handle_instance_joining(ctx).await - } - - async fn process_joined( - &self, - reconciler: &MulticastGroupReconciler, - ctx: &MemberReconcileCtx<'_>, - ) -> Result { - reconciler.handle_instance_joined(ctx).await - } - - async fn process_left( - &self, - reconciler: &MulticastGroupReconciler, - ctx: &MemberReconcileCtx<'_>, - ) -> Result { - reconciler.handle_instance_left(ctx).await - } -} - impl MulticastGroupReconciler { /// Group states that require member reconciliation processing. const RECONCILABLE_STATES: &'static [MulticastGroupState] = &[ @@ -301,16 +250,10 @@ impl MulticastGroupReconciler { // Get all groups that need member state processing ("Creating" and "Active") let groups = self.get_reconcilable_groups(opctx).await?; - // Build the reconciliation pass sled-to-port mapping once and share - // it across all members in this pass. Avoids per-member DDM RPCs - // and per-member inventory queries. - // - // A build failure (no DDM peers and no inventory yet) downgrades - // to an empty map: "Joining" → "Left" for stopped instances is a - // DB-only CAS that doesn't need a port lookup, so it still - // converges. Members that do need a port lookup (e.g. "Joining" - // → "Joined") fail their own processing this pass and retry on - // the next. + // Build the sled-to-port mapping once per pass and share it across + // members. A build failure (no DDM peers, no inventory yet) + // downgrades to an empty map. CAS-only transitions still converge. + // Members that need a port lookup fail this pass and retry next. let SledPortMap { sled_to_ports, ddm_inventory_drift: drift_count } = match self .build_sled_port_map( @@ -501,19 +444,29 @@ impl MulticastGroupReconciler { .await; } - // For now, all members are instance-based, but this is where we'd - // dispatch to different processors for different member types - let processor = InstanceMemberProcessor; + // Dispatch on (parent_kind, state). Probe OPTE state is set up at + // probe-zone provisioning, so the probe path skips the sled-agent + // RPCs the instance path needs. - match member.state { - MulticastGroupMemberState::Joining => { - processor.process_joining(self, ctx).await + use MulticastGroupMemberState::{Joined, Joining, Left}; + match (member.parent_ref(), member.state) { + (MemberParentRef::Instance(id), Joining) => { + self.handle_instance_joining(ctx, id).await + } + (MemberParentRef::Instance(id), Joined) => { + self.handle_instance_joined(ctx, id).await + } + (MemberParentRef::Instance(id), Left) => { + self.handle_instance_left(ctx, id).await } - MulticastGroupMemberState::Joined => { - processor.process_joined(self, ctx).await + (MemberParentRef::Probe(id), Joining) => { + self.handle_probe_joining(ctx, id).await } - MulticastGroupMemberState::Left => { - processor.process_left(self, ctx).await + (MemberParentRef::Probe(id), Joined) => { + self.handle_probe_joined(ctx, id).await + } + (MemberParentRef::Probe(_), Left) => { + self.handle_probe_left(ctx).await } } } @@ -566,6 +519,7 @@ impl MulticastGroupReconciler { async fn handle_instance_joining( &self, ctx: &MemberReconcileCtx<'_>, + instance_id: InstanceUuid, ) -> Result { let instance_state = self.get_instance_state_from_cache(ctx.instance_states, ctx.member); @@ -573,6 +527,7 @@ impl MulticastGroupReconciler { let reconcile_res = self .execute_joining_reconciliation( ctx, + instance_id, instance_state.valid, instance_state.sled_id, ) @@ -580,6 +535,7 @@ impl MulticastGroupReconciler { self.process_joining_reconcile_result( ctx, + instance_id, instance_state, reconcile_res, ) @@ -591,7 +547,7 @@ impl MulticastGroupReconciler { &self, instance_states: &InstanceStateMap, member: &MulticastGroupMember, - ) -> InstanceMulticastState { + ) -> ParentRuntimeState { instance_states.get(&member.parent_id).copied().unwrap_or_default() } @@ -599,16 +555,17 @@ impl MulticastGroupReconciler { async fn execute_joining_reconciliation( &self, ctx: &MemberReconcileCtx<'_>, + instance_id: InstanceUuid, instance_valid: bool, current_sled_id: Option, ) -> Result { let current_sled_id_db = current_sled_id.map(|id| id.into()); self.datastore - .multicast_group_member_reconcile_joining( + .multicast_group_member_reconcile_joining_for_parent( ctx.opctx, MulticastGroupUuid::from_untyped_uuid(ctx.group.id()), - InstanceUuid::from_untyped_uuid(ctx.member.parent_id), + MemberParentRef::Instance(instance_id), instance_valid, current_sled_id_db, ) @@ -620,7 +577,8 @@ impl MulticastGroupReconciler { async fn process_joining_reconcile_result( &self, ctx: &MemberReconcileCtx<'_>, - instance_state: InstanceMulticastState, + instance_id: InstanceUuid, + instance_state: ParentRuntimeState, reconcile_result: ReconcileJoiningResult, ) -> Result { match reconcile_result.action { @@ -631,6 +589,7 @@ impl MulticastGroupReconciler { ReconcileAction::UpdatedSledId { old, new } => { self.handle_sled_id_updated( ctx, + instance_id, instance_state, SledIdUpdate { old, new }, ) @@ -638,7 +597,12 @@ impl MulticastGroupReconciler { } ReconcileAction::NotFound | ReconcileAction::NoChange => { - self.handle_no_change_or_not_found(ctx, instance_state).await + self.handle_no_change_or_not_found( + ctx, + instance_id, + instance_state, + ) + .await } } } @@ -665,7 +629,8 @@ impl MulticastGroupReconciler { async fn handle_sled_id_updated( &self, ctx: &MemberReconcileCtx<'_>, - instance_state: InstanceMulticastState, + instance_id: InstanceUuid, + instance_state: ParentRuntimeState, sled_id_update: SledIdUpdate, ) -> Result { trace!( @@ -678,14 +643,15 @@ impl MulticastGroupReconciler { "instance_valid" => instance_state.valid ); - self.try_complete_join_if_ready(ctx, instance_state).await + self.try_complete_join_if_ready(ctx, instance_id, instance_state).await } /// Handle the case where no changes were made or member was not found. async fn handle_no_change_or_not_found( &self, ctx: &MemberReconcileCtx<'_>, - instance_state: InstanceMulticastState, + instance_id: InstanceUuid, + instance_state: ParentRuntimeState, ) -> Result { // Check if member is already in Joined state if ctx.member.state == MulticastGroupMemberState::Joined { @@ -700,7 +666,7 @@ impl MulticastGroupReconciler { } // Try to complete the join if conditions are met - self.try_complete_join_if_ready(ctx, instance_state).await + self.try_complete_join_if_ready(ctx, instance_id, instance_state).await } fn is_ready_to_join( @@ -714,10 +680,13 @@ impl MulticastGroupReconciler { async fn try_complete_join_if_ready( &self, ctx: &MemberReconcileCtx<'_>, - instance_state: InstanceMulticastState, + instance_id: InstanceUuid, + instance_state: ParentRuntimeState, ) -> Result { if self.is_ready_to_join(ctx.group, instance_state.valid) { - let joined = self.complete_instance_member_join(ctx, None).await?; + let joined = self + .complete_instance_member_join(ctx, instance_id, None) + .await?; if joined { Ok(StateTransition::StateChanged) } else { @@ -741,6 +710,7 @@ impl MulticastGroupReconciler { async fn handle_instance_joined( &self, ctx: &MemberReconcileCtx<'_>, + instance_id: InstanceUuid, ) -> Result { let instance_state = ctx .instance_states @@ -749,16 +719,16 @@ impl MulticastGroupReconciler { .unwrap_or_default(); match (instance_state.valid, instance_state.sled_id) { - (false, _) => self.handle_invalid_instance(ctx).await, + (false, _) => self.handle_invalid_instance(ctx, instance_id).await, (true, Some(sled_id)) if ctx.member.sled_id != Some(sled_id.into()) => { - self.handle_sled_migration(ctx, sled_id).await + self.handle_sled_migration(ctx, instance_id, sled_id).await } (true, Some(_)) => { - self.verify_members(ctx).await?; + self.verify_members(ctx, instance_id).await?; trace!( ctx.opctx.log, "member configuration verified, no changes needed"; @@ -768,7 +738,9 @@ impl MulticastGroupReconciler { Ok(StateTransition::NoChange) } - (true, None) => self.handle_joined_without_sled(ctx).await, + (true, None) => { + self.handle_joined_without_sled(ctx, instance_id).await + } } } @@ -776,6 +748,7 @@ impl MulticastGroupReconciler { async fn handle_invalid_instance( &self, ctx: &MemberReconcileCtx<'_>, + instance_id: InstanceUuid, ) -> Result { let MemberReconcileCtx { opctx, group, member, sled_client, .. } = ctx; // Remove from dataplane first @@ -810,10 +783,10 @@ impl MulticastGroupReconciler { // Update database state (atomically set "Left" and clear `sled_id`) let updated = self .datastore - .multicast_group_member_to_left_if_current( + .multicast_group_member_to_left_if_current_for_parent( opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(member.parent_id), + MemberParentRef::Instance(instance_id), MulticastGroupMemberState::Joined, ) .await @@ -863,6 +836,7 @@ impl MulticastGroupReconciler { async fn handle_sled_migration( &self, ctx: &MemberReconcileCtx<'_>, + instance_id: InstanceUuid, new_sled_id: SledUuid, ) -> Result { info!( @@ -889,22 +863,16 @@ impl MulticastGroupReconciler { return Err(e); } - // Source-sled OPTE cleanup (M2P, forwarding, port subscription) - // is handled by VMM teardown: remove_propolis_zone -> - // release_opte_ports -> PortTicket::release_inner, which - // clears multicast subscriptions along with V2P and firewall - // rules. - // - // This is consistent with all other OPTE state. Nexus - // never explicitly calls sled-agent for source-sled cleanup - // after migration. + // Source-sled OPTE cleanup (M2P, forwarding, multicast subscription) + // is handled by VMM teardown via `release_opte_ports`. Nexus does + // not call sled-agent for source-sled cleanup after migration. // Update `sled_id` in database using CAS let updated = self .datastore .multicast_group_member_update_sled_id_if_current( ctx.opctx, - InstanceUuid::from_untyped_uuid(ctx.member.parent_id), + instance_id, ctx.member.sled_id, Some(new_sled_id.into()), ) @@ -925,9 +893,12 @@ impl MulticastGroupReconciler { return Ok(StateTransition::NoChange); } - // Re-apply configuration on new sled. Pass `new_sled_id` explicitly - // because the in-memory member struct still has the old sled_id. - match self.complete_instance_member_join(ctx, Some(new_sled_id)).await { + // Pass `new_sled_id` explicitly because the in-memory member + // struct still has the old sled_id. + match self + .complete_instance_member_join(ctx, instance_id, Some(new_sled_id)) + .await + { Ok(joined) => { info!( ctx.opctx.log, @@ -961,10 +932,10 @@ impl MulticastGroupReconciler { let updated = self .datastore - .multicast_group_member_set_state_if_current( + .multicast_group_member_set_state_if_current_for_parent( ctx.opctx, MulticastGroupUuid::from_untyped_uuid(ctx.group.id()), - InstanceUuid::from_untyped_uuid(ctx.member.parent_id), + MemberParentRef::Instance(instance_id), MulticastGroupMemberState::Joined, MulticastGroupMemberState::Joining, ) @@ -994,6 +965,7 @@ impl MulticastGroupReconciler { async fn handle_joined_without_sled( &self, ctx: &MemberReconcileCtx<'_>, + instance_id: InstanceUuid, ) -> Result { let MemberReconcileCtx { opctx, group, member, .. } = ctx; warn!( @@ -1016,10 +988,10 @@ impl MulticastGroupReconciler { let updated = self .datastore - .multicast_group_member_set_state_if_current( + .multicast_group_member_set_state_if_current_for_parent( opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(member.parent_id), + MemberParentRef::Instance(instance_id), MulticastGroupMemberState::Joined, MulticastGroupMemberState::Left, ) @@ -1056,8 +1028,9 @@ impl MulticastGroupReconciler { async fn handle_instance_left( &self, ctx: &MemberReconcileCtx<'_>, + instance_id: InstanceUuid, ) -> Result { - let InstanceMulticastState { + let ParentRuntimeState { valid: instance_valid, sled_id: current_sled_id, .. @@ -1112,17 +1085,149 @@ impl MulticastGroupReconciler { } if instance_valid && ctx.group.state == MulticastGroupState::Active { - return self.reactivate_left_member(ctx, current_sled_id).await; + return self + .reactivate_left_member(ctx, instance_id, current_sled_id) + .await; + } + + Ok(StateTransition::NoChange) + } + + /// Probe-side "Joining" handler. + /// + /// Drives the polymorphic `_for_parent` reconcile-CAS with the probe's + /// hosting sled read directly from `probe.sled`. The probe's OPTE + /// multicast subscription is set up at zone provisioning, so no + /// sled-agent RPC. + async fn handle_probe_joining( + &self, + ctx: &MemberReconcileCtx<'_>, + probe_id: ProbeUuid, + ) -> Result { + let probe_state = self.lookup_probe_state(ctx.opctx, probe_id).await?; + + let reconcile_res = self + .datastore + .multicast_group_member_reconcile_joining_for_parent( + ctx.opctx, + MulticastGroupUuid::from_untyped_uuid(ctx.group.id()), + MemberParentRef::Probe(probe_id), + probe_state.valid, + probe_state.sled_id.map(Into::into), + ) + .await + .context("failed to reconcile probe member in 'Joining' state")?; + + match reconcile_res.action { + ReconcileAction::TransitionedToLeft => { + info!( + ctx.opctx.log, + "multicast probe member 'Joining' → 'Left'"; + "member_id" => %ctx.member.id, + "probe_id" => %ctx.member.parent_id, + "group_id" => %ctx.group.id(), + "reason" => "probe_not_valid_for_multicast", + ); + Ok(StateTransition::StateChanged) + } + ReconcileAction::UpdatedSledId { .. } + | ReconcileAction::NotFound + | ReconcileAction::NoChange => { + if ctx.group.state == MulticastGroupState::Active + && probe_state.valid + { + let _ = self + .datastore + .multicast_group_member_set_state_if_current_for_parent( + ctx.opctx, + MulticastGroupUuid::from_untyped_uuid( + ctx.group.id(), + ), + MemberParentRef::Probe(probe_id), + MulticastGroupMemberState::Joining, + MulticastGroupMemberState::Joined, + ) + .await?; + Ok(StateTransition::StateChanged) + } else { + Ok(StateTransition::NoChange) + } + } } + } + /// Probe-side "Joined" handler. + /// + /// Missing or invalid probe transitions the member to "Left". + /// Otherwise, noop (steady state). + async fn handle_probe_joined( + &self, + ctx: &MemberReconcileCtx<'_>, + probe_id: ProbeUuid, + ) -> Result { + let probe_state = self.lookup_probe_state(ctx.opctx, probe_id).await?; + + if !probe_state.valid { + self.datastore + .multicast_group_member_set_state_if_current_for_parent( + ctx.opctx, + MulticastGroupUuid::from_untyped_uuid(ctx.group.id()), + MemberParentRef::Probe(probe_id), + MulticastGroupMemberState::Joined, + MulticastGroupMemberState::Left, + ) + .await?; + return Ok(StateTransition::StateChanged); + } Ok(StateTransition::NoChange) } + /// Probe-specific handler for members in "Left" state. + /// + /// If the membership is soft-deleted, finish cleanup. Otherwise, + /// noop, as probes do not reactivate (membership is set at + /// probe-create time only). + async fn handle_probe_left( + &self, + ctx: &MemberReconcileCtx<'_>, + ) -> Result { + if ctx.member.time_deleted.is_some() { + self.cleanup_deleted_member(ctx).await?; + return Ok(StateTransition::NeedsCleanup); + } + // Best-effort DPD cleanup, and idempotent on missing entries. + if let Err(e) = self.remove_member_from_dataplane(ctx).await { + warn!( + ctx.opctx.log, + "failed to clean up DPD state for 'Left' probe member"; + "member_id" => %ctx.member.id, + "probe_id" => %ctx.member.parent_id, + "error" => ?e, + ); + } + Ok(StateTransition::NoChange) + } + + /// Per-row probe lookup state. Probes are rare, so there is no pre-pass + /// cache. The `valid` field reflects an active (not soft-deleted) probe row. + async fn lookup_probe_state( + &self, + opctx: &OpContext, + probe_id: ProbeUuid, + ) -> Result { + let sled_id = self + .datastore + .probe_get_sled_for_multicast(opctx, probe_id) + .await?; + Ok(ParentRuntimeState { valid: sled_id.is_some(), sled_id }) + } + /// Reactivate a member in "Left" state when instance becomes valid again. /// Transitions the member back to "Joining" state so it can rejoin the group. async fn reactivate_left_member( &self, ctx: &MemberReconcileCtx<'_>, + instance_id: InstanceUuid, current_sled_id: Option, ) -> Result { let MemberReconcileCtx { opctx, group, member, .. } = ctx; @@ -1137,10 +1242,10 @@ impl MulticastGroupReconciler { let updated = if let Some(sled_id) = current_sled_id { self.datastore - .multicast_group_member_left_to_joining_if_current( + .multicast_group_member_left_to_joining_if_current_for_parent( opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(member.parent_id), + MemberParentRef::Instance(instance_id), sled_id.into(), ) .await @@ -1149,10 +1254,10 @@ impl MulticastGroupReconciler { )? } else { self.datastore - .multicast_group_member_set_state_if_current( + .multicast_group_member_set_state_if_current_for_parent( opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(member.parent_id), + MemberParentRef::Instance(instance_id), MulticastGroupMemberState::Left, MulticastGroupMemberState::Joining, ) @@ -1182,12 +1287,8 @@ impl MulticastGroupReconciler { Ok(StateTransition::StateChanged) } - /// Batch-fetch instance states for multiple members to avoid N+1 queries. - /// Returns a map of instance_id -> (is_valid_for_multicast, current_sled_id). - /// - /// - Batch-fetching all instance records in one query via the datastore - /// - Batch-fetching all VMM records in one query via the datastore - /// - Building the result map from the fetched data + /// Batch-fetch instance states for multiple members to avoid N+1 + /// queries. Returns an [`InstanceStateMap`] keyed by `instance_id`. async fn batch_fetch_instance_states( &self, opctx: &OpContext, @@ -1199,11 +1300,11 @@ impl MulticastGroupReconciler { return Ok(state_map); } - // Extract unique instance IDs - let instance_ids: Vec = members - .iter() - .map(|m| InstanceUuid::from_untyped_uuid(m.parent_id)) - .collect(); + // Extract unique instance IDs. This drops any non-instance parents + // silently, while callers are expected to feed instance-parented + // members in. + let instance_ids: Vec = + members.iter().filter_map(|m| m.instance_id()).collect(); // Use datastore method to batch-fetch instance and VMM data let instance_vmm_data = self @@ -1231,9 +1332,9 @@ impl MulticastGroupReconciler { SledUuid::from_untyped_uuid(vmm.sled_id.into_untyped_uuid()) }); - InstanceMulticastState { valid, sled_id } + ParentRuntimeState { valid, sled_id } } else { - InstanceMulticastState::default() + ParentRuntimeState::default() }; (member.parent_id, state) @@ -1256,6 +1357,7 @@ impl MulticastGroupReconciler { async fn lookup_and_update_member_sled_id( &self, ctx: &MemberReconcileCtx<'_>, + instance_id: InstanceUuid, ) -> Result>, anyhow::Error> { let MemberReconcileCtx { opctx, member, .. } = ctx; debug!( @@ -1264,8 +1366,6 @@ impl MulticastGroupReconciler { "member" => ?member ); - let instance_id = InstanceUuid::from_untyped_uuid(member.parent_id); - // Try to get instance state let instance_state = match self .datastore @@ -1325,7 +1425,7 @@ impl MulticastGroupReconciler { self.datastore .multicast_group_member_update_sled_id( opctx, - InstanceUuid::from_untyped_uuid(member.parent_id), + instance_id, Some(sled_id.into()), ) .await @@ -1344,19 +1444,14 @@ impl MulticastGroupReconciler { } } - /// Complete a member join by configuring the dataplane and subscribing - /// the VMM. - /// - /// When `sled_id_override` is provided (e.g., during migration), it - /// is used instead of the potentially stale `member.sled_id`. - /// - /// # Returns - /// - /// `Ok(true)` when the join completed successfully. `Ok(false)` when no - /// sled was available and the operation was a noop. + /// Configure the dataplane and subscribe the VMM. `sled_id_override` + /// takes precedence over the potentially stale `member.sled_id` (used + /// during migration). Returns `Ok(false)` when no sled is available + /// (noop). async fn complete_instance_member_join( &self, ctx: &MemberReconcileCtx<'_>, + instance_id: InstanceUuid, sled_id_override: Option, ) -> Result { debug!( @@ -1373,7 +1468,7 @@ impl MulticastGroupReconciler { { id } else if let Some(id) = - self.lookup_and_update_member_sled_id(ctx).await? + self.lookup_and_update_member_sled_id(ctx, instance_id).await? { id.into() } else { @@ -1389,10 +1484,10 @@ impl MulticastGroupReconciler { if ctx.member.state != MulticastGroupMemberState::Joined { let updated = self .datastore - .multicast_group_member_set_state_if_current( + .multicast_group_member_set_state_if_current_for_parent( ctx.opctx, MulticastGroupUuid::from_untyped_uuid(ctx.group.id()), - InstanceUuid::from_untyped_uuid(ctx.member.parent_id), + MemberParentRef::Instance(instance_id), MulticastGroupMemberState::Joining, MulticastGroupMemberState::Joined, ) @@ -1414,18 +1509,13 @@ impl MulticastGroupReconciler { } } - // Propagate M2P mappings and forwarding entries to all sleds. - // - // At this point, the member is now "Joined" in the database, so propagate - // includes this sled in forwarding next-hops. If propagation or - // subscribe fails below, the member remains "Joined" with incomplete - // sled state. The reconciler's next pass converges via - // `handle_instance_joined` -> `verify_members`. - // - // Propagation failures are best-effort since the reconciler will - // re-converge all sleds on the next cycle. Subscribe failures - // below are treated as hard errors because the VMM cannot - // receive traffic without an OPTE port subscription. + // Propagate M2P mappings and forwarding entries to all sleds. The + // member is already "Joined" in the database, so propagation + // includes this sled in forwarding next-hops. Propagation failure + // is best-effort, since the next reconciler pass re-converges via + // `verify_members`. The subscribe call below is a hard error + // because the VMM cannot receive traffic without the OPTE port + // subscription. if let Err(e) = ctx .sled_client .propagate_m2p_and_forwarding(ctx.opctx, ctx.group) @@ -1544,11 +1634,8 @@ impl MulticastGroupReconciler { /// Multicast underlay membership is keyed by (port, link), not by /// member: the DPD member table tracks one entry per /// (group, port_id, link_id), so multiple members sharing a rear - /// port collapse to one entry per group. - /// - /// Compute the union of active rear ports across other "Joined" members - /// in the group and skip any port still in use, so that removing one - /// member does not tear down forwarding for siblings on the same sled. + /// port collapse to one entry per group. Removal must skip any port + /// still claimed by another "Joined" member in the group. async fn remove_from_known_ports( &self, ctx: &MemberReconcileCtx<'_>, @@ -1950,6 +2037,7 @@ impl MulticastGroupReconciler { async fn verify_members( &self, ctx: &MemberReconcileCtx<'_>, + instance_id: InstanceUuid, ) -> Result<(), anyhow::Error> { let MemberReconcileCtx { opctx, @@ -2012,10 +2100,10 @@ impl MulticastGroupReconciler { // Best effort removal on verification let _ = self.remove_member_from_dataplane(ctx).await; - // Unsubscribe the instance before the CAS clears sled_id; - // otherwise, the OPTE subscription is stranded with no - // way to identify the sled on later passes. Best-effort - // since the VMM may already be torn down. + // Unsubscribe the instance before the CAS clears sled_id, + // otherwise the OPTE subscription is stranded with no way + // to identify the sled on later passes. Best-effort, since + // the VMM may already be torn down. if let Err(e) = sled_client .unsubscribe_instance(opctx, group, member, sled_id.into()) .await @@ -2031,10 +2119,10 @@ impl MulticastGroupReconciler { let updated = self .datastore - .multicast_group_member_to_left_if_current( + .multicast_group_member_to_left_if_current_for_parent( opctx, MulticastGroupUuid::from_untyped_uuid(group.id()), - InstanceUuid::from_untyped_uuid(member.parent_id), + MemberParentRef::Instance(instance_id), MulticastGroupMemberState::Joined, ) .await @@ -2213,10 +2301,10 @@ impl MulticastGroupReconciler { } } - // Ensure the instance subscription is in place. Sled-agent resolves - // the active VMM under its per-instance state lock, which keeps this - // call correct across live-migration propolis_id changes when the - // sled_id stays the same. The call is idempotent. + // Ensure the instance subscription is in place ~ idempotent. + // Sled-agent resolves the active VMM under its per-instance lock, + // so this is safe across live-migration `propolis_id` changes + // when `sled_id` stays the same. if let Err(e) = sled_client .subscribe_instance(opctx, group, member, sled_id.into()) .await @@ -2274,9 +2362,9 @@ impl MulticastGroupReconciler { /// are removed. This function checks "Active" groups for any that have no /// active members and marks them for deletion. /// - /// This handles the case where instance deletion causes members to be - /// soft-deleted (via `multicast_group_members_mark_for_removal`), and after - /// the member cleanup removes those records, the group becomes empty. + /// Triggered after parent (instance or probe) deletion drains the + /// membership: members are soft-deleted, cleanup removes them, and + /// the group ends up empty. /// /// The underlying datastore method uses an atomic NOT EXISTS guard to /// prevent race conditions where a concurrent join could create a member @@ -2416,11 +2504,9 @@ impl MulticastGroupReconciler { dataplane_client: &MulticastDataplaneClient, switch_zone_client: Option<&MulticastSwitchZoneClient>, ) -> Result { - // Fetch DPD's backplane map once per reconciliation pass. It accounts - // for the enumeration of valid PortId values (regardless of how - // a peer's `if_name` ~ interface name ~ is shaped), so we use it to - // cross-validate parsed DDM peers and to ground the inventory - // fallback's slot lookups. + // Fetch DPD's backplane map once per pass. Used to cross-validate + // parsed DDM `if_name` entries and to ground the inventory + // fallback's `sp_slot` lookups. let backplane_map = self.fetch_backplane_map(opctx, dataplane_client).await?; @@ -2435,10 +2521,10 @@ impl MulticastGroupReconciler { // Prefer DDM: it reflects live peer status (link state, cable // up/down). Inventory is a periodic collection snapshot and can // lag actual topology. DDM may also be partial (a flapping link - // can drop a sled out of peers temporarily, or test/sim - // populates DDM from an earlier inventory snapshot); when it - // is, fill gaps from inventory rather than treat the partial - // result as authoritative. + // can drop a sled out of peers temporarily, or test/sim populates + // DDM from an earlier inventory snapshot). When it is partial, + // fill gaps from inventory rather than treat the partial result + // as authoritative. let mut mappings = match switch_zone_client { Some(switch_zone_client) => self .fetch_sled_mapping_from_ddm( @@ -2481,14 +2567,11 @@ impl MulticastGroupReconciler { .await { Ok(inventory_map) => { - // Surface inventory-vs-DDM drift signals before - // merging. (a) DDM-only: DDM lists a sled missing - // from the latest inventory collection, typical - // when inventory hasn't caught up to a - // freshly-attached sled. (b) Disagreement: both - // have the sled but with different port info; DDM - // wins (live state), but the inventory lag is - // worth flagging. + // Surface inventory-vs-DDM drift before merging. + // DDM-only sleds usually mean inventory hasn't caught + // up to a freshly-attached sled. Port-info + // disagreement is resolved in DDM's favor (live + // state), but the inventory lag is worth flagging. // // TODO: surface this drift as an observability // signal rather than reconciliation pass logs. @@ -2666,11 +2749,11 @@ impl MulticastGroupReconciler { /// Joins active DDM peers (by IPv6 address) against the in-service /// sled list and parses each peer's `tfport_` /// interface name into a [`SwitchBackplanePort`]. Any DPD port - /// variant (rear, qsfp, ...) is supported. Direction is derived - /// from the port kind. Parsed `PortId`s are cross-validated against - /// the DPD backplane map: peers whose port is unknown to DPD are - /// dropped, so the prefix shape (`tfport`) is just a tokenizer and - /// correctness rides on DPD's authoritative port enumeration. + /// variant (rear, qsfp, ...) is supported. Direction is derived from + /// the port kind. Parsed `PortId`s are cross-validated against the + /// DPD backplane map: peers whose port is unknown to DPD are dropped. + /// The `tfport` prefix is just a tokenizer. DPD is authoritative for + /// which ports actually exist. async fn fetch_sled_mapping_from_ddm( &self, opctx: &OpContext, diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 1bae9d66c44..3c3df53e0c9 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -29,6 +29,7 @@ use nexus_db_model::InstanceIntendedState as IntendedState; use nexus_db_model::InstanceUpdate; use nexus_db_model::IpAttachState; use nexus_db_model::IpKind; +use nexus_db_model::MemberParentRef; use nexus_db_model::Vmm as DbVmm; use nexus_db_model::VmmState as DbVmmState; use nexus_db_queries::authn; @@ -420,9 +421,11 @@ impl super::Nexus { // Get current multicast group memberships (active-only) let current_memberships = self .datastore() - .multicast_group_members_list_by_instance( + .multicast_group_members_list_by_parent( opctx, - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), &DataPageParams::max_page(), ) .await?; @@ -517,10 +520,12 @@ impl super::Nexus { "group_id" => %group_id ); self.datastore() - .multicast_group_member_detach_by_group_and_instance( + .multicast_group_member_detach_by_group_and_parent( opctx, MulticastGroupUuid::from_untyped_uuid(group_id), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), ) .await?; } @@ -541,10 +546,12 @@ impl super::Nexus { "source_ips" => ?source_ips ); self.datastore() - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( opctx, MulticastGroupUuid::from_untyped_uuid(group_id), - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), source_ips.as_deref(), ) .await?; @@ -1140,9 +1147,11 @@ impl super::Nexus { // that is still running if the request fails. if self.multicast_enabled() { self.db_datastore - .multicast_group_members_detach_by_instance( + .multicast_group_members_detach_by_parent( opctx, - InstanceUuid::from_untyped_uuid(authz_instance.id()), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + authz_instance.id(), + )), ) .await?; } @@ -1593,9 +1602,11 @@ impl super::Nexus { if self.multicast_enabled() { let multicast_members = self .db_datastore - .multicast_group_members_list_by_instance( + .multicast_group_members_list_by_parent( opctx, - InstanceUuid::from_untyped_uuid(authz_instance.id()), + MemberParentRef::Instance( + InstanceUuid::from_untyped_uuid(authz_instance.id()), + ), &DataPageParams::max_page(), ) .await diff --git a/nexus/src/app/multicast/mod.rs b/nexus/src/app/multicast/mod.rs index b95eadcaba8..7c62284ac78 100644 --- a/nexus/src/app/multicast/mod.rs +++ b/nexus/src/app/multicast/mod.rs @@ -55,7 +55,7 @@ use slog::{debug, error}; use super::MAX_MULTICAST_GROUPS_PER_INSTANCE; use nexus_db_lookup::{LookupPath, lookup}; -use nexus_db_model::Name; +use nexus_db_model::{MemberParentRef, Name}; use nexus_db_queries::context::OpContext; use nexus_db_queries::db::datastore::multicast::ExternalMulticastGroupWithSources; use nexus_db_queries::{authz, db}; @@ -379,9 +379,9 @@ impl super::Nexus { // to revisit doing so depending on customer need. let current_members = self .db_datastore - .multicast_group_members_list_by_instance( + .multicast_group_members_list_by_parent( opctx, - instance_id, + MemberParentRef::Instance(instance_id), &DataPageParams::max_page(), ) .await?; @@ -420,7 +420,7 @@ impl super::Nexus { // Preflight per-group source IP union cap for a descriptive 400 in // the non-racing common case. The datastore CTE enforces the same - // bound atomically inside `multicast_group_member_attach_to_instance`. + // bound atomically inside `multicast_group_member_attach`. if let Some(sources) = source_ips.filter(|s| !s.is_empty()) { self.validate_group_source_union(opctx, group_id, sources).await?; } @@ -428,10 +428,10 @@ impl super::Nexus { // Attach the member with its source IPs let member = self .db_datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( opctx, group_id, - instance_id, + MemberParentRef::Instance(instance_id), source_ips, ) .await?; @@ -791,10 +791,12 @@ impl super::Nexus { // Idempotent: if member doesn't exist, return success let member = match self .db_datastore - .multicast_group_member_get_by_group_and_instance( + .multicast_group_member_get_by_group_and_parent( opctx, MulticastGroupUuid::from_untyped_uuid(authz_group.id()), - InstanceUuid::from_untyped_uuid(authz_instance.id()), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + authz_instance.id(), + )), ) .await? { @@ -879,9 +881,11 @@ impl super::Nexus { let (.., authz_instance) = instance_lookup.lookup_for(authz::Action::Read).await?; self.db_datastore - .multicast_group_members_list_by_instance( + .multicast_group_members_list_by_parent( opctx, - InstanceUuid::from_untyped_uuid(authz_instance.id()), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + authz_instance.id(), + )), pagparams, ) .await diff --git a/nexus/src/app/probe.rs b/nexus/src/app/probe.rs index c2c4a94fad2..cfbedaf36a3 100644 --- a/nexus/src/app/probe.rs +++ b/nexus/src/app/probe.rs @@ -2,6 +2,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +use std::collections::HashSet; + use nexus_db_lookup::lookup; use nexus_db_model::Probe; use nexus_db_queries::authz; @@ -14,6 +16,9 @@ use omicron_common::api::external::{ CreateResult, DeleteResult, ListResultVec, LookupResult, NameOrId, http_pagination::PaginatedBy, }; +use omicron_uuid_kinds::{GenericUuid, ProbeUuid}; + +use super::MAX_MULTICAST_GROUPS_PER_INSTANCE; impl super::Nexus { /// List the probes in the given project. @@ -85,6 +90,65 @@ impl super::Nexus { ) .await?; + // Mirrors the validation in `Nexus::handle_multicast_group_changes` + // (instance update path) and the cap check in + // `Nexus::project_create_instance`. Probes have no + // membership-mutation API, so the create path is the only window + // to reject duplicates or exceed the per-parent cap. + if self.multicast_enabled() + && !new_probe_params.multicast_groups.is_empty() + { + if new_probe_params.multicast_groups.len() + > MAX_MULTICAST_GROUPS_PER_INSTANCE + { + return Err(Error::invalid_request(format!( + "A probe may not join more than \ + {MAX_MULTICAST_GROUPS_PER_INSTANCE} multicast groups", + ))); + } + + let probe_id = ProbeUuid::from_untyped_uuid(probe.id()); + let mut to_attach = + Vec::with_capacity(new_probe_params.multicast_groups.len()); + let mut seen = + HashSet::with_capacity(new_probe_params.multicast_groups.len()); + for spec in &new_probe_params.multicast_groups { + let source_ips = spec.source_ips.as_deref(); + let group_id = self + .resolve_multicast_group_identifier_with_sources( + opctx, + &spec.group, + source_ips, + spec.ip_version, + ) + .await + .map_err(|e| { + Error::invalid_request(format!( + "failed to resolve multicast group {:?}: {e}", + spec.group, + )) + })?; + if !seen.insert(group_id.into_untyped_uuid()) { + return Err(Error::invalid_request( + "Duplicate multicast group specified in request", + )); + } + to_attach.push((group_id, source_ips)); + } + + for (group_id, source_ips) in to_attach { + self.db_datastore + .multicast_group_member_attach( + opctx, + group_id, + nexus_db_model::MemberParentRef::Probe(probe_id), + source_ips, + ) + .await?; + } + self.background_tasks.task_multicast_reconciler.activate(); + } + let (.., sled) = self.sled_lookup(opctx, &new_probe_params.sled)?.fetch().await?; @@ -129,6 +193,21 @@ impl super::Nexus { name_or_id: NameOrId, ) -> DeleteResult { let probe = self.probe_get(opctx, project_lookup, &name_or_id).await?; + // Mark memberships for permanent removal (sets `time_deleted`) + // before soft-deleting the probe, mirroring the `instance_delete` + // saga. The probe-"Left" reconciler finalizes dataplane teardown + // on rows with `time_deleted` set. + if self.multicast_enabled() { + self.db_datastore + .multicast_group_members_mark_for_removal_by_parent( + opctx, + nexus_db_model::MemberParentRef::Probe( + ProbeUuid::from_untyped_uuid(probe.id), + ), + ) + .await?; + self.background_tasks.task_multicast_reconciler.activate(); + } self.probe_delete_dpd_config(opctx, probe.id).await?; let (.., authz_project) = project_lookup.lookup_for(authz::Action::CreateChild).await?; diff --git a/nexus/src/app/sagas/instance_create.rs b/nexus/src/app/sagas/instance_create.rs index afe911c95ca..b1b47c18f45 100644 --- a/nexus/src/app/sagas/instance_create.rs +++ b/nexus/src/app/sagas/instance_create.rs @@ -11,6 +11,7 @@ use crate::app::MAX_DISKS_PER_INSTANCE; use crate::app::sagas::declare_saga_actions; use crate::app::sagas::disk_create::{self, SagaDiskCreate}; use nexus_db_lookup::LookupPath; +use nexus_db_model::MemberParentRef; use nexus_db_model::NetworkInterfaceKind; use nexus_db_model::{ExternalIp, IpVersion}; use nexus_db_queries::db::queries::network_interface::InsertError as InsertNicError; @@ -1105,16 +1106,16 @@ async fn sic_join_instance_multicast_group( // Add the instance as a member of the multicast group in "Joining" state. // - // We use `multicast_group_member_attach_to_instance` (same as explicit join API) which + // We use `multicast_group_member_attach` (same as explicit join API) which // doesn't require the group to be in "Active" state. This supports // auto-created groups (which start in "Creating" state) // // The RPW reconciler handles transitioning both group and member to active states. if let Err(e) = datastore - .multicast_group_member_attach_to_instance( + .multicast_group_member_attach( &opctx, group_id, - instance_id, + MemberParentRef::Instance(instance_id), join_spec.source_ips.as_deref(), ) .await @@ -1180,10 +1181,10 @@ async fn sic_join_instance_multicast_group_undo( // This ensures saga undo doesn't affect other instances that may have // independently joined the same group. datastore - .multicast_group_member_delete_by_group_and_instance( + .multicast_group_member_delete_by_group_and_parent( &opctx, MulticastGroupUuid::from_untyped_uuid(db_group.id()), - instance_id, + MemberParentRef::Instance(instance_id), ) .await?; diff --git a/nexus/src/app/sagas/instance_delete.rs b/nexus/src/app/sagas/instance_delete.rs index 86463c55537..c7539c2676d 100644 --- a/nexus/src/app/sagas/instance_delete.rs +++ b/nexus/src/app/sagas/instance_delete.rs @@ -166,9 +166,11 @@ async fn sid_leave_multicast_groups( // Mark all multicast group memberships for this instance as deleted datastore - .multicast_group_members_mark_for_removal( + .multicast_group_members_mark_for_removal_by_parent( &opctx, - InstanceUuid::from_untyped_uuid(instance_id), + nexus_db_model::MemberParentRef::Instance( + InstanceUuid::from_untyped_uuid(instance_id), + ), ) .await .map_err(saga_action_failed)?; diff --git a/nexus/src/app/sagas/instance_update/mod.rs b/nexus/src/app/sagas/instance_update/mod.rs index 4660c88b419..4bd6ae135fc 100644 --- a/nexus/src/app/sagas/instance_update/mod.rs +++ b/nexus/src/app/sagas/instance_update/mod.rs @@ -352,6 +352,7 @@ use crate::app::db::model::Generation; use crate::app::db::model::InstanceIntendedState; use crate::app::db::model::InstanceRuntimeState; use crate::app::db::model::InstanceState; +use crate::app::db::model::MemberParentRef; use crate::app::db::model::MigrationState; use crate::app::db::model::Vmm; use crate::app::db::model::VmmState; @@ -1276,9 +1277,11 @@ async fn siu_commit_instance_updates( if update.deprovision.is_some() && nexus.multicast_enabled() { if let Err(e) = osagactx .datastore() - .multicast_group_members_detach_by_instance( + .multicast_group_members_detach_by_parent( &opctx, - InstanceUuid::from_untyped_uuid(instance_id), + MemberParentRef::Instance(InstanceUuid::from_untyped_uuid( + instance_id, + )), ) .await { diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index bdf802b6ecc..a523a1278d2 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -5290,10 +5290,35 @@ impl NexusExternalApi for NexusExternalApiImpl { .await } - // Pre-MULTICAST_SOURCE_LIMITS version: same types as the latest variant - // (re-exported through `latest::`), so delegate directly. The behavioral - // difference (per-member and per-group source IP caps) is enforced - // unconditionally in the Nexus app layer. + // Pre-PROBE_MULTICAST version: body and path types are unchanged, so + // delegate to the latest and downgrade the response via the version- + // chain `TryFrom`, which fails 406 if the row is probe-parented. + async fn instance_multicast_group_join_v2026_05_22_00( + rqctx: RequestContext, + path_params: Path< + v2026_01_08_00::multicast::InstanceMulticastGroupPath, + >, + query_params: Query, + body_params: TypedBody< + v2026_01_08_00::multicast::InstanceMulticastGroupJoin, + >, + ) -> Result< + HttpResponseCreated, + HttpError, + > { + let HttpResponseCreated(latest) = Self::instance_multicast_group_join( + rqctx, + path_params, + query_params, + body_params, + ) + .await?; + let member: v2026_01_08_00::multicast::MulticastGroupMember = + latest.try_into()?; + Ok(HttpResponseCreated(member)) + } + + // Same shape as the v2026_05_22_00 variant. Delegate. async fn instance_multicast_group_join_v2026_01_08_00( rqctx: RequestContext, path_params: Path< @@ -5307,7 +5332,7 @@ impl NexusExternalApi for NexusExternalApiImpl { HttpResponseCreated, HttpError, > { - Self::instance_multicast_group_join( + Self::instance_multicast_group_join_v2026_05_22_00( rqctx, path_params, query_params, @@ -5358,7 +5383,9 @@ impl NexusExternalApi for NexusExternalApiImpl { None, // Old API version doesn't support ip_version ) .await?; - let member = multicast::MulticastGroupMember::try_from(result)?; + let latest = multicast::MulticastGroupMember::try_from(result)?; + let member: v2026_01_08_00::multicast::MulticastGroupMember = + latest.try_into()?; Ok(HttpResponseCreated(member.into())) }; apictx diff --git a/nexus/tests/integration_tests/multicast/api.rs b/nexus/tests/integration_tests/multicast/api.rs index 7c5a4193cf6..286b77f50d6 100644 --- a/nexus/tests/integration_tests/multicast/api.rs +++ b/nexus/tests/integration_tests/multicast/api.rs @@ -217,7 +217,7 @@ async fn test_multicast_api_behavior(cptestctx: &ControlPlaneTestContext) { ) .await; - assert_eq!(member_uuid.instance_id, instance_uuid); + assert_eq!(member_uuid.parent_id, instance_uuid); // Instance is stopped (start: false), so reconciler transitions "Joining"→"Left" wait_for_member_state( cptestctx, @@ -246,7 +246,7 @@ async fn test_multicast_api_behavior(cptestctx: &ControlPlaneTestContext) { 1, "UUID-based list should show 1 membership" ); - assert_eq!(uuid_memberships[0].instance_id, instance_uuid); + assert_eq!(uuid_memberships[0].parent_id, instance_uuid); // Verify UUID-based group member listing let group_members_url_uuid = @@ -284,9 +284,7 @@ async fn test_multicast_api_behavior(cptestctx: &ControlPlaneTestContext) { let final_members_after_leave = list_multicast_group_members(client, group_name).await; assert!( - !final_members_after_leave - .iter() - .any(|m| m.instance_id == instance_uuid), + !final_members_after_leave.iter().any(|m| m.parent_id == instance_uuid), "instance3 should not be in the group after UUID-based leave" ); diff --git a/nexus/tests/integration_tests/multicast/authorization.rs b/nexus/tests/integration_tests/multicast/authorization.rs index 88550abab40..d288af05b39 100644 --- a/nexus/tests/integration_tests/multicast/authorization.rs +++ b/nexus/tests/integration_tests/multicast/authorization.rs @@ -548,7 +548,7 @@ async fn test_silo_user_multicast_permissions( list_multicast_group_members(client, &group.identity.name.to_string()) .await; assert!( - members.iter().any(|m| m.instance_id == instance2.identity.id), + members.iter().any(|m| m.parent_id == instance2.identity.id), "Instance2 should be a member of the group" ); } @@ -814,7 +814,7 @@ async fn test_unprivileged_users_can_list_group_members( // when operating on someone else's instance // Use the instance ID from the member list - let instance_id = privileged_members[0].instance_id; + let instance_id = privileged_members[0].parent_id; // Attempt JOIN via instance-centric path (by ID) as unprivileged user let group_id = group.identity.id; @@ -1229,7 +1229,7 @@ async fn test_cross_silo_multicast_isolation( assert_eq!(members.len(), 2, "Should have 2 members (one from each silo)"); // Verify both instances are in the member list - let instance_ids: Vec<_> = members.iter().map(|m| m.instance_id).collect(); + let instance_ids: Vec<_> = members.iter().map(|m| m.parent_id).collect(); assert!( instance_ids.contains(&instance_a.identity.id), "Instance from Silo A should be in member list" @@ -1312,7 +1312,7 @@ async fn test_cross_silo_multicast_isolation( "Should have 1 member after Silo B's instance detached" ); assert_eq!( - members_after_detach.items[0].instance_id, instance_a.identity.id, + members_after_detach.items[0].parent_id, instance_a.identity.id, "Remaining member should be Silo A's instance" ); @@ -1584,7 +1584,7 @@ async fn test_both_member_endpoints_have_same_permissions( // Verify the member was created let members = list_multicast_group_members(client, group_name).await; assert!( - members.iter().any(|m| m.instance_id == instance_b.identity.id), + members.iter().any(|m| m.parent_id == instance_b.identity.id), "Instance B should be a member of the group" ); diff --git a/nexus/tests/integration_tests/multicast/failures.rs b/nexus/tests/integration_tests/multicast/failures.rs index 9fc826f09a8..80bd9f905d5 100644 --- a/nexus/tests/integration_tests/multicast/failures.rs +++ b/nexus/tests/integration_tests/multicast/failures.rs @@ -113,7 +113,7 @@ async fn test_dpd_failure_during_creating_state( wait_for_member_state( cptestctx, group_name, - members[0].instance_id, + members[0].parent_id, nexus_db_model::MulticastGroupMemberState::Joined, ) .await; @@ -367,7 +367,7 @@ async fn test_multicast_group_members_during_dpd_failure( .await .items; assert_eq!(initial_members.len(), 1, "Should have exactly one member"); - assert_eq!(initial_members[0].instance_id, instance.identity.id); + assert_eq!(initial_members[0].parent_id, instance.identity.id); // Wait for reconciler - group should remain in "Creating" state wait_for_multicast_reconciler(&cptestctx.lockstep_client).await; @@ -384,7 +384,7 @@ async fn test_multicast_group_members_during_dpd_failure( 1, "Member should still be accessible during DPD failure" ); - assert_eq!(members_during_failure[0].instance_id, instance.identity.id); + assert_eq!(members_during_failure[0].parent_id, instance.identity.id); assert_eq!( members_during_failure[0].multicast_group_id, created_group.identity.id @@ -570,7 +570,7 @@ async fn test_concurrent_implicit_creation_race( // Verify each instance is a member for instance in &instances { assert!( - final_members.iter().any(|m| m.instance_id == instance.identity.id), + final_members.iter().any(|m| m.parent_id == instance.identity.id), "Instance {} should be a member of the group", instance.identity.name ); @@ -703,7 +703,7 @@ async fn test_implicit_deletion_race_with_instance_join( assert!( !members .iter() - .any(|m| m.instance_id == instances[0].identity.id), + .any(|m| m.parent_id == instances[0].identity.id), "Leaving instance should not be a member" ); } diff --git a/nexus/tests/integration_tests/multicast/groups.rs b/nexus/tests/integration_tests/multicast/groups.rs index 50fefb03ff8..19295cf3696 100644 --- a/nexus/tests/integration_tests/multicast/groups.rs +++ b/nexus/tests/integration_tests/multicast/groups.rs @@ -28,6 +28,7 @@ use http::{Method, StatusCode}; use crate::integration_tests::instances::{ instance_simulate, instance_wait_for_state, }; +use nexus_test_utils::SLED_AGENT_UUID; use nexus_test_utils::dpd_client; use nexus_test_utils::http_testing::{ AuthnMode, Collection, NexusRequest, RequestBuilder, @@ -38,15 +39,19 @@ use nexus_test_utils::resource_helpers::{ object_get, object_get_error, object_put_error, }; use nexus_test_utils_macros::nexus_test; +use nexus_types::external_api::ip_pool::PoolSelector; use nexus_types::external_api::ip_pool::{ IpPool, IpPoolCreate, IpPoolRange, IpRange, IpVersion, Ipv4Range, Ipv6Range, }; use nexus_types::external_api::multicast::{ - InstanceMulticastGroupJoin, MulticastGroup, MulticastGroupMember, + InstanceMulticastGroupJoin, MulticastGroup, MulticastGroupJoinSpec, + MulticastGroupMember, }; +use nexus_types::external_api::probe::ProbeCreate; use omicron_common::api::external::{ IdentityMetadataCreateParams, InstanceState, }; +use omicron_common::api::external::{Name, Probe}; use omicron_uuid_kinds::InstanceUuid; use super::*; @@ -192,7 +197,7 @@ async fn test_multicast_group_member_operations( put_upsert(client, &join_url, &join_params).await; assert_eq!( - added_member.instance_id.to_string(), + added_member.parent_id.to_string(), instance.identity.id.to_string() ); @@ -210,7 +215,7 @@ async fn test_multicast_group_member_operations( // Test listing members (should have 1 now in Joined state) let members = list_multicast_group_members(&client, group_name).await; assert_eq!(members.len(), 1, "Expected exactly 1 member"); - assert_eq!(members[0].instance_id, added_member.instance_id); + assert_eq!(members[0].parent_id, added_member.parent_id); assert_eq!(members[0].multicast_group_id, added_member.multicast_group_id); // Test listing groups (should include our implicitly created group) @@ -384,7 +389,7 @@ async fn test_instance_multicast_endpoints( // Use PUT method and expect 201 Created (implicitly creating group1) let member1: MulticastGroupMember = put_upsert(client, &instance_join_group1_url, &join_params).await; - assert_eq!(member1.instance_id, instance.identity.id); + assert_eq!(member1.parent_id, instance.identity.id); // Wait for group1 to become active after implicitly create wait_for_group_active(client, group1_name).await; @@ -403,7 +408,7 @@ async fn test_instance_multicast_endpoints( let group1_members = list_multicast_group_members(&client, group1_name).await; assert_eq!(group1_members.len(), 1); - assert_eq!(group1_members[0].instance_id, instance.identity.id); + assert_eq!(group1_members[0].parent_id, instance.identity.id); // Check instance-centric view (test the list endpoint thoroughly) let instance_memberships: ResultsPage = @@ -413,7 +418,7 @@ async fn test_instance_multicast_endpoints( 1, "Instance should have exactly 1 membership" ); - assert_eq!(instance_memberships.items[0].instance_id, instance.identity.id); + assert_eq!(instance_memberships.items[0].parent_id, instance.identity.id); assert_eq!( instance_memberships.items[0].multicast_group_id, member1.multicast_group_id @@ -428,7 +433,7 @@ async fn test_instance_multicast_endpoints( InstanceMulticastGroupJoin { source_ips: None, ip_version: None }; let member2: MulticastGroupMember = put_upsert(client, &join_group2_url, &join_params2).await; - assert_eq!(member2.instance_id, instance.identity.id); + assert_eq!(member2.parent_id, instance.identity.id); // Wait for group2 to become active after implicitly create wait_for_group_active(client, group2_name).await; @@ -468,7 +473,7 @@ async fn test_instance_multicast_endpoints( // Verify all memberships show correct instance_id and state for membership in &instance_memberships.items { - assert_eq!(membership.instance_id, instance.identity.id); + assert_eq!(membership.parent_id, instance.identity.id); assert_eq!(membership.state, "Joined"); } @@ -479,8 +484,8 @@ async fn test_instance_multicast_endpoints( list_multicast_group_members(&client, group2_name).await; assert_eq!(group1_members.len(), 1); assert_eq!(group2_members.len(), 1); - assert_eq!(group1_members[0].instance_id, instance.identity.id); - assert_eq!(group2_members[0].instance_id, instance.identity.id); + assert_eq!(group1_members[0].parent_id, instance.identity.id); + assert_eq!(group2_members[0].parent_id, instance.identity.id); // Leave group1 using instance-centric endpoint let instance_leave_group1_url = format!( @@ -649,7 +654,7 @@ async fn test_instance_deletion_removes_multicast_memberships( // Verify member was added let members = list_multicast_group_members(&client, group_name).await; assert_eq!(members.len(), 1, "Instance should be a member of the group"); - assert_eq!(members[0].instance_id, instance.identity.id); + assert_eq!(members[0].parent_id, instance.identity.id); // Verify MRIB route exists while group is active with a joined member. assert_mrib_route_exists(cptestctx, multicast_ip).await; @@ -1736,3 +1741,132 @@ async fn test_multicast_group_ip_version_conflict( wait_for_group_deleted(cptestctx, "conflict-test-group").await; wait_for_group_deleted(cptestctx, &ip_based_group_name).await; } + +/// Drives the probe-multicast end-to-end path for both v4 and v6 in one +/// test: pre-create the multicast group via an instance attach, then +/// create a probe enrolling in the same group at probe-create time, then +/// verify the probe membership row appears in the group's member list. +#[nexus_test] +async fn test_probe_multicast_member(cptestctx: &ControlPlaneTestContext) { + let client = &cptestctx.external_client; + let project_name = "probe-mcast-project"; + create_project(client, project_name).await; + let (v4_default_pool, _v6_default_pool) = + create_default_ip_pools(client).await; + + // v4 setup + create_multicast_ip_pool_with_range( + client, + "probe-mcast-v4-pool", + (224, 50, 0, 10), + (224, 50, 0, 50), + ) + .await; + enroll_probe_in_group( + cptestctx, + project_name, + "probe-mcast-v4-group", + "primer-instance-v4", + "probe-mcaster-v4", + &v4_default_pool, + IpVersion::V4, + ) + .await; + + // v6 setup. Both multicast pools exist at this point, so the + // by-name auto-create needs `ip_version` to disambiguate which + // pool to allocate the group from. + create_multicast_ip_pool_v6(client, "probe-mcast-v6-pool").await; + enroll_probe_in_group( + cptestctx, + project_name, + "probe-mcast-v6-group", + "primer-instance-v6", + "probe-mcaster-v6", + &v4_default_pool, + IpVersion::V6, + ) + .await; +} + +/// Inner step shared by the v4 and v6 setups of +/// [`test_probe_multicast_member`]. Materializes `group_name` by attaching +/// `instance_name`, drives it to "Joined", then creates `probe_name` +/// enrolling in the same group and asserts the probe row appears in the +/// group's member list. `unicast_pool` supplies the probe's external IP, which +/// is separate from the multicast pool. +async fn enroll_probe_in_group( + cptestctx: &ControlPlaneTestContext, + project_name: &str, + group_name: &str, + instance_name: &str, + probe_name: &str, + unicast_pool: &IpPool, + ip_version: IpVersion, +) { + let client = &cptestctx.external_client; + + // Materialize the group via an instance attach. Group creation has no + // direct API and instance join auto-creates. `ip_version` selects the + // multicast pool when multiple are linked. + let instance = create_instance(client, project_name, instance_name).await; + let join_url = format!( + "/v1/instances/{instance_name}/multicast-groups/{group_name}?project={project_name}" + ); + let join_params = InstanceMulticastGroupJoin { + source_ips: None, + ip_version: Some(ip_version), + }; + let _: MulticastGroupMember = + put_upsert(client, &join_url, &join_params).await; + + // Drive instance member to "Joined" so the group is "Active" and the + // probe-side enrollment lands in a steady state. + wait_for_member_state( + cptestctx, + group_name, + instance.identity.id, + nexus_db_model::MulticastGroupMemberState::Joined, + ) + .await; + + // Create a probe enrolling in the same group at create time. Use an + // explicit unicast pool selector since the probe's external IP is + // unicast (the multicast group is separate from that pool). + let probe_body = ProbeCreate { + identity: IdentityMetadataCreateParams { + name: probe_name.parse().unwrap(), + description: "probe-multicast e2e fixture".to_owned(), + }, + sled: SLED_AGENT_UUID.parse().unwrap(), + pool_selector: PoolSelector::Explicit { + pool: unicast_pool.identity.name.clone().into(), + }, + multicast_groups: vec![MulticastGroupJoinSpec { + group: group_name.parse::().unwrap().into(), + source_ips: None, + ip_version: None, + }], + }; + let probe: Probe = NexusRequest::objects_post( + client, + &format!("/experimental/v1/probes?project={project_name}"), + &probe_body, + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap() + .parsed_body() + .unwrap(); + + // Verify the probe appears as a member of the group. The view's + // `parent_id` field carries the parent's UUID regardless of kind. + let members = list_multicast_group_members(client, group_name).await; + let probe_id = probe.identity.id; + let actual: Vec<_> = members.iter().map(|m| m.parent_id).collect(); + assert!( + members.iter().any(|m| m.parent_id == probe_id), + "expected probe {probe_id} in group {group_name} members. Received {actual:?}", + ); +} diff --git a/nexus/tests/integration_tests/multicast/instances.rs b/nexus/tests/integration_tests/multicast/instances.rs index db2e5c0f5ae..7c468b0988d 100644 --- a/nexus/tests/integration_tests/multicast/instances.rs +++ b/nexus/tests/integration_tests/multicast/instances.rs @@ -247,7 +247,7 @@ async fn test_multicast_lifecycle(cptestctx: &ControlPlaneTestContext) { 1, "group-lifecycle-2 should have 1 member after detach" ); - assert_eq!(members[0].instance_id, instances[2].identity.id); + assert_eq!(members[0].parent_id, instances[2].identity.id); // Verify groups are still active and functional for group_name in group_names.iter() { @@ -447,7 +447,7 @@ async fn test_multicast_group_attach_multiple( 1, "Instance should be member of group {group_name}" ); - assert_eq!(members[0].instance_id, instance.identity.id); + assert_eq!(members[0].parent_id, instance.identity.id); } cleanup_instances(cptestctx, client, PROJECT_NAME, &["mcast-instance-1"]) @@ -602,7 +602,7 @@ async fn test_multicast_concurrent_operations( // Wait for all remaining members to reach "Joined" state. let expected: Vec<_> = post_rapid_members .iter() - .map(|m| (m.instance_id, MulticastGroupMemberState::Joined)) + .map(|m| (m.parent_id, MulticastGroupMemberState::Joined)) .collect(); wait_for_members_state(cptestctx, "concurrent-test-group", &expected).await; @@ -1530,7 +1530,7 @@ async fn test_instance_create_with_ssm_multicast_groups( assert_eq!(members.len(), 1, "Should have one member"); let member = &members[0]; - assert_eq!(member.instance_id, instance.identity.id); + assert_eq!(member.parent_id, instance.identity.id); assert_eq!(member.source_ips.len(), 1, "Member should have 1 source IP"); assert_eq!( member.source_ips[0], source_ip, @@ -1703,8 +1703,9 @@ async fn test_ssm_without_sources_fails_create_and_reconfigure( /// Test that instance deletion only removes that instance's membership, /// preserving other instances' memberships in the same group. /// -/// This tests the invariant that `multicast_group_member_delete_by_group_and_instance` -/// filters by both `group_id` and `instance_id`, not just `group_id`. This is +/// This tests the invariant that `multicast_group_member_delete_by_group_and_parent` +/// filters by both `group_id` and the parent (instance/probe), not just +/// `group_id`. This is /// important for saga undo correctness: if instance B's create saga fails after /// joining a group, the undo must not affect instance A's existing membership /// in the same group. @@ -1879,7 +1880,7 @@ async fn test_instance_delete_preserves_other_memberships( list_multicast_group_members(client, group_name).await; assert_eq!(members_after_b.len(), 2, "A and C must survive B's deletion"); let remaining_instance_ids: BTreeSet<_> = - members_after_b.iter().map(|m| m.instance_id).collect(); + members_after_b.iter().map(|m| m.parent_id).collect(); assert!( remaining_instance_ids.contains(&instance_a.identity.id), "A's membership must remain" @@ -1976,7 +1977,7 @@ async fn test_multicast_ipv6_lifecycle(cptestctx: &ControlPlaneTestContext) { ) .await; - assert_eq!(member.instance_id, instance.identity.id); + assert_eq!(member.parent_id, instance.identity.id); // Activate reconciler and wait for group to become Active wait_for_multicast_reconciler(&cptestctx.lockstep_client).await; diff --git a/nexus/tests/integration_tests/multicast/mod.rs b/nexus/tests/integration_tests/multicast/mod.rs index 3fdb96dd56d..23f7de9ba9e 100644 --- a/nexus/tests/integration_tests/multicast/mod.rs +++ b/nexus/tests/integration_tests/multicast/mod.rs @@ -23,6 +23,7 @@ use http::{Method, StatusCode}; use slog::{debug, info, warn}; use uuid::Uuid; +use nexus_db_model::MemberParentRef; use nexus_db_queries::context::OpContext; use nexus_db_queries::db::fixed_data::silo::DEFAULT_SILO; use nexus_test_utils::http_testing::{AuthnMode, NexusRequest, RequestBuilder}; @@ -568,7 +569,7 @@ pub(crate) async fn wait_for_member_state( // `NotYet`, but treat any *other* observed state as a permanent failure. let check_member = || async { let members = list_multicast_group_members(client, group_name).await; - match members.iter().find(|m| m.instance_id == instance_id) { + match members.iter().find(|m| m.parent_id == instance_id) { Some(member) if member.state == expected_state_as_str => { Ok(member.clone()) } @@ -644,7 +645,7 @@ pub(crate) async fn wait_for_members_state( let mut resolved = Vec::with_capacity(expected.len()); for (instance_id, expected_state) in expected { let expected_str = expected_state.to_string(); - match members.iter().find(|m| m.instance_id == *instance_id) { + match members.iter().find(|m| m.parent_id == *instance_id) { Some(member) if member.state == expected_str => { resolved.push(member.clone()); } @@ -936,9 +937,9 @@ pub(crate) async fn verify_inventory_based_port_mapping( // Get the multicast member for this instance to find its external_group_id let members = datastore - .multicast_group_members_list_by_instance( + .multicast_group_members_list_by_parent( &opctx, - *instance_uuid, + MemberParentRef::Instance(*instance_uuid), &DataPageParams::max_page(), ) .await diff --git a/nexus/tests/integration_tests/multicast/networking_integration.rs b/nexus/tests/integration_tests/multicast/networking_integration.rs index 3b7efe9e1d1..71219056b15 100644 --- a/nexus/tests/integration_tests/multicast/networking_integration.rs +++ b/nexus/tests/integration_tests/multicast/networking_integration.rs @@ -163,7 +163,7 @@ async fn test_multicast_external_ip_scenarios( 1, "Multicast member should still exist after external IP allocation" ); - assert_eq!(members_after_ip[0].instance_id, instance_id); + assert_eq!(members_after_ip[0].parent_id, instance_id); assert_eq!( members_after_ip[0].state, "Joined", "Member state should remain Joined" @@ -192,7 +192,7 @@ async fn test_multicast_external_ip_scenarios( 1, "Multicast member should persist after external IP removal" ); - assert_eq!(members_after_detach[0].instance_id, instance_id); + assert_eq!(members_after_detach[0].parent_id, instance_id); assert_eq!( members_after_detach[0].state, "Joined", "Member should remain Joined" @@ -586,7 +586,7 @@ async fn test_multicast_with_floating_ip_basic( 1, "Multicast member should still exist after floating IP attachment" ); - assert_eq!(members_after_ip[0].instance_id, instance_id); + assert_eq!(members_after_ip[0].parent_id, instance_id); assert_eq!( members_after_ip[0].state, "Joined", "Member state should remain Joined" @@ -643,7 +643,7 @@ async fn test_multicast_with_floating_ip_basic( 1, "Multicast member should persist after floating IP detachment" ); - assert_eq!(members_after_detach[0].instance_id, instance_id); + assert_eq!(members_after_detach[0].parent_id, instance_id); assert_eq!( members_after_detach[0].state, "Joined", "Member should remain Joined" diff --git a/nexus/tests/integration_tests/multicast/pool_selection.rs b/nexus/tests/integration_tests/multicast/pool_selection.rs index e9ebde8b924..16dfedcd5b7 100644 --- a/nexus/tests/integration_tests/multicast/pool_selection.rs +++ b/nexus/tests/integration_tests/multicast/pool_selection.rs @@ -84,7 +84,7 @@ async fn test_pool_selection_ssm_asm(cptestctx: &ControlPlaneTestContext) { .await; // Verify member was created - assert_eq!(member1.instance_id, instance.identity.id); + assert_eq!(member1.parent_id, instance.identity.id); // Activate reconciler to process the new group ("Creating" -> "Active") wait_for_multicast_reconciler(&cptestctx.lockstep_client).await; @@ -279,7 +279,7 @@ async fn test_ip_version_disambiguation(cptestctx: &ControlPlaneTestContext) { ) .await; - assert_eq!(v4_member.instance_id, instance.identity.id); + assert_eq!(v4_member.parent_id, instance.identity.id); wait_for_multicast_reconciler(&cptestctx.lockstep_client).await; let v4_group = wait_for_group_active(client, "ipv4-group").await; @@ -316,7 +316,7 @@ async fn test_ip_version_disambiguation(cptestctx: &ControlPlaneTestContext) { ) .await; - assert_eq!(v6_member.instance_id, instance.identity.id); + assert_eq!(v6_member.parent_id, instance.identity.id); wait_for_multicast_reconciler(&cptestctx.lockstep_client).await; let v6_group = wait_for_group_active(client, "ipv6-group").await; diff --git a/nexus/tests/integration_tests/probe.rs b/nexus/tests/integration_tests/probe.rs index 2d4f4fbf09c..f2fc8e6e338 100644 --- a/nexus/tests/integration_tests/probe.rs +++ b/nexus/tests/integration_tests/probe.rs @@ -43,6 +43,7 @@ async fn test_probe_basic_crud(ctx: &ControlPlaneTestContext) { pool: v6_pool.identity.name.clone().into(), }, sled: SLED_AGENT_UUID.parse().unwrap(), + multicast_groups: Vec::new(), }; let created: Probe = NexusRequest::objects_post( @@ -148,6 +149,7 @@ async fn test_probe_pool_selector_ip_version(ctx: &ControlPlaneTestContext) { }, pool_selector: PoolSelector::Auto { ip_version: Some(IpVersion::V6) }, sled: SLED_AGENT_UUID.parse().unwrap(), + multicast_groups: Vec::new(), }; let created_v6: Probe = NexusRequest::objects_post( @@ -194,6 +196,7 @@ async fn test_probe_pool_selector_ip_version(ctx: &ControlPlaneTestContext) { }, pool_selector: PoolSelector::Auto { ip_version: Some(IpVersion::V4) }, sled: SLED_AGENT_UUID.parse().unwrap(), + multicast_groups: Vec::new(), }; let created_v4: Probe = NexusRequest::objects_post( diff --git a/nexus/types/versions/src/latest.rs b/nexus/types/versions/src/latest.rs index 443b5f39d33..e4d992f8544 100644 --- a/nexus/types/versions/src/latest.rs +++ b/nexus/types/versions/src/latest.rs @@ -228,12 +228,14 @@ pub mod multicast { pub use crate::v2026_01_08_00::multicast::InstanceMulticastGroupPath; pub use crate::v2026_01_08_00::multicast::MulticastGroupIdentifier; pub use crate::v2026_01_08_00::multicast::MulticastGroupJoinSpec; - pub use crate::v2026_01_08_00::multicast::MulticastGroupMember; pub use crate::v2026_01_08_00::multicast::MulticastGroupMemberAdd; pub use crate::v2026_01_08_00::multicast::MulticastGroupMemberPath; pub use crate::v2026_01_08_00::multicast::MulticastGroupPath; pub use crate::v2026_01_08_00::multicast::MulticastGroupSelector; + pub use crate::v2026_05_26_00::multicast::MulticastGroupMember; + pub use crate::v2026_05_26_00::multicast::MulticastGroupMemberParentKind; + pub use crate::v2026_03_14_00::multicast::MulticastGroup; pub use crate::v2026_03_14_00::multicast::MulticastGroupCreate; pub use crate::v2026_03_14_00::multicast::MulticastGroupUpdate; @@ -332,7 +334,7 @@ pub mod probe { pub use crate::v2026_01_03_00::probe::ProbeInfo; - pub use crate::v2026_01_05_00::probe::ProbeCreate; + pub use crate::v2026_05_26_00::probe::ProbeCreate; } pub mod project { diff --git a/nexus/types/versions/src/lib.rs b/nexus/types/versions/src/lib.rs index ceaf0faa94a..9845f5d125c 100644 --- a/nexus/types/versions/src/lib.rs +++ b/nexus/types/versions/src/lib.rs @@ -87,3 +87,5 @@ pub mod v2026_05_07_00; pub mod v2026_05_08_00; #[path = "add_contact_support_to_update_status/mod.rs"] pub mod v2026_05_20_00; +#[path = "probe_multicast/mod.rs"] +pub mod v2026_05_26_00; diff --git a/nexus/types/versions/src/probe_multicast/mod.rs b/nexus/types/versions/src/probe_multicast/mod.rs new file mode 100644 index 00000000000..bebeede36de --- /dev/null +++ b/nexus/types/versions/src/probe_multicast/mod.rs @@ -0,0 +1,13 @@ +// 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 `PROBE_MULTICAST` of the Nexus external API. +//! +//! This module adds `multicast_groups` to `ProbeCreate` so that probes can be +//! enrolled as multicast group members at creation time, using the same +//! `MulticastGroupJoinSpec` shape as instance joins. Membership is fixed +//! at probe creation. + +pub mod multicast; +pub mod probe; diff --git a/nexus/types/versions/src/probe_multicast/multicast.rs b/nexus/types/versions/src/probe_multicast/multicast.rs new file mode 100644 index 00000000000..eda21cf2397 --- /dev/null +++ b/nexus/types/versions/src/probe_multicast/multicast.rs @@ -0,0 +1,115 @@ +// 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 from API version 2026_05_26_00 (PROBE_MULTICAST) that changed in +//! a later version. +//! +//! Reshapes `MulticastGroupMember` from a single `instance_id` field to a +//! `kind` / `parent_id` pair. Members may now be parented by a probe in +//! addition to an instance, so the `instance_id` field name no longer +//! reflects reality for probe-parented rows. + +use api_identity::ObjectIdentity; +use omicron_common::api::external::{IdentityMetadata, ObjectIdentity}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::net::IpAddr; +use uuid::Uuid; + +/// Kind of resource that owns a multicast group member. +/// +/// Members can be parented by either an instance or a probe. The two kinds +/// resolve their hosting sled differently in the control plane, but the +/// API surface only needs to expose the discriminator alongside `parent_id`. +#[derive( + Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, +)] +#[serde(rename_all = "snake_case")] +pub enum MulticastGroupMemberParentKind { + Instance, + Probe, +} + +/// View of a Multicast Group Member. +/// +/// A member may be parented by either an instance or a probe. The `kind` +/// discriminator selects how to interpret `parent_id`. +#[derive( + ObjectIdentity, Debug, PartialEq, Clone, Deserialize, Serialize, JsonSchema, +)] +pub struct MulticastGroupMember { + #[serde(flatten)] + pub identity: IdentityMetadata, + /// The ID of the multicast group this member belongs to. + pub multicast_group_id: Uuid, + /// The multicast IP address of the group this member belongs to. + pub multicast_ip: IpAddr, + /// Discriminator for `parent_id`. + pub kind: MulticastGroupMemberParentKind, + /// The UUID of the parent (instance or probe) that owns this membership. + pub parent_id: Uuid, + /// Source IP addresses for this member's multicast subscription. + /// + /// - **ASM**: Sources are optional. Empty array means any source is allowed. + /// Non-empty array enables source filtering (IGMPv3/MLDv2). + /// - **SSM**: Sources are required for SSM addresses (232/8, ff3x::/32). + pub source_ips: Vec, + /// Current state of the multicast group membership. + pub state: String, +} + +// -- Conversions between PROBE_MULTICAST and v2026_01_08_00 multicast types -- + +/// Down-conversion to the prior API shape, which only models instance-parented +/// members. Probe-parented members have no representation in the older view, +/// so the conversion fails with `406 Not Acceptable` to signal that the row +/// post-dates the client's pinned version. +impl TryFrom + for crate::v2026_01_08_00::multicast::MulticastGroupMember +{ + type Error = dropshot::HttpError; + + fn try_from(new: MulticastGroupMember) -> Result { + match new.kind { + MulticastGroupMemberParentKind::Instance => Ok(Self { + identity: new.identity, + multicast_group_id: new.multicast_group_id, + multicast_ip: new.multicast_ip, + instance_id: new.parent_id, + source_ips: new.source_ips, + state: new.state, + }), + MulticastGroupMemberParentKind::Probe => { + Err(dropshot::HttpError::for_client_error( + Some(String::from("Not Acceptable")), + dropshot::ClientErrorStatusCode::NOT_ACCEPTABLE, + String::from( + "multicast group member kind not supported for \ + client version", + ), + )) + } + } + } +} + +/// Up-conversion: older clients only saw instance-parented members, so +/// synthesize `kind = Instance` and carry `instance_id` through as `parent_id`. +impl From + for MulticastGroupMember +{ + fn from( + old: crate::v2026_01_08_00::multicast::MulticastGroupMember, + ) -> Self { + Self { + identity: old.identity, + multicast_group_id: old.multicast_group_id, + multicast_ip: old.multicast_ip, + kind: MulticastGroupMemberParentKind::Instance, + parent_id: old.instance_id, + source_ips: old.source_ips, + state: old.state, + } + } +} diff --git a/nexus/types/versions/src/probe_multicast/probe.rs b/nexus/types/versions/src/probe_multicast/probe.rs new file mode 100644 index 00000000000..bb37c877464 --- /dev/null +++ b/nexus/types/versions/src/probe_multicast/probe.rs @@ -0,0 +1,54 @@ +// 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/. + +//! Probe types for version PROBE_MULTICAST. +//! +//! Extends `ProbeCreate` with a `multicast_groups` field so probes can be +//! enrolled as multicast group members at creation time, mirroring how +//! `InstanceCreate` already accepts multicast group memberships. +//! Membership is fixed at probe creation. Recreate the probe to change it. + +use omicron_common::api::external::IdentityMetadataCreateParams; +use omicron_uuid_kinds::SledUuid; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::v2026_01_05_00; +use crate::v2026_01_05_00::ip_pool::PoolSelector; +use crate::v2026_01_08_00::multicast::MulticastGroupJoinSpec; + +/// Create time parameters for probes. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct ProbeCreate { + #[serde(flatten)] + pub identity: IdentityMetadataCreateParams, + #[schemars(with = "Uuid")] + pub sled: SledUuid, + /// Pool to allocate from. + #[serde(default)] + pub pool_selector: PoolSelector, + /// Multicast groups to enroll this probe as a member of at creation + /// time. Each entry specifies a group (by name, UUID, or IP address), + /// plus optional source IPs and IP version, matching the instance + /// join shape. Non-existent groups are created automatically when + /// given an IP covered by a multicast pool. + /// + /// If empty (the default), this leaves the probe unenrolled. + /// Probe memberships are fixed at creation. Recreate the probe to + /// change or modify them. + #[serde(default)] + pub multicast_groups: Vec, +} + +impl From for ProbeCreate { + fn from(old: v2026_01_05_00::probe::ProbeCreate) -> ProbeCreate { + ProbeCreate { + identity: old.identity, + sled: old.sled, + pool_selector: old.pool_selector, + multicast_groups: Vec::new(), + } + } +} diff --git a/openapi/nexus/nexus-2026052200.0.0-c91da3.json b/openapi/nexus/nexus-2026052200.0.0-ad7a10.json similarity index 99% rename from openapi/nexus/nexus-2026052200.0.0-c91da3.json rename to openapi/nexus/nexus-2026052200.0.0-ad7a10.json index 0f3fa92f5bf..c76e27b8e53 100644 --- a/openapi/nexus/nexus-2026052200.0.0-c91da3.json +++ b/openapi/nexus/nexus-2026052200.0.0-ad7a10.json @@ -4770,7 +4770,6 @@ "experimental" ], "summary": "Join multicast group by name, IP address, or UUID", - "description": "Groups can be referenced by name, IP address, or UUID. If the group doesn't exist, it's implicitly created with an auto-allocated IP from a multicast pool linked to the caller's silo. When referencing by UUID, the group must already exist.\n\nSource IPs are optional for ASM addresses but required for SSM addresses (232.0.0.0/8 for IPv4, ff3x::/32 for IPv6). Duplicate source IPs in a single request are rejected. Per-member source list is capped at 32, and the union of source IPs across all members of a single group is capped at 256.", "operationId": "instance_multicast_group_join", "parameters": [ { @@ -6582,7 +6581,6 @@ "experimental" ], "summary": "List members of multicast group", - "description": "The group can be specified by name, UUID, or multicast IP address.", "operationId": "multicast_group_member_list", "parameters": [ { diff --git a/openapi/nexus/nexus-2026052600.0.0-77f1e4.json b/openapi/nexus/nexus-2026052600.0.0-77f1e4.json new file mode 100644 index 00000000000..29d428b2f1d --- /dev/null +++ b/openapi/nexus/nexus-2026052600.0.0-77f1e4.json @@ -0,0 +1,32104 @@ +{ + "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": "2026052600.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 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 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 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 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 support bundle contents", + "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 support bundle metadata", + "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 file from 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 metadata of file in 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 support bundle index", + "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 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 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 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/external-subnets": { + "get": { + "tags": [ + "external-subnets" + ], + "summary": "List external subnets", + "operationId": "external_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", + "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/ExternalSubnetResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "project" + ] + } + }, + "post": { + "tags": [ + "external-subnets" + ], + "summary": "Create external subnet", + "operationId": "external_subnet_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/ExternalSubnetCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalSubnet" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/external-subnets/{external_subnet}": { + "get": { + "tags": [ + "external-subnets" + ], + "summary": "Fetch external subnet", + "operationId": "external_subnet_view", + "parameters": [ + { + "in": "path", + "name": "external_subnet", + "description": "Name or ID of the external subnet", + "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/ExternalSubnet" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "external-subnets" + ], + "summary": "Update external subnet", + "operationId": "external_subnet_update", + "parameters": [ + { + "in": "path", + "name": "external_subnet", + "description": "Name or ID of the external subnet", + "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/ExternalSubnetUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalSubnet" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "external-subnets" + ], + "summary": "Delete external subnet", + "operationId": "external_subnet_delete", + "parameters": [ + { + "in": "path", + "name": "external_subnet", + "description": "Name or ID of the external subnet", + "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/external-subnets/{external_subnet}/attach": { + "post": { + "tags": [ + "external-subnets" + ], + "summary": "Attach external subnet to instance", + "operationId": "external_subnet_attach", + "parameters": [ + { + "in": "path", + "name": "external_subnet", + "description": "Name or ID of the external subnet", + "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/ExternalSubnetAttach" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalSubnet" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/external-subnets/{external_subnet}/detach": { + "post": { + "tags": [ + "external-subnets" + ], + "summary": "Detach external subnet from instance", + "operationId": "external_subnet_detach", + "parameters": [ + { + "in": "path", + "name": "external_subnet", + "description": "Name or ID of the external subnet", + "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/ExternalSubnet" + } + } + } + }, + "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", + "description": "A specific IP address can be reserved, or an IP can be auto-allocated from a specific pool or the silo's default pool.", + "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", + "description": "When an instance has both IPv4 and IPv6 ephemeral IPs, the `ip_version` query parameter must be specified to identify which IP to detach.", + "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": "ip_version", + "description": "The IP version of the ephemeral IP to detach.\n\nRequired when the instance has both IPv4 and IPv6 ephemeral IPs. If only one ephemeral IP is attached, this field may be omitted.", + "schema": { + "$ref": "#/components/schemas/IpVersion" + } + }, + { + "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}/external-subnets": { + "get": { + "tags": [ + "instances" + ], + "summary": "List external subnets attached to instance", + "operationId": "instance_external_subnet_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/ExternalSubnetResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/instances/{instance}/multicast-groups": { + "get": { + "tags": [ + "experimental" + ], + "summary": "List multicast groups for an instance", + "operationId": "instance_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": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + }, + { + "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" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/instances/{instance}/multicast-groups/{multicast_group}": { + "put": { + "tags": [ + "experimental" + ], + "summary": "Join multicast group by name, IP address, or UUID", + "description": "Groups can be referenced by name, IP address, or UUID. If the group doesn't exist, it's implicitly created with an auto-allocated IP from a multicast pool linked to the caller's silo. When referencing by UUID, the group must already exist.\n\nSource IPs are optional for ASM addresses but required for SSM addresses (232.0.0.0/8 for IPv4, ff3x::/32 for IPv6). Duplicate source IPs in a single request are rejected. Per-member source list is capped at 32, and the union of source IPs across all members of a single group is capped at 256.", + "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, ID, or IP address of the multicast group", + "required": true, + "schema": { + "$ref": "#/components/schemas/MulticastGroupIdentifier" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceMulticastGroupJoin" + } + } + }, + "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" + } + } + }, + "delete": { + "tags": [ + "experimental" + ], + "summary": "Leave multicast group by name, IP address, or UUID", + "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, ID, or IP address of the multicast group", + "required": true, + "schema": { + "$ref": "#/components/schemas/MulticastGroupIdentifier" + } + }, + { + "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 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": { + "101": { + "description": "Negotiating protocol upgrade from HTTP/1.1 to WebSocket" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "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": [ + "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/SiloIpPoolResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/ip-pools/{pool}": { + "get": { + "tags": [ + "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/SiloIpPool" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/login/{silo_name}/local": { + "post": { + "tags": [ + "login" + ], + "summary": "Authenticate 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 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": [] + } + } + }, + "/v1/multicast-groups/{multicast_group}": { + "get": { + "tags": [ + "experimental" + ], + "summary": "Fetch multicast group", + "description": "The group can be specified by name, UUID, or multicast IP address. (e.g., \"224.1.2.3\" or \"ff38::1\").", + "operationId": "multicast_group_view", + "parameters": [ + { + "in": "path", + "name": "multicast_group", + "description": "Name, ID, or IP address of the multicast group", + "required": true, + "schema": { + "$ref": "#/components/schemas/MulticastGroupIdentifier" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroup" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/multicast-groups/{multicast_group}/members": { + "get": { + "tags": [ + "experimental" + ], + "summary": "List members of multicast group", + "description": "The group can be specified by name, UUID, or multicast IP address.", + "operationId": "multicast_group_member_list", + "parameters": [ + { + "in": "path", + "name": "multicast_group", + "description": "Name, ID, or IP address of the multicast group", + "required": true, + "schema": { + "$ref": "#/components/schemas/MulticastGroupIdentifier" + } + }, + { + "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": [] + } + } + }, + "/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 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/subnet-pools": { + "get": { + "tags": [ + "subnet-pools" + ], + "summary": "List subnet pools", + "operationId": "subnet_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/SiloSubnetPoolResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/subnet-pools/{pool}": { + "get": { + "tags": [ + "subnet-pools" + ], + "summary": "Fetch subnet pool", + "operationId": "subnet_pool_view", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the subnet pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloSubnetPool" + } + } + } + }, + "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/disk-adoption-request": { + "put": { + "tags": [ + "system/hardware" + ], + "summary": "Enable adoption of a physical disk for general use", + "operationId": "physical_disk_enable_adoption", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PhysicalDiskManufacturerIdentity" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PhysicalDiskAdoptionRequest" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/hardware/disk-adoption-request/{physical_disk_adoption_req_id}": { + "delete": { + "tags": [ + "system/hardware" + ], + "summary": "Disable adoption of a physical disk for general use", + "operationId": "physical_disk_disable_adoption", + "parameters": [ + { + "in": "path", + "name": "physical_disk_adoption_req_id", + "description": "ID of the physical disk adoption request", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/hardware/disk-adoption-requests": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "List physical disk adoption requests", + "operationId": "physical_disk_list_adoption_requests", + "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/PhysicalDiskAdoptionRequestResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/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 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/disks-unadopted": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "List physical disks that have not yet been adopted for use", + "operationId": "physical_disk_list_unadopted", + "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/UnadoptedPhysicalDiskResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/hardware/rack-switch-port/{rack_id}/{switch_slot}/{port}/lldp/neighbors": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Fetch LLDP neighbors for 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_slot", + "description": "The slot of the switch within the rack to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/SwitchSlot" + } + }, + { + "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/racks/{rack_id}/membership": { + "get": { + "tags": [ + "experimental" + ], + "summary": "Fetch rack cluster membership status", + "description": "Returns the status for the most recent change, or a specific version if one is specified.", + "operationId": "rack_membership_status", + "parameters": [ + { + "in": "path", + "name": "rack_id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "version", + "schema": { + "$ref": "#/components/schemas/RackMembershipVersion" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RackMembershipStatus" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/hardware/racks/{rack_id}/membership/abort": { + "post": { + "tags": [ + "experimental" + ], + "summary": "Abort the latest rack membership change", + "description": "This operation is synchronous. Upon returning from the API call, a success response indicates that the prior membership change was aborted. An error response indicates that there is no active membership change in progress (previous changes have completed) or that the current membership change could not be aborted.", + "operationId": "rack_membership_abort", + "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/RackMembershipStatus" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/hardware/racks/{rack_id}/membership/add": { + "post": { + "tags": [ + "experimental" + ], + "summary": "Add new sleds to rack membership", + "operationId": "rack_membership_add_sleds", + "parameters": [ + { + "in": "path", + "name": "rack_id", + "description": "ID of the rack", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RackMembershipAddSledsRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RackMembershipStatus" + } + } + } + }, + "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": [] + } + } + }, + "/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 LLDP configuration for 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_slot", + "description": "The slot of the switch within the rack to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/SwitchSlot" + } + } + ], + "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 LLDP configuration for 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_slot", + "description": "The slot of the switch within the rack to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/SwitchSlot" + } + } + ], + "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_slot", + "description": "The slot of the switch within the rack to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/SwitchSlot" + } + } + ], + "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_slot", + "description": "The slot of the switch within the rack to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/SwitchSlot" + } + } + ], + "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_slot", + "description": "The slot of the switch within the rack to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/SwitchSlot" + } + } + ], + "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": "system_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", + "operationId": "system_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": "system_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": "system_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": "system_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": "system_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": "For 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": "system_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": "system_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": "system_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": "system_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": "system_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": "system_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": "system_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": "system_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": "system_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": "system_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": "system_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/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 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 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 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": "List BGP exported routes", + "operationId": "networking_bgp_exported", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_BgpExported", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpExported" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/bgp-imported": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Get imported IPv4 BGP routes", + "operationId": "networking_bgp_imported", + "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_BgpImported", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpImported" + } + } + } + } + }, + "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-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_slot}/{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_slot", + "description": "The slot of the switch within the rack to use when selecting the loopback address.", + "required": true, + "schema": { + "$ref": "#/components/schemas/SwitchSlot" + } + } + ], + "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": "List 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 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 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/silos/{silo}/subnet-pools": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "List subnet pools linked to a silo", + "operationId": "silo_subnet_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/SiloSubnetPoolResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/subnet-pools": { + "get": { + "tags": [ + "system/subnet-pools" + ], + "summary": "List subnet pools", + "operationId": "system_subnet_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/SubnetPoolResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "system/subnet-pools" + ], + "summary": "Create subnet pool", + "operationId": "system_subnet_pool_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubnetPoolCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubnetPool" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/subnet-pools/{pool}": { + "get": { + "tags": [ + "system/subnet-pools" + ], + "summary": "Fetch subnet pool", + "operationId": "system_subnet_pool_view", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the subnet pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubnetPool" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "system/subnet-pools" + ], + "summary": "Update subnet pool", + "operationId": "system_subnet_pool_update", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the subnet pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubnetPoolUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubnetPool" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "system/subnet-pools" + ], + "summary": "Delete subnet pool", + "operationId": "system_subnet_pool_delete", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the subnet 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/subnet-pools/{pool}/members": { + "get": { + "tags": [ + "system/subnet-pools" + ], + "summary": "List members in subnet pool", + "operationId": "system_subnet_pool_member_list", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the subnet 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/SubnetPoolMemberResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/subnet-pools/{pool}/members/add": { + "post": { + "tags": [ + "system/subnet-pools" + ], + "summary": "Add member to subnet pool", + "operationId": "system_subnet_pool_member_add", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the subnet pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubnetPoolMemberAdd" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubnetPoolMember" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/subnet-pools/{pool}/members/remove": { + "post": { + "tags": [ + "system/subnet-pools" + ], + "summary": "Remove member from subnet pool", + "operationId": "system_subnet_pool_member_remove", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the subnet pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubnetPoolMemberRemove" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/subnet-pools/{pool}/silos": { + "get": { + "tags": [ + "system/subnet-pools" + ], + "summary": "List silos linked to subnet pool", + "operationId": "system_subnet_pool_silo_list", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the subnet 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/SubnetPoolSiloLinkResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "system/subnet-pools" + ], + "summary": "Link subnet pool to silo", + "operationId": "system_subnet_pool_silo_link", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the subnet pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubnetPoolLinkSilo" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubnetPoolSiloLink" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/subnet-pools/{pool}/silos/{silo}": { + "put": { + "tags": [ + "system/subnet-pools" + ], + "summary": "Update subnet pool's link to silo", + "operationId": "system_subnet_pool_silo_update", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the subnet pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "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/SubnetPoolSiloUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubnetPoolSiloLink" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "system/subnet-pools" + ], + "summary": "Unlink subnet pool from silo", + "operationId": "system_subnet_pool_silo_unlink", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the subnet pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "silo", + "description": "Name or ID of the 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/subnet-pools/{pool}/utilization": { + "get": { + "tags": [ + "system/subnet-pools" + ], + "summary": "Fetch subnet pool utilization", + "operationId": "system_subnet_pool_utilization_view", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the subnet pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubnetPoolUtilization" + } + } + } + }, + "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/recovery-finish": { + "put": { + "tags": [ + "system/update" + ], + "summary": "Clear system recovery status", + "description": "Instructs the system that a system recovery operation (\"mupdate\") was completed using the software in the specified release.\n\nThe system recovery operation is used to bypass the control plane to deploy known-working software when the control plane itself is not functioning or otherwise unable to update itself. When the control plane detects this, it stops making any changes to deployed software to avoid reverting the recovery itself. This operation puts the control plane back in charge of determining what software should be deployed, instructing it that the specified software (which is also what's currently running) is what's supposed to be deployed.\n\nIf the provided version does not match what's currently running, the control plane will continue to avoid changing deployed software until this operation is invoked with the correct version.\n\nThis endpoint should only be called at the direction of Oxide support.", + "operationId": "system_update_recovery_finish", + "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/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 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" + ] + }, + "AddressAllocator": { + "description": "Specify how to allocate a floating IP address.", + "oneOf": [ + { + "description": "Reserve a specific IP address. The pool is inferred from the address since IP pools cannot have overlapping ranges.", + "type": "object", + "properties": { + "ip": { + "description": "The IP address to reserve.", + "type": "string", + "format": "ip" + }, + "type": { + "type": "string", + "enum": [ + "explicit" + ] + } + }, + "required": [ + "ip", + "type" + ] + }, + { + "description": "Automatically allocate an IP address from a pool.", + "type": "object", + "properties": { + "pool_selector": { + "description": "Pool selection.\n\nIf omitted, the silo's default pool is used. If the silo has default pools for both IPv4 and IPv6, the request will fail unless `ip_version` is specified.", + "default": { + "ip_version": null, + "type": "auto" + }, + "allOf": [ + { + "$ref": "#/components/schemas/PoolSelector" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "auto" + ] + } + }, + "required": [ + "type" + ] + } + ] + }, + "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. First 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": { + "description": "A list containing the IDs of the secret keys used to sign payloads sent to this receiver.", + "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 (access token, session, or SCIM token). Null for unauthenticated requests like login attempts.", + "allOf": [ + { + "$ref": "#/components/schemas/AuthMethod" + } + ] + }, + "credential_id": { + "nullable": true, + "description": "ID of the credential used for authentication. Null for unauthenticated requests. The value of `auth_method` indicates what kind of credential it is (access token, session, or SCIM token).", + "type": "string", + "format": "uuid" + }, + "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" + ] + }, + "AuthMethod": { + "description": "Authentication method used for a request", + "oneOf": [ + { + "description": "Console session cookie", + "type": "string", + "enum": [ + "session_cookie" + ] + }, + { + "description": "Device access token (OAuth 2.0 device authorization flow)", + "type": "string", + "enum": [ + "access_token" + ] + }, + { + "description": "SCIM client bearer token", + "type": "string", + "enum": [ + "scim_token" + ] + } + ] + }, + "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" + ] + }, + "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" + ] + }, + "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_slot": { + "description": "The slot of the switch within the rack to disable this session on.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchSlot" + } + ] + } + }, + "required": [ + "remote", + "switch_slot" + ] + }, + "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_slot": { + "description": "The slot of the switch within the rack to enable this session on.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchSlot" + } + ] + } + }, + "required": [ + "detection_threshold", + "mode", + "remote", + "required_rx", + "switch_slot" + ] + }, + "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_slot": { + "$ref": "#/components/schemas/SwitchSlot" + } + }, + "required": [ + "detection_threshold", + "mode", + "peer", + "required_rx", + "state", + "switch_slot" + ] + }, + "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" + }, + "max_paths": { + "description": "Maximum number of paths to use when multiple \"best paths\" exist", + "allOf": [ + { + "$ref": "#/components/schemas/MaxPathConfig" + } + ] + }, + "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", + "max_paths", + "name", + "time_created", + "time_modified" + ] + }, + "BgpConfigCreate": { + "description": "Parameters for creating a BGP configuration. This includes an 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" + }, + "max_paths": { + "description": "Maximum number of paths to use when multiple \"best paths\" exist", + "default": 1, + "allOf": [ + { + "$ref": "#/components/schemas/MaxPathConfig" + } + ] + }, + "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": "Route exported to a peer.", + "type": "object", + "properties": { + "peer_id": { + "description": "Identifier for the BGP peer.", + "type": "string" + }, + "prefix": { + "description": "The destination network prefix.", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + }, + "switch": { + "description": "Switch the route is exported from.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchSlot" + } + ] + } + }, + "required": [ + "peer_id", + "prefix", + "switch" + ] + }, + "BgpImported": { + "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": "ip" + }, + "prefix": { + "description": "The destination network prefix.", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + }, + "switch": { + "description": "Switch the route is imported into.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchSlot" + } + ] + } + }, + "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. The `bgp_config` parameter is a reference to global BGP parameters.", + "type": "object", + "properties": { + "addr": { + "description": "The address of the host to peer with, or specifying the configuration of an unnumbered BGP session.", + "allOf": [ + { + "$ref": "#/components/schemas/RouterPeerType" + } + ] + }, + "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 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 + }, + "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": [ + "addr", + "allowed_export", + "allowed_import", + "bgp_config", + "communities", + "connect_retry", + "delay_open", + "enforce_first_as", + "hold_time", + "idle_hold_time", + "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 + }, + "peer_id": { + "description": "Interface name", + "type": "string" + }, + "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/SwitchSlot" + } + ] + } + }, + "required": [ + "addr", + "local_asn", + "peer_id", + "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" + } + ] + }, + "time_created": { + "description": "Timestamp when this user was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "Timestamp when this user was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "display_name", + "fleet_viewer", + "id", + "silo_admin", + "silo_id", + "silo_name", + "time_created", + "time_modified" + ] + }, + "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.\n\nNote 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" + }, + "read_only": { + "description": "Whether or not this disk is read-only.", + "type": "boolean" + }, + "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", + "read_only", + "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": { + "read_only": { + "description": "If `true`, the disk created from this snapshot will be read-only.", + "default": false, + "type": "boolean" + }, + "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" + }, + "read_only": { + "description": "If `true`, the disk created from this image will be read-only.", + "default": false, + "type": "boolean" + }, + "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": { + "pool_selector": { + "description": "Pool to allocate from.", + "default": { + "ip_version": null, + "type": "auto" + }, + "allOf": [ + { + "$ref": "#/components/schemas/PoolSelector" + } + ] + } + } + }, + "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 a pool.", + "type": "object", + "properties": { + "pool_selector": { + "description": "Pool to allocate from.", + "default": { + "ip_version": null, + "type": "auto" + }, + "allOf": [ + { + "$ref": "#/components/schemas/PoolSelector" + } + ] + }, + "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" + ] + }, + "ExternalSubnet": { + "description": "An external subnet allocated from a subnet pool", + "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 instance this subnet is attached to, if any", + "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 subnet belongs to", + "type": "string", + "format": "uuid" + }, + "subnet": { + "description": "The allocated subnet CIDR", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + }, + "subnet_pool_id": { + "description": "The subnet pool this was allocated from", + "type": "string", + "format": "uuid" + }, + "subnet_pool_member_id": { + "description": "The subnet pool member this subnet corresponds to", + "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", + "project_id", + "subnet", + "subnet_pool_id", + "subnet_pool_member_id", + "time_created", + "time_modified" + ] + }, + "ExternalSubnetAllocator": { + "description": "Specify how to allocate an external subnet.", + "oneOf": [ + { + "description": "Reserve a specific subnet.", + "type": "object", + "properties": { + "subnet": { + "description": "The subnet CIDR to reserve. Must be available in the pool.", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "explicit" + ] + } + }, + "required": [ + "subnet", + "type" + ] + }, + { + "description": "Automatically allocate a subnet with the specified prefix length.", + "type": "object", + "properties": { + "pool_selector": { + "description": "Pool selection.\n\nIf omitted, this field uses the silo's default pool. If the silo has default pools for both IPv4 and IPv6, the request will fail unless `ip_version` is specified in the pool selector.", + "default": { + "ip_version": null, + "type": "auto" + }, + "allOf": [ + { + "$ref": "#/components/schemas/PoolSelector" + } + ] + }, + "prefix_length": { + "description": "The prefix length for the allocated subnet (e.g., 24 for a /24).", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "auto" + ] + } + }, + "required": [ + "prefix_length", + "type" + ] + } + ] + }, + "ExternalSubnetAttach": { + "description": "Attach an external subnet to an instance", + "type": "object", + "properties": { + "instance": { + "description": "Name or ID of the instance to attach to", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + } + }, + "required": [ + "instance" + ] + }, + "ExternalSubnetCreate": { + "description": "Create an external subnet", + "type": "object", + "properties": { + "allocator": { + "description": "Subnet allocation method.", + "allOf": [ + { + "$ref": "#/components/schemas/ExternalSubnetAllocator" + } + ] + }, + "description": { + "type": "string" + }, + "name": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "allocator", + "description", + "name" + ] + }, + "ExternalSubnetResultsPage": { + "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/ExternalSubnet" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "ExternalSubnetUpdate": { + "description": "Update an external subnet", + "type": "object", + "properties": { + "description": { + "nullable": true, + "type": "string" + }, + "name": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + } + }, + "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": { + "address_allocator": { + "description": "IP address allocation method.", + "default": { + "pool_selector": { + "ip_version": null, + "type": "auto" + }, + "type": "auto" + }, + "allOf": [ + { + "$ref": "#/components/schemas/AddressAllocator" + } + ] + }, + "description": { + "type": "string" + }, + "name": { + "$ref": "#/components/schemas/Name" + } + }, + "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" + }, + "time_created": { + "description": "Timestamp when this group was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "Timestamp when this group was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "display_name", + "id", + "silo_id", + "time_created", + "time_modified" + ] + }, + "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_slot": { + "description": "The slot of the switch within the rack where this loopback address is assigned.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchSlot" + } + ] + } + }, + "required": [ + "address", + "address_lot_block_id", + "id", + "rack_id", + "switch_slot" + ] + }, + "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.\n\nThis 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_slot": { + "description": "The slot of the switch within the rack this loopback address will be configured on.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchSlot" + } + ] + } + }, + "required": [ + "address", + "address_lot", + "anycast", + "mask", + "rack_id", + "switch_slot" + ] + }, + "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" + ] + }, + "MaxPathConfig": { + "type": "integer", + "format": "uint8", + "minimum": 1, + "maximum": 32 + }, + "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" + }, + "has_any_source_member": { + "description": "True if any member joined without specifying source IPs (any-source).\n\nWhen true, at least one member receives traffic from any source rather than filtering to specific sources.", + "type": "boolean" + }, + "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" + }, + "name": { + "description": "Unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "source_ips": { + "description": "Deduplicated union of source IPs specified by members.\n\nContains only sources from members that joined with explicit `source_ips`. Members using any-source multicast (empty `source_ips`) do not contribute, so a non-empty value does not imply all members use source filtering. For SSM addresses (232/8, ff3x::/32), this is always non-empty.", + "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", + "has_any_source_member", + "id", + "ip_pool_id", + "multicast_ip", + "name", + "source_ips", + "state", + "time_created", + "time_modified" + ] + }, + "MulticastGroupIdentifier": { + "title": "A multicast group identifier", + "description": "Can be a UUID, a name, or an IP address", + "type": "string" + }, + "MulticastGroupJoinSpec": { + "description": "Specification for joining a multicast group with optional source filtering.\n\nUsed in `InstanceCreate` and `InstanceUpdate` to specify multicast group membership along with per-member source IP configuration.", + "type": "object", + "properties": { + "group": { + "description": "The multicast group to join, specified by name, UUID, or IP address.", + "allOf": [ + { + "$ref": "#/components/schemas/MulticastGroupIdentifier" + } + ] + }, + "ip_version": { + "nullable": true, + "description": "IP version for pool selection when creating a group by name. Required if both IPv4 and IPv6 default multicast pools are linked.", + "default": null, + "allOf": [ + { + "$ref": "#/components/schemas/IpVersion" + } + ] + }, + "source_ips": { + "nullable": true, + "description": "Source IPs for source-filtered multicast (SSM). Optional for ASM groups, required for SSM groups (232.0.0.0/8, ff3x::/32).", + "default": null, + "type": "array", + "items": { + "type": "string", + "format": "ip" + } + } + }, + "required": [ + "group" + ] + }, + "MulticastGroupMember": { + "description": "View of a Multicast Group Member.\n\nA member may be parented by either an instance or a probe. The `kind` discriminator selects how to interpret `parent_id`.", + "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": "Discriminator for `parent_id`.", + "allOf": [ + { + "$ref": "#/components/schemas/MulticastGroupMemberParentKind" + } + ] + }, + "multicast_group_id": { + "description": "The ID of the multicast group this member belongs to.", + "type": "string", + "format": "uuid" + }, + "multicast_ip": { + "description": "The multicast IP address of the group this member belongs to.", + "type": "string", + "format": "ip" + }, + "name": { + "description": "Unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "parent_id": { + "description": "The UUID of the parent (instance or probe) that owns this membership.", + "type": "string", + "format": "uuid" + }, + "source_ips": { + "description": "Source IP addresses for this member's multicast subscription.\n\n- **ASM**: Sources are optional. Empty array means any source is allowed. Non-empty array enables source filtering (IGMPv3/MLDv2). - **SSM**: Sources are required for SSM addresses (232/8, ff3x::/32).", + "type": "array", + "items": { + "type": "string", + "format": "ip" + } + }, + "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", + "kind", + "multicast_group_id", + "multicast_ip", + "name", + "parent_id", + "source_ips", + "state", + "time_created", + "time_modified" + ] + }, + "MulticastGroupMemberParentKind": { + "description": "Kind of resource that owns a multicast group member.\n\nMembers can be parented by either an instance or a probe. The two kinds resolve their hosting sled differently in the control plane, but the API surface only needs to expose the discriminator alongside `parent_id`.", + "type": "string", + "enum": [ + "instance", + "probe" + ] + }, + "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" + ] + }, + "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" + ] + }, + "PhysicalDiskAdoptionRequest": { + "description": "A request to adopt a physical disk into the control plane", + "type": "object", + "properties": { + "disk_id": { + "$ref": "#/components/schemas/PhysicalDiskManufacturerIdentity" + }, + "id": { + "$ref": "#/components/schemas/PhysicalDiskAdoptionRequestUuid" + }, + "time_created": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "disk_id", + "id", + "time_created" + ] + }, + "PhysicalDiskAdoptionRequestResultsPage": { + "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/PhysicalDiskAdoptionRequest" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "PhysicalDiskAdoptionRequestUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::PhysicalDiskAdoptionRequestUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "PhysicalDiskKind": { + "description": "Describes the form factor of physical disks.", + "type": "string", + "enum": [ + "m2", + "u2" + ] + }, + "PhysicalDiskManufacturerIdentity": { + "description": "The unique identity of a physical disk provided by the manufacturer", + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "serial": { + "type": "string" + }, + "vendor": { + "type": "string" + } + }, + "required": [ + "model", + "serial", + "vendor" + ] + }, + "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" + ] + }, + "PoolSelector": { + "description": "Specify which IP or external subnet pool to allocate from.", + "oneOf": [ + { + "description": "Use the specified pool by name or ID.", + "type": "object", + "properties": { + "pool": { + "description": "The pool to allocate from.", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "explicit" + ] + } + }, + "required": [ + "pool", + "type" + ] + }, + { + "description": "Use the default pool for the silo.", + "type": "object", + "properties": { + "ip_version": { + "nullable": true, + "description": "IP version to use when multiple default pools exist. Required if both IPv4 and IPv6 default pools are configured.", + "default": null, + "allOf": [ + { + "$ref": "#/components/schemas/IpVersion" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "auto" + ] + } + }, + "required": [ + "type" + ] + } + ] + }, + "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": "A networking probe", + "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" + }, + "multicast_groups": { + "description": "Multicast groups to enroll this probe as a member of at creation time. Each entry specifies a group (by name, UUID, or IP address), plus optional source IPs and IP version, matching the instance join shape. Non-existent groups are created automatically when given an IP covered by a multicast pool.\n\nIf empty (the default), this leaves the probe unenrolled. Probe memberships are fixed at creation. Recreate the probe to change or modify them.", + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/MulticastGroupJoinSpec" + } + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "pool_selector": { + "description": "Pool to allocate from.", + "default": { + "ip_version": null, + "type": "auto" + }, + "allOf": [ + { + "$ref": "#/components/schemas/PoolSelector" + } + ] + }, + "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 a 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" + ] + }, + "RackMembershipAddSledsRequest": { + "type": "object", + "properties": { + "sled_ids": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseboardId" + }, + "uniqueItems": true + } + }, + "required": [ + "sled_ids" + ] + }, + "RackMembershipChangeState": { + "type": "string", + "enum": [ + "in_progress", + "committed", + "aborted" + ] + }, + "RackMembershipStatus": { + "description": "Status of the rack membership uniquely identified by the (rack_id, version) pair", + "type": "object", + "properties": { + "members": { + "description": "All members of the rack for this version", + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseboardId" + }, + "uniqueItems": true + }, + "rack_id": { + "type": "string", + "format": "uuid" + }, + "state": { + "$ref": "#/components/schemas/RackMembershipChangeState" + }, + "time_aborted": { + "nullable": true, + "type": "string", + "format": "date-time" + }, + "time_committed": { + "nullable": true, + "type": "string", + "format": "date-time" + }, + "time_created": { + "type": "string", + "format": "date-time" + }, + "unacknowledged_members": { + "description": "All members that have not yet confirmed this membership version", + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseboardId" + }, + "uniqueItems": true + }, + "version": { + "description": "Version that uniquely identifies the rack membership at a given point in time", + "allOf": [ + { + "$ref": "#/components/schemas/RackMembershipVersion" + } + ] + } + }, + "required": [ + "members", + "rack_id", + "state", + "time_created", + "unacknowledged_members", + "version" + ] + }, + "RackMembershipVersion": { + "description": "A unique, monotonically increasing number representing the set of active sleds in a rack at a given point in time.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "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" + ] + } + ] + }, + "RouterLifetimeConfig": { + "description": "Router lifetime in seconds for unnumbered BGP peers", + "type": "integer", + "format": "uint16", + "minimum": 0, + "maximum": 9000 + }, + "RouterPeerType": { + "oneOf": [ + { + "type": "object", + "properties": { + "router_lifetime": { + "description": "Router lifetime in seconds for unnumbered BGP peers.", + "allOf": [ + { + "$ref": "#/components/schemas/RouterLifetimeConfig" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "unnumbered" + ] + } + }, + "required": [ + "router_lifetime", + "type" + ] + }, + { + "type": "object", + "properties": { + "ip": { + "description": "IP address for numbered BGP peers.", + "type": "string", + "format": "ip" + }, + "type": { + "type": "string", + "enum": [ + "numbered" + ] + } + }, + "required": [ + "ip", + "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": "A SAML identity provider", + "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" + ] + }, + "SiloSubnetPool": { + "description": "A subnet 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, external subnet allocations will come from that pool when no other pool is specified.\n\nA silo can have at most one default pool per IP version (IPv4 or IPv6), allowing up to 2 default pools total.", + "type": "boolean" + }, + "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", + "ip_version", + "is_default", + "name", + "time_created", + "time_modified" + ] + }, + "SiloSubnetPoolResultsPage": { + "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/SiloSubnetPool" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "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 the total resources allocated by the silo, including CPU and memory for running instances and storage for disks and snapshots.\n\nNote that CPU and memory resources associated with 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" + ] + }, + "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" + ] + } + ] + }, + "SledUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::SledUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "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" + ] + }, + "SubnetPool": { + "description": "A pool of subnets for external subnet allocation", + "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 this pool", + "allOf": [ + { + "$ref": "#/components/schemas/IpVersion" + } + ] + }, + "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", + "ip_version", + "name", + "time_created", + "time_modified" + ] + }, + "SubnetPoolCreate": { + "description": "Create a subnet pool", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "ip_version": { + "description": "The IP version for this pool (IPv4 or IPv6). All subnets in the pool must match this version.", + "allOf": [ + { + "$ref": "#/components/schemas/IpVersion" + } + ] + }, + "name": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "description", + "ip_version", + "name" + ] + }, + "SubnetPoolLinkSilo": { + "description": "Link a subnet pool to a silo", + "type": "object", + "properties": { + "is_default": { + "description": "Whether this is the default subnet pool for the silo. When true, external subnet allocations that don't specify a pool use this one.", + "type": "boolean" + }, + "silo": { + "description": "The silo to link", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + } + }, + "required": [ + "is_default", + "silo" + ] + }, + "SubnetPoolMember": { + "description": "A member (subnet) within a subnet pool", + "type": "object", + "properties": { + "id": { + "description": "ID of the pool member", + "type": "string", + "format": "uuid" + }, + "max_prefix_length": { + "description": "Maximum prefix length for allocations from this subnet; a larger prefix means smaller allocations are allowed (e.g. a /24 prefix yields smaller subnet allocations than a /16 prefix).", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "min_prefix_length": { + "description": "Minimum prefix length for allocations from this subnet; a smaller prefix means larger allocations are allowed (e.g. a /16 prefix yields larger subnet allocations than a /24 prefix).", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "subnet": { + "description": "The subnet CIDR", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + }, + "subnet_pool_id": { + "description": "ID of the parent subnet pool", + "type": "string", + "format": "uuid" + }, + "time_created": { + "description": "Time the pool member was created.", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "max_prefix_length", + "min_prefix_length", + "subnet", + "subnet_pool_id", + "time_created" + ] + }, + "SubnetPoolMemberAdd": { + "description": "Add a member (subnet) to a subnet pool", + "type": "object", + "properties": { + "max_prefix_length": { + "nullable": true, + "description": "Maximum prefix length for allocations from this subnet; a larger prefix means smaller allocations are allowed (e.g. a /24 prefix yields smaller subnet allocations than a /16 prefix).\n\nValid values: 0-32 for IPv4, 0-128 for IPv6. Default if not specified is 32 for IPv4 and 128 for IPv6.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "min_prefix_length": { + "nullable": true, + "description": "Minimum prefix length for allocations from this subnet; a smaller prefix means larger allocations are allowed (e.g. a /16 prefix yields larger subnet allocations than a /24 prefix).\n\nValid values: 0-32 for IPv4, 0-128 for IPv6. Default if not specified is equal to the subnet's prefix length.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "subnet": { + "description": "The subnet to add to the pool", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + } + }, + "required": [ + "subnet" + ] + }, + "SubnetPoolMemberRemove": { + "description": "Remove a subnet from a pool", + "type": "object", + "properties": { + "subnet": { + "description": "The subnet to remove from the pool. Must match an existing entry exactly.", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + } + }, + "required": [ + "subnet" + ] + }, + "SubnetPoolMemberResultsPage": { + "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/SubnetPoolMember" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "SubnetPoolResultsPage": { + "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/SubnetPool" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "SubnetPoolSiloLink": { + "description": "A link between a subnet pool and a silo", + "type": "object", + "properties": { + "is_default": { + "type": "boolean" + }, + "silo_id": { + "type": "string", + "format": "uuid" + }, + "subnet_pool_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "is_default", + "silo_id", + "subnet_pool_id" + ] + }, + "SubnetPoolSiloLinkResultsPage": { + "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/SubnetPoolSiloLink" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "SubnetPoolSiloUpdate": { + "description": "Update a subnet pool's silo link", + "type": "object", + "properties": { + "is_default": { + "description": "Whether this is the default subnet pool for the silo", + "type": "boolean" + } + }, + "required": [ + "is_default" + ] + }, + "SubnetPoolUpdate": { + "description": "Update a subnet pool", + "type": "object", + "properties": { + "description": { + "nullable": true, + "type": "string" + }, + "name": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + } + }, + "SubnetPoolUtilization": { + "description": "Utilization of addresses in a subnet pool.\n\nNote that both the count of remaining addresses and the total capacity are integers, reported as floating point numbers. This accommodates allocations larger than a 64-bit integer, which is common with IPv6 address spaces. With very large subnet pools (> 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" + ] + }, + "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/SwitchSlot" + } + ] + } + }, + "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/SwitchInterfaceKind" + } + ] + }, + "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" + ] + } + ] + }, + "SwitchLinkState": {}, + "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_slot": { + "description": "The slot of the switch within the rack of this switch port.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchSlot" + } + ] + } + }, + "required": [ + "id", + "port_name", + "rack_id", + "switch_slot" + ] + }, + "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/SwitchPortGeometry" + } + ] + }, + "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" + ] + } + ] + }, + "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/TxEqConfig" + } + ] + } + }, + "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" + } + }, + "required": [ + "addresses", + "bgp_peers", + "description", + "groups", + "id", + "interfaces", + "links", + "name", + "port", + "routes", + "time_created", + "time_modified" + ] + }, + "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" + ] + }, + "SwitchSlot": { + "description": "Identifies switch physical location", + "oneOf": [ + { + "description": "Switch in upper slot", + "type": "string", + "enum": [ + "switch0" + ] + }, + { + "description": "Switch in lower slot", + "type": "string", + "enum": [ + "switch1" + ] + } + ] + }, + "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" + } + } + }, + "UnadoptedPhysicalDisk": { + "description": "A physical disk that has not yet been adopted by the control plane", + "type": "object", + "properties": { + "disk_id": { + "$ref": "#/components/schemas/PhysicalDiskManufacturerIdentity" + }, + "sled_id": { + "$ref": "#/components/schemas/SledUuid" + }, + "slot": { + "type": "integer", + "format": "int64" + }, + "variant": { + "$ref": "#/components/schemas/PhysicalDiskKind" + } + }, + "required": [ + "disk_id", + "sled_id", + "slot", + "variant" + ] + }, + "UnadoptedPhysicalDiskResultsPage": { + "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/UnadoptedPhysicalDisk" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "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" + ] + }, + "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", + "joules" + ] + }, + { + "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 + } + }, + "contact_support": { + "description": "If true, the system has detected one or more known conditions that require Oxide support to resolve\n\nYou should contact support to resolve these issues before proceeding with an update, or after one has completed. The checks underlying this field are not exhaustive, so this being `false` does not mean the entire system is completely healthy.", + "type": "boolean" + }, + "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", + "contact_support", + "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" + }, + "time_created": { + "description": "Timestamp when this user was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "Timestamp when this user was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "display_name", + "id", + "silo_id", + "time_created", + "time_modified" + ] + }, + "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.\n\nNote that CPU and memory resources associated with 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" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "icmp6" + ] + }, + "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 an 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": { + "description": "A list containing the IDs of the secret keys used to sign payloads sent to this receiver.", + "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": "external-subnets", + "description": "External subnets that can be attached to instances.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/external-subnets" + } + }, + { + "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": "ip-pools", + "description": "IP pools are collections of external IPs that can be allocated and attached to instances.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/ip-pools" + } + }, + { + "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": "subnet-pools", + "description": "Subnet pools are collections of external subnets that can be allocated and attached to instances.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/subnet-pools" + } + }, + { + "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. Linking a pool to a silo makes it available for allocation by users in that silo.", + "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/subnet-pools", + "description": "Subnet pools are collections of external subnets. Linking a pool to a silo makes it available for allocation by users in that silo.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/system-subnet-pools" + } + }, + { + "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 07096de5e65..5fcbbd7037e 120000 --- a/openapi/nexus/nexus-latest.json +++ b/openapi/nexus/nexus-latest.json @@ -1 +1 @@ -nexus-2026052200.0.0-c91da3.json \ No newline at end of file +nexus-2026052600.0.0-77f1e4.json \ No newline at end of file diff --git a/openapi/sled-agent/sled-agent-42.0.0-7cdb41.json b/openapi/sled-agent/sled-agent-42.0.0-7cdb41.json new file mode 100644 index 00000000000..0dae4599fd3 --- /dev/null +++ b/openapi/sled-agent/sled-agent-42.0.0-7cdb41.json @@ -0,0 +1,11328 @@ +{ + "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": "42.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" + } + } + } + }, + "/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": { + "post": { + "summary": "Create a local storage dataset", + "operationId": "local_storage_dataset_ensure", + "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", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocalStorageDatasetDeleteRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/network-bootstore-config": { + "put": { + "operationId": "write_network_bootstore_config", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WriteNetworkConfigRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/networking/mcast-fwd": { + "get": { + "summary": "List multicast forwarding entries present on this sled.", + "operationId": "list_mcast_fwd", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_McastForwardingEntry", + "type": "array", + "items": { + "$ref": "#/components/schemas/McastForwardingEntry" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Set multicast forwarding entries for an underlay address.", + "operationId": "set_mcast_fwd", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/McastForwardingEntry" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Clear multicast forwarding entries for an underlay address.", + "operationId": "clear_mcast_fwd", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClearMcastForwarding" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/networking/mcast-m2p": { + "get": { + "summary": "List M2P mappings present on this sled.", + "operationId": "list_mcast_m2p", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_Mcast2PhysMapping", + "type": "array", + "items": { + "$ref": "#/components/schemas/Mcast2PhysMapping" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Set a multicast-to-physical (M2P) mapping in OPTE.", + "operationId": "set_mcast_m2p", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Mcast2PhysMapping" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Clear a multicast-to-physical (M2P) mapping in OPTE.", + "operationId": "clear_mcast_m2p", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClearMcast2Phys" + } + } + }, + "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" + } + } + } + }, + "/rot/{rot}/attest": { + "post": { + "summary": "Return attestation over recorded measurments and nonce from the RoT.", + "operationId": "rot_attest", + "parameters": [ + { + "in": "path", + "name": "rot", + "description": "Which RoT to use", + "required": true, + "schema": { + "$ref": "#/components/schemas/Rot" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Nonce" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Attestation" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/rot/{rot}/certificate-chain": { + "get": { + "summary": "Return the certificate chain for the attestation signer from the RoT.", + "operationId": "rot_certificate_chain", + "parameters": [ + { + "in": "path", + "name": "rot", + "description": "Which RoT to use", + "required": true, + "schema": { + "$ref": "#/components/schemas/Rot" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CertificateChain" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/rot/{rot}/measurement-log": { + "get": { + "summary": "Return the set of measurments recorded by the RoT.", + "operationId": "rot_measurement_log", + "parameters": [ + { + "in": "path", + "name": "rot", + "description": "Which RoT to use", + "required": true, + "schema": { + "$ref": "#/components/schemas/Rot" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MeasurementLog" + } + } + } + }, + "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/network-config": { + "get": { + "summary": "Get the current network config from trust quorum", + "operationId": "trust_quorum_network_config_get", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TrustQuorumNetworkConfig" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Update the network config in trust quorum", + "operationId": "trust_quorum_network_config_put", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TrustQuorumNetworkConfig" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "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/status": { + "get": { + "summary": "Get the status of this trust quorum node", + "operationId": "trust_quorum_status", + "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}/attached-subnets": { + "put": { + "summary": "Update the subnets attached to an instance.", + "operationId": "vmm_put_attached_subnets", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AttachedSubnets" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "summary": "Attach a subnet to an instance.", + "operationId": "vmm_post_attached_subnet", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AttachedSubnet" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Delete all subnets attached to an instance.", + "operationId": "vmm_delete_attached_subnets", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/vmms/{propolis_id}/attached-subnets/{subnet}": { + "delete": { + "summary": "Detach a subnet from an instance.", + "operationId": "vmm_delete_attached_subnet", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + }, + { + "in": "path", + "name": "subnet", + "required": true, + "schema": { + "$ref": "#/components/schemas/IpNet" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "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/InstanceMulticastMembership" + } + } + }, + "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/InstanceMulticastMembership" + } + } + }, + "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": "successful operation", + "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" + ] + }, + "AttachedSubnet": { + "description": "A subnet attached to a single instance.", + "type": "object", + "properties": { + "kind": { + "description": "The kind of subnet that is attached.", + "allOf": [ + { + "$ref": "#/components/schemas/AttachedSubnetKind" + } + ] + }, + "subnet": { + "description": "The IP subnet.", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + } + }, + "required": [ + "kind", + "subnet" + ] + }, + "AttachedSubnetKind": { + "description": "The kind of attached subnet.", + "oneOf": [ + { + "description": "This is a VPC subnet.", + "type": "string", + "enum": [ + "vpc" + ] + }, + { + "description": "This is an external subnet.", + "type": "string", + "enum": [ + "external" + ] + } + ] + }, + "AttachedSubnets": { + "description": "Subnets attached to a single instance.", + "type": "object", + "properties": { + "subnets": { + "title": "IdOrdMap", + "x-rust-type": { + "crate": "iddqd", + "parameters": [ + { + "$ref": "#/components/schemas/AttachedSubnet" + } + ], + "path": "iddqd::IdOrdMap", + "version": "*" + }, + "type": "array", + "items": { + "$ref": "#/components/schemas/AttachedSubnet" + }, + "uniqueItems": true + } + }, + "required": [ + "subnets" + ] + }, + "Attestation": { + "description": "An RoT produced attestation that represents a signature over the provided [`Nonce`] combined with the [`MeasurementLog`] and signed by a key certified by the [`CertificateChain`].", + "oneOf": [ + { + "description": "An Ed25519 signature.", + "type": "object", + "properties": { + "ed25519": { + "type": "string", + "format": "hex string (64 bytes)" + } + }, + "required": [ + "ed25519" + ], + "additionalProperties": false + } + ] + }, + "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/SwitchSlot" + } + }, + "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" + }, + "max_paths": { + "description": "Maximum number of paths to use when multiple \"best paths\" exist", + "default": 1, + "allOf": [ + { + "$ref": "#/components/schemas/MaxPathConfig" + } + ] + }, + "originate": { + "description": "The set of prefixes for the BGP router to originate.", + "type": "array", + "items": { + "$ref": "#/components/schemas/IpNet" + } + }, + "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.", + "allOf": [ + { + "$ref": "#/components/schemas/RouterPeerType" + } + ] + }, + "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 + }, + "BlueprintExternalNetworkingConfig": { + "description": "External networking configuration controlled by Reconfigurator via blueprints.", + "type": "object", + "properties": { + "blueprint_external_networking_generation": { + "description": "The current generation number of the blueprint's external networking config.\n\nThis generation number is only bumped when a new blueprint is produced that changes the external networking configuration in some way.", + "allOf": [ + { + "$ref": "#/components/schemas/Generation" + } + ] + }, + "service_zone_nat_entries": { + "description": "Set of all Omicron service zone NAT entries.", + "allOf": [ + { + "$ref": "#/components/schemas/ServiceZoneNatEntries" + } + ] + } + }, + "required": [ + "blueprint_external_networking_generation", + "service_zone_nat_entries" + ] + }, + "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 + }, + "CertificateChain": { + "description": "A chain of PEM-encoded X.509 certificates (RFC5280) that link an attestation signing key to a trusted PKI root.", + "type": "array", + "items": { + "type": "string" + } + }, + "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" + } + ] + }, + "ClearMcast2Phys": { + "description": "Clear a mapping from an overlay multicast group to an underlay multicast address.", + "type": "object", + "properties": { + "group": { + "description": "Overlay multicast group address.", + "type": "string", + "format": "ip" + }, + "underlay": { + "description": "Underlay IPv6 multicast address. See [`Mcast2PhysMapping::underlay`].", + "type": "string", + "format": "ipv6" + } + }, + "required": [ + "group", + "underlay" + ] + }, + "ClearMcastForwarding": { + "description": "Clear all forwarding entries for an underlay multicast address.", + "type": "object", + "properties": { + "underlay": { + "description": "Underlay IPv6 multicast address. See [`Mcast2PhysMapping::underlay`].", + "type": "string", + "format": "ipv6" + } + }, + "required": [ + "underlay" + ] + }, + "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" + ] + }, + "Component": { + "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/VirtioSocket" + }, + "type": { + "type": "string", + "enum": [ + "virtio_socket" + ] + } + }, + "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 _unencrypted_ 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_unencrypted" + ] + }, + "zpool_id": { + "$ref": "#/components/schemas/ExternalZpoolUuid" + } + }, + "required": [ + "dataset_id", + "type", + "zpool_id" + ] + }, + { + "description": "Delegate a slice of the _encrypted_ 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_encrypted" + ] + }, + "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" + ] + }, + "DiskIdentity": { + "description": "Uniquely identifies a disk.", + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "serial": { + "type": "string" + }, + "vendor": { + "type": "string" + } + }, + "required": [ + "model", + "serial", + "vendor" + ] + }, + "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" + ] + }, + "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": "External IP address configuration.\n\nThis encapsulates all external addresses for an instance, with separate optional configurations for IPv4 and IPv6.", + "type": "object", + "properties": { + "v4": { + "nullable": true, + "description": "IPv4 external IP configuration.", + "allOf": [ + { + "$ref": "#/components/schemas/ExternalIpv4Config" + } + ] + }, + "v6": { + "nullable": true, + "description": "IPv6 external IP configuration.", + "allOf": [ + { + "$ref": "#/components/schemas/ExternalIpv6Config" + } + ] + } + } + }, + "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": { + "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" + }, + "uniqueItems": true + }, + "source_nat": { + "nullable": true, + "description": "Source NAT configuration, for outbound-only connectivity.", + "allOf": [ + { + "$ref": "#/components/schemas/SourceNatConfigV4" + } + ] + } + }, + "required": [ + "floating_ips" + ] + }, + "ExternalIpv6Config": { + "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" + }, + "uniqueItems": true + }, + "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 + }, + "FmdHostCase": { + "description": "A diagnosed fault case from the illumos Fault Management Daemon on a sled.", + "type": "object", + "properties": { + "code": { + "description": "Diagnostic code (e.g. \"PCIEX-8000-DJ\").", + "type": "string" + }, + "event": { + "nullable": true, + "description": "Full fault event payload as JSON, if present. Contains the fault-list with classes, certainties, affected FMRIs, and other diagnostic detail." + }, + "url": { + "description": "URL for human-readable information about this fault (e.g. `http://illumos.org/msg/PCIEX-8000-DJ`).", + "type": "string" + }, + "uuid": { + "description": "Unique identifier for this case.", + "allOf": [ + { + "$ref": "#/components/schemas/FmdHostCaseUuid" + } + ] + } + }, + "required": [ + "code", + "url", + "uuid" + ] + }, + "FmdHostCaseUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::FmdHostCaseUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "FmdInventory": { + "description": "Successfully collected FMD fault data.", + "type": "object", + "properties": { + "cases": { + "title": "IdOrdMap", + "x-rust-type": { + "crate": "iddqd", + "parameters": [ + { + "$ref": "#/components/schemas/FmdHostCase" + } + ], + "path": "iddqd::IdOrdMap", + "version": "*" + }, + "type": "array", + "items": { + "$ref": "#/components/schemas/FmdHostCase" + }, + "uniqueItems": true + }, + "resources": { + "title": "IdOrdMap", + "x-rust-type": { + "crate": "iddqd", + "parameters": [ + { + "$ref": "#/components/schemas/FmdResource" + } + ], + "path": "iddqd::IdOrdMap", + "version": "*" + }, + "type": "array", + "items": { + "$ref": "#/components/schemas/FmdResource" + }, + "uniqueItems": true + } + }, + "required": [ + "cases", + "resources" + ] + }, + "FmdInventoryError": { + "description": "An error reported by sled-agent in place of an [`FmdInventory`].\n\n`kind` is a typed discriminator suitable for filtering / monitoring. `message` is a human-readable description (built via `Display`); it is informational only and should not be parsed.", + "type": "object", + "properties": { + "kind": { + "$ref": "#/components/schemas/FmdInventoryErrorKind" + }, + "message": { + "type": "string" + } + }, + "required": [ + "kind", + "message" + ] + }, + "FmdInventoryErrorKind": { + "description": "Classification of an [`FmdInventoryError`].\n\n`FmdError` is a catch-all for any FMD-side failure: the daemon was unreachable, a case/resource listing failed, or the platform doesn't have FMD at all. The accompanying message disambiguates these cases. `TooManyCases` and `TooManyResources` are first-class because exceeding those bounds is operationally distinct from a transient FMD failure.", + "oneOf": [ + { + "description": "Catch-all for FMD-side failures.", + "type": "string", + "enum": [ + "fmd_error" + ] + }, + { + "description": "Number of FMD cases exceeded [`FMD_MAX_CASES`].", + "type": "string", + "enum": [ + "too_many_cases" + ] + }, + { + "description": "Number of FMD resources exceeded [`FMD_MAX_RESOURCES`].", + "type": "string", + "enum": [ + "too_many_resources" + ] + } + ] + }, + "FmdResource": { + "description": "A resource affected by a diagnosed fault.", + "type": "object", + "properties": { + "case_id": { + "description": "UUID of the case that diagnosed this fault.", + "allOf": [ + { + "$ref": "#/components/schemas/FmdHostCaseUuid" + } + ] + }, + "faulty": { + "description": "Whether the resource is marked faulty.", + "type": "boolean" + }, + "fmri": { + "description": "Fault Management Resource Identifier (e.g. \"dev:////pci@af,0/pci1022,1483@3,5\").", + "type": "string" + }, + "invisible": { + "description": "Whether the resource is marked invisible.", + "type": "boolean" + }, + "unusable": { + "description": "Whether the resource is marked unusable.", + "type": "boolean" + }, + "uuid": { + "description": "Unique identifier for this resource entry.", + "allOf": [ + { + "$ref": "#/components/schemas/FmdResourceUuid" + } + ] + } + }, + "required": [ + "case_id", + "faulty", + "fmri", + "invisible", + "unusable", + "uuid" + ] + }, + "FmdResourceUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::FmdResourceUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "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 + }, + "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": "Name 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/SwitchSlot" + } + ] + }, + "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/LinkFec" + } + ] + }, + "uplink_port_speed": { + "description": "Port speed.", + "allOf": [ + { + "$ref": "#/components/schemas/LinkSpeed" + } + ] + } + }, + "required": [ + "addresses", + "bgp_peers", + "port", + "routes", + "switch", + "uplink_port_speed" + ] + }, + "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.\n\nExtends [`v10::probes::ProbeCreate`] with `multicast_groups` so the probe's OPTE port can be subscribed to multicast groups during probe zone provisioning.\n\nEmpty groups (the default) preserve pre-version behavior. Each entry carries a `group_ip` and an optional source filter (for SSM), matching the instance multicast join.", + "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" + } + ] + }, + "multicast_groups": { + "description": "Multicast groups the probe's OPTE port should subscribe to.", + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/InstanceMulticastMembership" + } + } + }, + "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": "ip" + }, + "infra_ip_last": { + "description": "Last ip address to be used for configuring network infrastructure", + "type": "string", + "format": "ip" + }, + "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" + ] + } + ] + }, + "RouterLifetimeConfig": { + "description": "Router lifetime in seconds for unnumbered BGP peers", + "type": "integer", + "format": "uint16", + "minimum": 0, + "maximum": 9000 + }, + "RouterPeerType": { + "oneOf": [ + { + "type": "object", + "properties": { + "router_lifetime": { + "description": "Router lifetime in seconds for unnumbered BGP peers.", + "allOf": [ + { + "$ref": "#/components/schemas/RouterLifetimeConfig" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "unnumbered" + ] + } + }, + "required": [ + "router_lifetime", + "type" + ] + }, + { + "type": "object", + "properties": { + "ip": { + "description": "IP address for numbered BGP peers.", + "type": "string", + "format": "ip" + }, + "type": { + "type": "string", + "enum": [ + "numbered" + ] + } + }, + "required": [ + "ip", + "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" + ] + }, + "ServiceZoneNatEntries": { + "title": "IdOrdMap", + "description": "Description of all NAT entries for control plane services.", + "x-rust-type": { + "crate": "iddqd", + "parameters": [ + { + "$ref": "#/components/schemas/ServiceZoneNatEntry" + } + ], + "path": "iddqd::IdOrdMap", + "version": "*" + }, + "type": "array", + "items": { + "$ref": "#/components/schemas/ServiceZoneNatEntry" + }, + "uniqueItems": true + }, + "ServiceZoneNatEntry": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/components/schemas/ServiceZoneNatKind" + }, + "nic_mac": { + "$ref": "#/components/schemas/MacAddr" + }, + "sled_underlay_ip": { + "type": "string", + "format": "ipv6" + }, + "vni": { + "$ref": "#/components/schemas/Vni" + }, + "zone_id": { + "$ref": "#/components/schemas/OmicronZoneUuid" + } + }, + "required": [ + "kind", + "nic_mac", + "sled_underlay_ip", + "vni", + "zone_id" + ] + }, + "ServiceZoneNatKind": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "boundary_ntp" + ] + }, + "snat_cfg": { + "$ref": "#/components/schemas/SourceNatConfigGeneric" + } + }, + "required": [ + "kind", + "snat_cfg" + ] + }, + { + "type": "object", + "properties": { + "external_ip": { + "type": "string", + "format": "ip" + }, + "kind": { + "type": "string", + "enum": [ + "external_dns" + ] + } + }, + "required": [ + "external_ip", + "kind" + ] + }, + { + "type": "object", + "properties": { + "external_ip": { + "type": "string", + "format": "ip" + }, + "kind": { + "type": "string", + "enum": [ + "nexus" + ] + } + }, + "required": [ + "external_ip", + "kind" + ] + } + ] + }, + "SingleMeasurementInventory": { + "description": "An attempt at resolving a single measurement file to a valid path", + "type": "object", + "properties": { + "path": { + "type": "string", + "format": "Utf8PathBuf" + }, + "result": { + "$ref": "#/components/schemas/ConfigReconcilerInventoryResult" + } + }, + "required": [ + "path", + "result" + ] + }, + "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" + ] + }, + "SmbiosType1Input": { + "type": "object", + "properties": { + "manufacturer": { + "type": "string" + }, + "product_name": { + "type": "string" + }, + "serial_number": { + "type": "string" + }, + "version": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "manufacturer", + "product_name", + "serial_number", + "version" + ], + "additionalProperties": false + }, + "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" + }, + "SvcEnabledNotOnline": { + "description": "Information about an SMF service that is enabled but not running", + "type": "object", + "properties": { + "fmri": { + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/SvcEnabledNotOnlineState" + }, + "zone": { + "type": "string" + } + }, + "required": [ + "fmri", + "state", + "zone" + ] + }, + "SvcEnabledNotOnlineState": { + "description": "Each service instance is always in a well-defined state based on its dependencies, the results of the execution of its methods, and its potential contracts events.\n\nThis enum contains all possible states except `online`, `disabled`, `uninitialized` and `legacy_run`. We only want to represent states that represent some sort of \"unhealthy\" or \"unexpected\" state. See for more information.", + "oneOf": [ + { + "description": "The instance is enabled, but not yet running or available to run.", + "type": "string", + "enum": [ + "offline" + ] + }, + { + "description": "The instance is enabled and running or available to run. It is, however, functioning at a limited capacity in comparison to normal operation.", + "type": "string", + "enum": [ + "degraded" + ] + }, + { + "description": "The instance is enabled, but not able to run.", + "type": "string", + "enum": [ + "maintenance" + ] + } + ] + }, + "SvcsEnabledNotOnline": { + "description": "Lists services that are enabled but not in an online state if any, the time the sample was collected, and any errors that may have ocurred during the collection", + "type": "object", + "properties": { + "errors": { + "type": "array", + "items": { + "type": "string" + } + }, + "services": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SvcEnabledNotOnline" + } + }, + "time_of_status": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "errors", + "services", + "time_of_status" + ] + }, + "SvcsEnabledNotOnlineResult": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "svcs_enabled_not_online" + ] + }, + "value": { + "$ref": "#/components/schemas/SvcsEnabledNotOnline" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "svcs_cmd_error" + ] + }, + "value": { + "$ref": "#/components/schemas/SvcsError" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "data_unavailable" + ] + } + }, + "required": [ + "type" + ] + } + ] + }, + "SvcsError": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "time_of_status": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "error", + "time_of_status" + ] + }, + "SwitchPorts": { + "description": "A set of switch uplinks.", + "type": "object", + "properties": { + "uplinks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HostPortConfig" + } + } + }, + "required": [ + "uplinks" + ] + }, + "SwitchSlot": { + "description": "Identifies switch physical location", + "oneOf": [ + { + "description": "Switch in upper slot", + "type": "string", + "enum": [ + "switch0" + ] + }, + { + "description": "Switch in lower slot", + "type": "string", + "enum": [ + "switch1" + ] + } + ] + }, + "SystemNetworkingConfig": { + "description": "All configuration needed to set up system-level networking.", + "type": "object", + "properties": { + "blueprint_external_networking_config": { + "nullable": true, + "description": "External networking configuration specified by blueprints.", + "allOf": [ + { + "$ref": "#/components/schemas/BlueprintExternalNetworkingConfig" + } + ] + }, + "rack_network_config": { + "$ref": "#/components/schemas/RackNetworkConfig" + } + }, + "required": [ + "rack_network_config" + ] + }, + "TrustQuorumNetworkConfig": { + "description": "Network configuration used to bring up the control plane.\n\nThis type mirrors `bootstore::schemes::v0::NetworkConfig` but adds `JsonSchema` for API compatibility.", + "type": "object", + "properties": { + "blob": { + "description": "A serialized blob of configuration data (base64 encoded).", + "type": "string" + }, + "generation": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "blob", + "generation" + ] + }, + "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" + } + } + }, + "UplinkAddress": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "addrconf" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "ip_net": { + "$ref": "#/components/schemas/IpNet" + }, + "type": { + "type": "string", + "enum": [ + "static" + ] + } + }, + "required": [ + "ip_net", + "type" + ] + } + ] + }, + "UplinkAddressConfig": { + "type": "object", + "properties": { + "address": { + "description": "The address to be used on the uplink.", + "allOf": [ + { + "$ref": "#/components/schemas/UplinkAddress" + } + ] + }, + "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 + }, + "VirtioSocket": { + "description": "A socket device that presents a virtio-socket interface to the guest.", + "type": "object", + "properties": { + "guest_cid": { + "description": "The guest's Context ID.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pci_path": { + "description": "The PCI path at which to attach this device.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + } + }, + "required": [ + "guest_cid", + "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/InstanceSpec" + } + ] + }, + "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" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "icmp6" + ] + }, + "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" + ] + }, + "WriteNetworkConfigRequest": { + "description": "Structure for requests from Nexus to sled-agent to write a new [`SystemNetworkingConfig`] into the replicated bootstore.\n\n[`WriteNetworkConfigRequest`] INTENTIONALLY does not have a `From` implementation from prior API versions. It is critically important that sled-agent not attempt to rewrite old [`SystemNetworkingConfig`] types to the latest version. For more about this, see the comments on the relevant endpoint in `sled-agent-api`.", + "type": "object", + "properties": { + "body": { + "$ref": "#/components/schemas/SystemNetworkingConfig" + }, + "generation": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "body", + "generation" + ] + }, + "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" + ] + }, + "ZpoolHealth": { + "oneOf": [ + { + "description": "The device is online and functioning.", + "type": "string", + "enum": [ + "online" + ] + }, + { + "description": "One or more components are degraded or faulted, but sufficient replicas exist to continue functioning.", + "type": "string", + "enum": [ + "degraded" + ] + }, + { + "description": "One or more components are degraded or faulted, and insufficient replicas exist to continue functioning.", + "type": "string", + "enum": [ + "faulted" + ] + }, + { + "description": "The device was explicitly taken offline by \"zpool offline\".", + "type": "string", + "enum": [ + "offline" + ] + }, + { + "description": "The device was physically removed.", + "type": "string", + "enum": [ + "removed" + ] + }, + { + "description": "The device could not be opened.", + "type": "string", + "enum": [ + "unavailable" + ] + } + ] + }, + "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" + }, + "Rot": { + "description": "A Root of Trust (RoT) which provides measurments and signed attestations.", + "oneOf": [ + { + "description": "The per-sled RoT in an Oxide rack.", + "type": "string", + "enum": [ + "oxide" + ] + } + ] + }, + "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 4d51056ae30..83232433b94 120000 --- a/openapi/sled-agent/sled-agent-latest.json +++ b/openapi/sled-agent/sled-agent-latest.json @@ -1 +1 @@ -sled-agent-41.0.0-6be62d.json \ No newline at end of file +sled-agent-42.0.0-7cdb41.json \ No newline at end of file diff --git a/schema/crdb/dbinit.sql b/schema/crdb/dbinit.sql index 308379c85c5..5d4e3cbcac4 100644 --- a/schema/crdb/dbinit.sql +++ b/schema/crdb/dbinit.sql @@ -8069,6 +8069,13 @@ CREATE TYPE IF NOT EXISTS omicron.public.multicast_group_member_state AS ENUM ( 'left' ); +CREATE TYPE IF NOT EXISTS omicron.public.multicast_group_member_parent_kind AS ENUM ( + /* A guest instance. */ + 'instance', + /* A probe (network connectivity diagnostic). */ + 'probe' +); + /* * External multicast groups (customer-facing, allocated from IP pools) * Following the bifurcated design from RFD 488 @@ -8174,10 +8181,10 @@ CREATE TABLE IF NOT EXISTS omicron.public.multicast_group_member ( /* External group for customer/external membership */ external_group_id UUID NOT NULL, - /* Parent instance or service (following external_ip pattern) */ + /* Parent instance or probe (following external_ip pattern) */ parent_id UUID NOT NULL, - /* Sled hosting the parent instance (NULL when stopped) */ + /* Sled hosting the parent (NULL when stopped) */ sled_id UUID, /* RPW state for reliable operations */ @@ -8196,7 +8203,10 @@ CREATE TABLE IF NOT EXISTS omicron.public.multicast_group_member ( /* Empty array means any source is allowed (ASM) */ /* Non-empty array enables source filtering (IGMPv3/MLDv2) */ /* The group's source_ips in API views is the union of all active members */ - source_ips INET[] NOT NULL DEFAULT ARRAY[]::INET[] + source_ips INET[] NOT NULL DEFAULT ARRAY[]::INET[], + + /* Discriminator for `parent_id`. */ + parent_kind omicron.public.multicast_group_member_parent_kind NOT NULL DEFAULT 'instance' ); /* External Multicast Group Indexes */ @@ -8347,10 +8357,13 @@ CREATE INDEX IF NOT EXISTS multicast_member_by_parent_and_group ON omicron.publi external_group_id ) WHERE time_deleted IS NULL; --- Business logic constraint: one instance per group (also serves queries) --- Supports: SELECT ... WHERE external_group_id = ? AND parent_id = ? AND time_deleted IS NULL -CREATE UNIQUE INDEX IF NOT EXISTS multicast_member_unique_parent_per_group ON omicron.public.multicast_group_member ( +-- One parent (instance or probe) per group, keyed by +-- (external_group_id, parent_kind, parent_id) so two members with the same +-- UUID across kinds are not collapsed by the unique constraint. +-- Supports: SELECT ... WHERE external_group_id = ? AND parent_kind = ? AND parent_id = ? AND time_deleted IS NULL +CREATE UNIQUE INDEX IF NOT EXISTS multicast_member_unique_kind_parent_per_group ON omicron.public.multicast_group_member ( external_group_id, + parent_kind, parent_id ) WHERE time_deleted IS NULL; @@ -8623,7 +8636,7 @@ INSERT INTO omicron.public.db_metadata ( version, target_version ) VALUES - (TRUE, NOW(), NOW(), '261.0.0', NULL) + (TRUE, NOW(), NOW(), '262.0.0', NULL) ON CONFLICT DO NOTHING; COMMIT; diff --git a/schema/crdb/multicast-member-parent-kind/up01.sql b/schema/crdb/multicast-member-parent-kind/up01.sql new file mode 100644 index 00000000000..017b967a69e --- /dev/null +++ b/schema/crdb/multicast-member-parent-kind/up01.sql @@ -0,0 +1,6 @@ +CREATE TYPE IF NOT EXISTS omicron.public.multicast_group_member_parent_kind AS ENUM ( + /* A guest instance. */ + 'instance', + /* A probe (network connectivity diagnostic). */ + 'probe' +); diff --git a/schema/crdb/multicast-member-parent-kind/up02.sql b/schema/crdb/multicast-member-parent-kind/up02.sql new file mode 100644 index 00000000000..110f8f5d8b4 --- /dev/null +++ b/schema/crdb/multicast-member-parent-kind/up02.sql @@ -0,0 +1,9 @@ +/* Add `parent_kind` to `multicast_group_member` so the reconciler can + dispatch on parent kind without joining the `instance` and `probe` + tables on every row. Probe membership is introduced by this change, + so existing rows are all instance-typed and the backfill default is + 'instance'. */ +ALTER TABLE omicron.public.multicast_group_member + ADD COLUMN IF NOT EXISTS parent_kind + omicron.public.multicast_group_member_parent_kind + NOT NULL DEFAULT 'instance'; diff --git a/schema/crdb/multicast-member-parent-kind/up03.sql b/schema/crdb/multicast-member-parent-kind/up03.sql new file mode 100644 index 00000000000..2ea6bb64578 --- /dev/null +++ b/schema/crdb/multicast-member-parent-kind/up03.sql @@ -0,0 +1,10 @@ +/* The unique identity for a membership row is + `(external_group_id, parent_kind, parent_id)` now that parents may be + either an instance or a probe. Add the kind-aware unique index first so + uniqueness is enforced throughout the swap. */ +CREATE UNIQUE INDEX IF NOT EXISTS multicast_member_unique_kind_parent_per_group + ON omicron.public.multicast_group_member ( + external_group_id, + parent_kind, + parent_id + ) WHERE time_deleted IS NULL; diff --git a/schema/crdb/multicast-member-parent-kind/up03.verify.sql b/schema/crdb/multicast-member-parent-kind/up03.verify.sql new file mode 100644 index 00000000000..86c1f6b5d9b --- /dev/null +++ b/schema/crdb/multicast-member-parent-kind/up03.verify.sql @@ -0,0 +1,2 @@ +-- DO NOT EDIT. Generated by test_migration_verification_files. +SELECT CAST(IF((SELECT true WHERE EXISTS (SELECT index_name FROM omicron.crdb_internal.table_indexes WHERE descriptor_name = 'multicast_group_member' AND index_name = 'multicast_member_unique_kind_parent_per_group')),'true','Schema change verification failed: index multicast_member_unique_kind_parent_per_group on table multicast_group_member does not exist') AS BOOL); diff --git a/schema/crdb/multicast-member-parent-kind/up04.sql b/schema/crdb/multicast-member-parent-kind/up04.sql new file mode 100644 index 00000000000..d4eb40ae1ee --- /dev/null +++ b/schema/crdb/multicast-member-parent-kind/up04.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS omicron.public.multicast_group_member@multicast_member_unique_parent_per_group; diff --git a/sled-agent/api/src/lib.rs b/sled-agent/api/src/lib.rs index 17ec2abb23a..bf0ddf1fe98 100644 --- a/sled-agent/api/src/lib.rs +++ b/sled-agent/api/src/lib.rs @@ -42,6 +42,7 @@ api_versions!([ // | example for the next person. // v // (next_int, IDENT), + (42, PROBE_MULTICAST_GROUPS), (41, MCAST_M2P_FORWARDING), (40, ADD_FMD_TO_INVENTORY), (39, BOOTSTORE_SERVICE_NAT_GENERATION), @@ -1570,7 +1571,7 @@ pub trait SledAgentApi { #[endpoint { method = PUT, path = "/probes", - versions = VERSION_ADD_DUAL_STACK_SHARED_NETWORK_INTERFACES.., + versions = VERSION_PROBE_MULTICAST_GROUPS.., }] async fn probes_put( request_context: RequestContext, @@ -1582,6 +1583,22 @@ pub trait SledAgentApi { /// 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. + #[endpoint { + operation_id = "probes_put", + method = PUT, + path = "/probes", + versions = + VERSION_ADD_DUAL_STACK_SHARED_NETWORK_INTERFACES..VERSION_PROBE_MULTICAST_GROUPS, + }] + async fn probes_put_v10( + request_context: RequestContext, + body: TypedBody, + ) -> Result { + let body = body.map(latest::probes::ProbeSet::from); + Self::probes_put(request_context, body).await + } + + /// Update the entire set of probe zones on this sled. #[endpoint { operation_id = "probes_put", method = PUT, @@ -1593,8 +1610,8 @@ pub trait SledAgentApi { request_context: RequestContext, body: TypedBody, ) -> Result { - let body = body.try_map(latest::probes::ProbeSet::try_from)?; - Self::probes_put(request_context, body).await + let body = body.try_map(v10::probes::ProbeSet::try_from)?; + Self::probes_put_v10(request_context, body).await } /// Create a local storage dataset diff --git a/sled-agent/src/probe_manager.rs b/sled-agent/src/probe_manager.rs index 38c0d0eba06..81d10688c02 100644 --- a/sled-agent/src/probe_manager.rs +++ b/sled-agent/src/probe_manager.rs @@ -31,6 +31,7 @@ use sled_agent_resolvable_files::ramdisk_file_source; use sled_agent_types::instance::ExternalIpConfig; use sled_agent_types::instance::ExternalIpv4Config; use sled_agent_types::instance::ExternalIpv6Config; +use sled_agent_types::instance::InstanceMulticastMembership; use sled_agent_types::instance::ResolvedVpcFirewallRule; use sled_agent_types::inventory::NetworkInterface; use sled_agent_types::probes::ExternalIp; @@ -153,6 +154,10 @@ struct ProbeState { /// If we've built this object from a request through the sled-agent API, /// then we always have this. interface: Option, + /// Multicast groups the probe's OPTE port should subscribe to, + /// fixed at probe zone provisioning. Empty when reconstructing + /// state from an existing zone. + multicast_groups: Vec, } impl IdHashItem for ProbeState { @@ -172,6 +177,7 @@ impl From for ProbeState { status: zone::State::Running, external_ips: params.external_ips, interface: Some(params.interface), + multicast_groups: params.multicast_groups, } } } @@ -204,6 +210,7 @@ impl TryFrom for ProbeState { status: value.state(), external_ips: Vec::new(), interface: None, + multicast_groups: Vec::new(), }) } } @@ -382,7 +389,14 @@ impl ProbeManagerInner { // but probes are supposed to mimic instances as closely as // possible. We should consider if we want to support them here. attached_subnets: vec![], - multicast_groups: &[], + multicast_groups: &probe + .multicast_groups + .iter() + .map(|m| illumos_utils::opte::MulticastGroupCfg { + group_ip: m.group_ip, + sources: m.sources.clone(), + }) + .collect::>(), })?; let installed_zone = ZoneBuilderFactory::new() @@ -532,6 +546,7 @@ mod test { status: zone::State::Configured, external_ips: Vec::new(), interface: None, + multicast_groups: Vec::new(), }; let mut b = a.clone(); diff --git a/sled-agent/types/versions/src/latest.rs b/sled-agent/types/versions/src/latest.rs index 8d5a4fd19b4..9b43d16308e 100644 --- a/sled-agent/types/versions/src/latest.rs +++ b/sled-agent/types/versions/src/latest.rs @@ -218,8 +218,8 @@ pub mod inventory { pub mod probes { pub use crate::v10::probes::ExternalIp; pub use crate::v10::probes::IpKind; - pub use crate::v10::probes::ProbeCreate; - pub use crate::v10::probes::ProbeSet; + pub use crate::v42::probes::ProbeCreate; + pub use crate::v42::probes::ProbeSet; } pub mod rot { diff --git a/sled-agent/types/versions/src/lib.rs b/sled-agent/types/versions/src/lib.rs index ffce49cf852..f26265e1418 100644 --- a/sled-agent/types/versions/src/lib.rs +++ b/sled-agent/types/versions/src/lib.rs @@ -89,6 +89,8 @@ pub mod v4; pub mod v40; #[path = "mcast_m2p_forwarding/mod.rs"] pub mod v41; +#[path = "probe_multicast_groups/mod.rs"] +pub mod v42; #[path = "add_probe_put_endpoint/mod.rs"] pub mod v6; #[path = "multicast_support/mod.rs"] diff --git a/sled-agent/types/versions/src/probe_multicast_groups/mod.rs b/sled-agent/types/versions/src/probe_multicast_groups/mod.rs new file mode 100644 index 00000000000..a8fef88c0f3 --- /dev/null +++ b/sled-agent/types/versions/src/probe_multicast_groups/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 `PROBE_MULTICAST_GROUPS` of the Sled Agent API. + +pub mod probes; diff --git a/sled-agent/types/versions/src/probe_multicast_groups/probes.rs b/sled-agent/types/versions/src/probe_multicast_groups/probes.rs new file mode 100644 index 00000000000..16093574574 --- /dev/null +++ b/sled-agent/types/versions/src/probe_multicast_groups/probes.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/. + +use iddqd::IdHashItem; +use iddqd::IdHashMap; +use iddqd::id_upcast; +use omicron_uuid_kinds::ProbeUuid; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; + +use crate::v7::instance::InstanceMulticastMembership; +use crate::v10; +use crate::v10::inventory::NetworkInterface; +use crate::v10::probes::ExternalIp; + +/// Parameters used to create a probe. +/// +/// Extends [`v10::probes::ProbeCreate`] with `multicast_groups` so the +/// probe's OPTE port can be subscribed to multicast groups during probe +/// zone provisioning. +/// +/// Empty groups (the default) preserve pre-version behavior. Each entry +/// carries a `group_ip` and an optional source filter (for SSM), matching +/// the instance multicast join. +#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] +pub struct ProbeCreate { + /// The ID for the probe. + pub id: ProbeUuid, + /// The external IP addresses assigned to the probe. + pub external_ips: Vec, + /// The probe's networking interface. + pub interface: NetworkInterface, + /// Multicast groups the probe's OPTE port should subscribe to. + #[serde(default)] + pub multicast_groups: Vec, +} + +impl IdHashItem for ProbeCreate { + type Key<'a> = ProbeUuid; + + fn key(&self) -> Self::Key<'_> { + self.id + } + + id_upcast!(); +} + +/// A set of probes that the target sled should run. +#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] +pub struct ProbeSet { + /// The exact set of probes to run. + pub probes: IdHashMap, +} + +impl From for ProbeCreate { + fn from(old: v10::probes::ProbeCreate) -> Self { + Self { + id: old.id, + external_ips: old.external_ips, + interface: old.interface, + multicast_groups: Vec::new(), + } + } +} + +impl From for ProbeSet { + fn from(old: v10::probes::ProbeSet) -> Self { + Self { probes: old.probes.into_iter().map(ProbeCreate::from).collect() } + } +}