From 2fe0ed7e614bd02aabfd94715ffa21f986728bb6 Mon Sep 17 00:00:00 2001 From: Benjamin Naecker Date: Thu, 11 Dec 2025 19:11:16 +0000 Subject: [PATCH 1/2] Support dual-stack NICs in the public API - Move IP traits and config types to common modules so they can be used in the public API - Add private IP stack types to external API, used when fetching NICs - Add new `IpConfig` type to the parameters for creating NICs. This gives a more flexible scheme for selecting an IP address automatically or explicitly, and independently for each IP stack. - Add a new version of the external API for the new types, and implement conversions from the previous - Ensure external IPs are allocated for NICs with a corresponding private IP stack, checking at both IP creation and attachment. --- Cargo.lock | 3 + common/src/address.rs | 45 + common/src/api/external/mod.rs | 116 +- .../api/internal/shared/external_ip/mod.rs | 54 +- end-to-end-tests/src/instance_launch.rs | 2 +- nexus/db-model/src/network_interface.rs | 274 +- nexus/db-queries/src/db/collection_attach.rs | 282 +- .../deployment/external_networking.rs | 12 +- nexus/db-queries/src/db/datastore/disk.rs | 1 + .../src/db/datastore/external_ip.rs | 39 +- .../src/db/datastore/network_interface.rs | 4 +- nexus/db-queries/src/db/datastore/probe.rs | 4 +- nexus/db-queries/src/db/datastore/rack.rs | 8 +- nexus/db-queries/src/db/datastore/vpc.rs | 6 +- .../db-queries/src/db/queries/external_ip.rs | 239 +- .../src/db/queries/network_interface.rs | 86 +- nexus/external-api/Cargo.toml | 3 + nexus/external-api/src/lib.rs | 143 +- nexus/external-api/src/v2025112000.rs | 96 +- nexus/external-api/src/v2025121800.rs | 474 + .../background/tasks/abandoned_vmm_reaper.rs | 2 +- .../tasks/instance_reincarnation.rs | 4 +- nexus/src/app/external_ip.rs | 10 +- nexus/src/app/instance.rs | 8 +- nexus/src/app/ip_pool.rs | 25 - nexus/src/app/network_interface.rs | 38 +- nexus/src/app/sagas/instance_common.rs | 4 +- nexus/src/app/sagas/instance_create.rs | 254 +- nexus/src/app/sagas/instance_delete.rs | 6 +- nexus/src/app/sagas/instance_ip_attach.rs | 26 +- nexus/src/app/sagas/instance_migrate.rs | 4 +- nexus/src/app/sagas/instance_start.rs | 6 +- nexus/src/app/sagas/instance_update/mod.rs | 4 +- .../app/sagas/multicast_group_dpd_ensure.rs | 4 +- nexus/src/app/sagas/snapshot_create.rs | 4 +- nexus/src/app/sagas/vpc_create.rs | 4 +- nexus/src/app/sagas/vpc_subnet_create.rs | 4 +- nexus/src/app/sagas/vpc_subnet_delete.rs | 4 +- nexus/src/app/sagas/vpc_subnet_update.rs | 4 +- nexus/src/external_api/http_entrypoints.rs | 14 +- nexus/test-utils/src/resource_helpers.rs | 32 +- nexus/tests/integration_tests/affinity.rs | 12 +- nexus/tests/integration_tests/audit_log.rs | 6 +- .../crucible_replacements.rs | 4 +- nexus/tests/integration_tests/disks.rs | 6 +- nexus/tests/integration_tests/endpoints.rs | 10 +- nexus/tests/integration_tests/external_ips.rs | 458 +- nexus/tests/integration_tests/instances.rs | 518 +- .../integration_tests/internet_gateway.rs | 5 +- .../tests/integration_tests/local_storage.rs | 4 +- nexus/tests/integration_tests/metrics.rs | 6 +- .../tests/integration_tests/multicast/api.rs | 10 +- .../multicast/authorization.rs | 26 +- .../multicast/cache_invalidation.rs | 8 +- .../integration_tests/multicast/enablement.rs | 4 +- .../integration_tests/multicast/failures.rs | 14 +- .../integration_tests/multicast/groups.rs | 16 +- .../integration_tests/multicast/instances.rs | 22 +- .../tests/integration_tests/multicast/mod.rs | 4 +- .../multicast/networking_integration.rs | 18 +- nexus/tests/integration_tests/pantry.rs | 4 +- nexus/tests/integration_tests/probe.rs | 4 +- nexus/tests/integration_tests/projects.rs | 6 +- nexus/tests/integration_tests/quotas.rs | 2 +- nexus/tests/integration_tests/schema.rs | 2 +- nexus/tests/integration_tests/sleds.rs | 4 +- nexus/tests/integration_tests/snapshots.rs | 4 +- .../integration_tests/subnet_allocation.rs | 26 +- nexus/tests/integration_tests/utilization.rs | 9 +- .../integration_tests/volume_management.rs | 4 +- nexus/tests/integration_tests/vpc_routers.rs | 10 +- nexus/tests/integration_tests/vpc_subnets.rs | 4 +- nexus/tests/integration_tests/vpcs.rs | 10 +- nexus/types/src/external_api/params.rs | 289 +- .../nexus/nexus-2025121800.0.0-c18c45.json | 29598 ++++++++++++++++ openapi/nexus/nexus-latest.json | 2 +- sled-agent/src/services.rs | 2 +- 77 files changed, 32641 insertions(+), 842 deletions(-) create mode 100644 nexus/external-api/src/v2025121800.rs create mode 100644 openapi/nexus/nexus-2025121800.0.0-c18c45.json diff --git a/Cargo.lock b/Cargo.lock index fcf081ea019..6642f4be106 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6901,18 +6901,21 @@ name = "nexus-external-api" version = "0.1.0" dependencies = [ "anyhow", + "api_identity", "chrono", "dropshot", "dropshot-api-manager-types", "http", "hyper", "ipnetwork", + "itertools 0.14.0", "nexus-types", "omicron-common", "omicron-uuid-kinds", "omicron-workspace-hack", "openapiv3", "oximeter-types 0.1.0", + "oxnet", "oxql-types", "schemars 0.8.22", "scim2-rs", diff --git a/common/src/address.rs b/common/src/address.rs index 94ff1aa9d43..87a6701aec9 100644 --- a/common/src/address.rs +++ b/common/src/address.rs @@ -878,6 +878,51 @@ impl Iterator for IpRangeIter { } } +/// Trait for any IP address type. +pub trait Ip: + Clone + + Copy + + std::fmt::Debug + + Diffable + + Eq + + JsonSchema + + std::hash::Hash + + PartialOrd + + PartialEq + + Ord + + Serialize +{ +} +impl Ip for Ipv4Addr {} +impl Ip for Ipv6Addr {} +impl Ip for IpAddr {} + +/// An IP address of a specific version, IPv4 or IPv6. +pub trait ConcreteIp: Ip { + fn into_ipaddr(self) -> IpAddr; + fn into_ipnet(self) -> ipnetwork::IpNetwork; +} + +impl ConcreteIp for Ipv4Addr { + fn into_ipaddr(self) -> IpAddr { + IpAddr::V4(self) + } + + fn into_ipnet(self) -> ipnetwork::IpNetwork { + ipnetwork::IpNetwork::V4(ipnetwork::Ipv4Network::from(self)) + } +} + +impl ConcreteIp for Ipv6Addr { + fn into_ipaddr(self) -> IpAddr { + IpAddr::V6(self) + } + + fn into_ipnet(self) -> ipnetwork::IpNetwork { + ipnetwork::IpNetwork::V6(ipnetwork::Ipv6Network::from(self)) + } +} + #[cfg(test)] mod test { use serde_json::json; diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index 90e13c47c2e..85bf5d45e0e 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -27,6 +27,7 @@ use omicron_uuid_kinds::InstanceUuid; use omicron_uuid_kinds::SledUuid; use oxnet::IpNet; use oxnet::Ipv4Net; +use oxnet::Ipv6Net; use parse_display::Display; use parse_display::FromStr; use rand::Rng; @@ -43,6 +44,7 @@ use std::fmt::Formatter; use std::fmt::Result as FormatResult; use std::net::IpAddr; use std::net::Ipv4Addr; +use std::net::Ipv6Addr; use std::num::ParseIntError; use std::num::{NonZeroU16, NonZeroU32}; use std::ops::Deref; @@ -2608,18 +2610,118 @@ pub struct InstanceNetworkInterface { /// The MAC address assigned to this interface. pub mac: MacAddr, - /// The IP address assigned to this interface. - // TODO-correctness: We need to split this into an optional V4 and optional - // V6 address, at least one of which must be specified. - pub ip: IpAddr, /// True if this interface is the primary for the instance to which it's /// attached. pub primary: bool, - /// A set of additional networks that this interface may send and + /// The VPC-private IP stack for this interface. + pub ip_stack: PrivateIpStack, +} + +/// The VPC-private IP stack for a network interface. +#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)] +#[serde(rename_all = "snake_case", tag = "type", content = "value")] +pub enum PrivateIpStack { + /// The interface has only an IPv4 stack. + V4(PrivateIpv4Stack), + /// The interface has only an IPv6 stack. + V6(PrivateIpv6Stack), + /// The interface is dual-stack IPv4 and IPv6. + DualStack { v4: PrivateIpv4Stack, v6: PrivateIpv6Stack }, +} + +impl PrivateIpStack { + /// Return the IPv4 stack, if it exists. + pub fn ipv4_stack(&self) -> Option<&PrivateIpv4Stack> { + match self { + PrivateIpStack::V4(v4) | PrivateIpStack::DualStack { v4, .. } => { + Some(v4) + } + PrivateIpStack::V6(_) => None, + } + } + + /// Return the VPC-private IPv4 address, if it exists. + pub fn ipv4_addr(&self) -> Option<&Ipv4Addr> { + self.ipv4_stack().map(|s| &s.ip) + } + + /// Return the IPv6 stack, if it exists. + pub fn ipv6_stack(&self) -> Option<&PrivateIpv6Stack> { + match self { + PrivateIpStack::V6(v6) | PrivateIpStack::DualStack { v6, .. } => { + Some(v6) + } + PrivateIpStack::V4(_) => None, + } + } + + /// Return the VPC-private IPv6 address, if it exists. + pub fn ipv6_addr(&self) -> Option<&Ipv6Addr> { + self.ipv6_stack().map(|s| &s.ip) + } + + /// Return true if this is an IPv4-only stack, and false otherwise. + pub fn is_ipv4_only(&self) -> bool { + matches!(self, PrivateIpStack::V4(_)) + } + + /// Return true if this is an IPv6-only stack, and false otherwise. + pub fn is_ipv6_only(&self) -> bool { + matches!(self, PrivateIpStack::V6(_)) + } + + /// Return true if this is dual-stack, and false otherwise. + pub fn is_dual_stack(&self) -> bool { + matches!(self, PrivateIpStack::DualStack { .. }) + } + + /// Return the IPv4 transit IPs, if they exist. + pub fn ipv4_transit_ips(&self) -> Option<&[Ipv4Net]> { + self.ipv4_stack().map(|c| c.transit_ips.as_slice()) + } + + /// Return the IPv6 transit IPs, if they exist. + pub fn ipv6_transit_ips(&self) -> Option<&[Ipv6Net]> { + self.ipv6_stack().map(|c| c.transit_ips.as_slice()) + } + + /// Return all transit IPs, of any IP version. + pub fn all_transit_ips(&self) -> impl Iterator + '_ { + let v4 = self + .ipv4_transit_ips() + .into_iter() + .flatten() + .copied() + .map(Into::into); + let v6 = self + .ipv6_transit_ips() + .into_iter() + .flatten() + .copied() + .map(Into::into); + v4.chain(v6) + } +} + +/// The VPC-private IPv4 stack for a network interface +#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)] +pub struct PrivateIpv4Stack { + /// The VPC-private IPv4 address for the interface. + pub ip: Ipv4Addr, + /// A set of additional IPv4 networks that this interface may send and /// receive traffic on. - #[serde(default)] - pub transit_ips: Vec, + pub transit_ips: Vec, +} + +/// The VPC-private IPv6 stack for a network interface +#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)] +pub struct PrivateIpv6Stack { + /// The VPC-private IPv6 address for the interface. + pub ip: Ipv6Addr, + /// A set of additional IPv6 networks that this interface may send and + /// receive traffic on. + pub transit_ips: Vec, } #[derive( diff --git a/common/src/api/internal/shared/external_ip/mod.rs b/common/src/api/internal/shared/external_ip/mod.rs index 44f2312c4da..631c7af6eac 100644 --- a/common/src/api/internal/shared/external_ip/mod.rs +++ b/common/src/api/internal/shared/external_ip/mod.rs @@ -6,6 +6,8 @@ pub mod v1; +use crate::address::ConcreteIp; +use crate::address::Ip; use crate::address::NUM_SOURCE_NAT_PORTS; use daft::Diffable; use itertools::Either; @@ -17,28 +19,6 @@ use std::net::IpAddr; use std::net::Ipv4Addr; use std::net::Ipv6Addr; -/// Trait for any IP address type. -/// -/// This is used to constrain the external addressing types below. -pub trait Ip: - Clone - + Copy - + std::fmt::Debug - + Diffable - + Eq - + JsonSchema - + std::hash::Hash - + PartialOrd - + PartialEq - + Ord - + Serialize - + SnatSchema -{ -} -impl Ip for Ipv4Addr {} -impl Ip for Ipv6Addr {} -impl Ip for IpAddr {} - /// Helper trait specifying the name of the JSON Schema for a `SourceNatConfig`. /// /// This exists so we can use a generic type and have the names of the concrete @@ -65,23 +45,6 @@ impl SnatSchema for IpAddr { } } -/// An IP address of a specific version, IPv4 or IPv6. -pub trait ConcreteIp: Ip { - fn into_ipaddr(self) -> IpAddr; -} - -impl ConcreteIp for Ipv4Addr { - fn into_ipaddr(self) -> IpAddr { - IpAddr::V4(self) - } -} - -impl ConcreteIp for Ipv6Addr { - fn into_ipaddr(self) -> IpAddr { - IpAddr::V6(self) - } -} - /// Helper trait specifying the name of the JSON Schema for an /// `ExternalIpConfig` object. /// @@ -140,7 +103,7 @@ pub struct SourceNatConfig { // should be fine as long as we're using JSON or similar formats.) #[derive(Deserialize, JsonSchema)] #[serde(remote = "SourceNatConfig")] -struct SourceNatConfigShadow { +struct SourceNatConfigShadow { /// The external address provided to the instance or service. ip: T, /// The first port used for source NAT, inclusive. @@ -155,7 +118,7 @@ pub type SourceNatConfigGeneric = SourceNatConfig; impl JsonSchema for SourceNatConfig where - T: Ip, + T: Ip + SnatSchema, { fn schema_name() -> String { ::json_schema_name() @@ -170,7 +133,10 @@ where // We implement `Deserialize` manually to add validity checking on the port // range. -impl<'de, T: Ip + Deserialize<'de>> Deserialize<'de> for SourceNatConfig { +impl<'de, T> Deserialize<'de> for SourceNatConfig +where + T: Ip + SnatSchema + Deserialize<'de>, +{ fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, @@ -346,7 +312,7 @@ pub type ExternalIpv6Config = ExternalIps; #[serde(remote = "ExternalIps")] struct ExternalIpsShadow where - T: ConcreteIp, + T: ConcreteIp + SnatSchema + ExternalIpSchema, { /// Source NAT configuration, for outbound-only connectivity. source_nat: Option>, @@ -384,7 +350,7 @@ impl JsonSchema for ExternalIpv6Config { // SNAT, ephemeral, and floating IPs in the input data. impl<'de, T> Deserialize<'de> for ExternalIps where - T: ConcreteIp + Deserialize<'de>, + T: ConcreteIp + SnatSchema + ExternalIpSchema + Deserialize<'de>, { fn deserialize(deserializer: D) -> Result where diff --git a/end-to-end-tests/src/instance_launch.rs b/end-to-end-tests/src/instance_launch.rs index 1d46197cee2..7cd6e97b56a 100644 --- a/end-to-end-tests/src/instance_launch.rs +++ b/end-to-end-tests/src/instance_launch.rs @@ -70,7 +70,7 @@ async fn instance_launch() -> Result<()> { name: disk_name.clone(), }), disks: Vec::new(), - network_interfaces: InstanceNetworkInterfaceAttachment::Default, + network_interfaces: InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![ExternalIpCreate::Ephemeral { pool: None }], user_data: String::new(), ssh_public_keys: Some(vec![oxide_client::types::NameOrId::Name( diff --git a/nexus/db-model/src/network_interface.rs b/nexus/db-model/src/network_interface.rs index 598b78b1681..d1f0c35c31d 100644 --- a/nexus/db-model/src/network_interface.rs +++ b/nexus/db-model/src/network_interface.rs @@ -18,8 +18,12 @@ use nexus_db_schema::schema::instance_network_interface; use nexus_db_schema::schema::network_interface; use nexus_db_schema::schema::service_network_interface; use nexus_types::external_api::params; +use nexus_types::external_api::params::PrivateIpStackCreate; use nexus_types::identity::Resource; use omicron_common::api::external::Error; +use omicron_common::api::external::PrivateIpStack; +use omicron_common::api::external::PrivateIpv4Stack; +use omicron_common::api::external::PrivateIpv6Stack; use omicron_common::api::internal::shared::PrivateIpConfig; use omicron_common::api::internal::shared::PrivateIpv4Config; use omicron_common::api::internal::shared::PrivateIpv6Config; @@ -29,8 +33,6 @@ use omicron_uuid_kinds::InstanceUuid; use omicron_uuid_kinds::OmicronZoneUuid; use omicron_uuid_kinds::VnicUuid; use oxnet::IpNet; -use oxnet::Ipv4Net; -use oxnet::Ipv6Net; use sled_agent_types::inventory::ZoneKind; use std::net::IpAddr; use uuid::Uuid; @@ -421,208 +423,6 @@ impl From for NetworkInterface { } } -mod private { - pub trait IpSealed: Clone + Copy + std::fmt::Debug { - fn into_ipnet(self) -> ipnetwork::IpNetwork; - } - - impl IpSealed for std::net::Ipv4Addr { - fn into_ipnet(self) -> ipnetwork::IpNetwork { - ipnetwork::IpNetwork::V4(ipnetwork::Ipv4Network::from(self)) - } - } - impl IpSealed for std::net::Ipv6Addr { - fn into_ipnet(self) -> ipnetwork::IpNetwork { - ipnetwork::IpNetwork::V6(ipnetwork::Ipv6Network::from(self)) - } - } -} - -pub trait Ip: private::IpSealed {} -impl Ip for T where T: private::IpSealed {} - -/// How an IP address is assigned to an interface. -#[derive(Clone, Copy, Debug, Default)] -pub enum IpAssignment { - /// Automatically assign an IP address. - #[default] - Auto, - /// Explicitly assign a specific address, if available. - Explicit(T), -} - -/// How to assign an IPv4 address. -pub type Ipv4Assignment = IpAssignment; - -/// How to assign an IPv6 address. -pub type Ipv6Assignment = IpAssignment; - -/// Configuration for a network interface's IPv4 addressing. -#[derive(Clone, Debug, Default)] -pub struct Ipv4Config { - /// The VPC-private address to assign to the interface. - pub ip: Ipv4Assignment, - /// Additional IP networks the interface can send / receive on. - pub transit_ips: Vec, -} - -/// Configuration for a network interface's IPv6 addressing. -#[derive(Clone, Debug, Default)] -pub struct Ipv6Config { - /// The VPC-private address to assign to the interface. - pub ip: Ipv6Assignment, - /// Additional IP networks the interface can send / receive on. - pub transit_ips: Vec, -} - -/// Configuration for a network interface's IP addressing. -#[derive(Clone, Debug)] -pub enum IpConfig { - /// The interface has only an IPv4 stack. - V4(Ipv4Config), - /// The interface has only an IPv6 stack. - V6(Ipv6Config), - /// The interface has both an IPv4 and IPv6 stack. - DualStack { v4: Ipv4Config, v6: Ipv6Config }, -} - -impl IpConfig { - /// Construct an IPv4 configuration with no transit IPs. - pub fn from_ipv4(addr: std::net::Ipv4Addr) -> Self { - IpConfig::V4(Ipv4Config { - ip: Ipv4Assignment::Explicit(addr), - transit_ips: vec![], - }) - } - - /// Construct an IP configuration with only an automatic IPv4 address. - pub fn auto_ipv4() -> Self { - IpConfig::V4(Ipv4Config::default()) - } - - /// Return the IPv4 configuration, if it exists. - pub fn ipv4_config(&self) -> Option<&Ipv4Config> { - match self { - IpConfig::V4(v4) | IpConfig::DualStack { v4, .. } => Some(v4), - IpConfig::V6(_) => None, - } - } - - /// Return the IPv4 address assignment. - pub fn ipv4_assignment(&self) -> Option<&Ipv4Assignment> { - self.ipv4_config().map(|v4| &v4.ip) - } - - /// Return the IPv4 address explicitly requested, if one exists. - pub fn ipv4_addr(&self) -> Option<&std::net::Ipv4Addr> { - self.ipv4_assignment().and_then(|assignment| match assignment { - IpAssignment::Auto => None, - IpAssignment::Explicit(addr) => Some(addr), - }) - } - - /// Return the IPv4 transit IPs, if any. - pub fn ipv4_transit_ips(&self) -> Option> { - self.ipv4_config().map(|v4| v4.transit_ips.clone()) - } - - /// Construct an IPv6 configuration with no transit IPs. - pub fn from_ipv6(addr: std::net::Ipv6Addr) -> Self { - IpConfig::V6(Ipv6Config { - ip: Ipv6Assignment::Explicit(addr), - transit_ips: vec![], - }) - } - - /// Construct an IP configuration with only an automatic IPv6 address. - pub fn auto_ipv6() -> Self { - IpConfig::V6(Ipv6Config::default()) - } - - /// Return the IPv6 configuration, if it exists. - pub fn ipv6_config(&self) -> Option<&Ipv6Config> { - match self { - IpConfig::V6(v6) | IpConfig::DualStack { v6, .. } => Some(v6), - IpConfig::V4(_) => None, - } - } - - /// Return the IPv6 address assignment. - pub fn ipv6_assignment(&self) -> Option<&Ipv6Assignment> { - self.ipv6_config().map(|v6| &v6.ip) - } - - /// Return the IPv6 address explicitly requested, if one exists. - pub fn ipv6_addr(&self) -> Option<&std::net::Ipv6Addr> { - self.ipv6_assignment().and_then(|assignment| match assignment { - IpAssignment::Auto => None, - IpAssignment::Explicit(addr) => Some(addr), - }) - } - - /// Return the IPv6 transit IPs, if any. - pub fn ipv6_transit_ips(&self) -> Option> { - self.ipv6_config().map(|v6| v6.transit_ips.clone()) - } - - /// Return the transit IPs requested in this configuration. - pub fn transit_ips(&self) -> Vec { - self.ipv4_transit_ips() - .unwrap_or_default() - .into_iter() - .map(Into::into) - .chain( - self.ipv6_transit_ips() - .unwrap_or_default() - .into_iter() - .map(Into::into), - ) - .collect() - } - - /// Construct a dual-stack IP configuration with explicit IP addresses. - pub fn new_dual_stack( - ipv4: std::net::Ipv4Addr, - ipv6: std::net::Ipv6Addr, - ) -> Self { - IpConfig::DualStack { - v4: Ipv4Config { - ip: Ipv4Assignment::Explicit(ipv4), - transit_ips: Vec::new(), - }, - v6: Ipv6Config { - ip: Ipv6Assignment::Explicit(ipv6), - transit_ips: Vec::new(), - }, - } - } - - /// Construct an IP configuration with both IPv4 / IPv6 addresses and no - /// transit IPs. - pub fn auto_dual_stack() -> Self { - IpConfig::DualStack { - v4: Ipv4Config::default(), - v6: Ipv6Config::default(), - } - } - - /// Return true if this config has any transit IPs - fn has_transit_ips(&self) -> bool { - match self { - IpConfig::V4(Ipv4Config { transit_ips, .. }) => { - !transit_ips.is_empty() - } - IpConfig::V6(Ipv6Config { transit_ips, .. }) => { - !transit_ips.is_empty() - } - IpConfig::DualStack { - v4: Ipv4Config { transit_ips: ipv4_addrs, .. }, - v6: Ipv6Config { transit_ips: ipv6_addrs, .. }, - } => !ipv4_addrs.is_empty() || !ipv6_addrs.is_empty(), - } - } -} - /// A not fully constructed NetworkInterface. It may not yet have an IP /// address allocated. #[derive(Clone, Debug)] @@ -631,7 +431,7 @@ pub struct IncompleteNetworkInterface { pub kind: NetworkInterfaceKind, pub parent_id: Uuid, pub subnet: VpcSubnet, - pub ip_config: IpConfig, + pub ip_config: PrivateIpStackCreate, pub mac: Option, pub slot: Option, } @@ -644,7 +444,7 @@ impl IncompleteNetworkInterface { parent_id: Uuid, subnet: VpcSubnet, identity: external::IdentityMetadataCreateParams, - ip_config: IpConfig, + ip_config: PrivateIpStackCreate, mac: Option, slot: Option, ) -> Result { @@ -704,7 +504,7 @@ impl IncompleteNetworkInterface { instance_id: InstanceUuid, subnet: VpcSubnet, identity: external::IdentityMetadataCreateParams, - ip_config: IpConfig, + ip_config: PrivateIpStackCreate, ) -> Result { Self::new( interface_id, @@ -723,7 +523,7 @@ impl IncompleteNetworkInterface { service_id: Uuid, subnet: VpcSubnet, identity: external::IdentityMetadataCreateParams, - ip_config: IpConfig, + ip_config: PrivateIpStackCreate, mac: external::MacAddr, slot: u8, ) -> Result { @@ -749,7 +549,7 @@ impl IncompleteNetworkInterface { probe_id: Uuid, subnet: VpcSubnet, identity: external::IdentityMetadataCreateParams, - ip_config: IpConfig, + ip_config: PrivateIpStackCreate, mac: Option, ) -> Result { Self::new( @@ -779,28 +579,52 @@ pub struct NetworkInterfaceUpdate { pub transit_ips_v6: Vec, } -impl From for external::InstanceNetworkInterface { - fn from(iface: InstanceNetworkInterface) -> Self { - // TODO-completeness: Support dual-stack in the public API, see - // https://github.com/oxidecomputer/omicron/issues/9248. - let ip = iface.ipv4.expect("only IPv4 addresses").into(); - Self { +impl TryFrom for external::InstanceNetworkInterface { + type Error = external::Error; + + fn try_from(iface: InstanceNetworkInterface) -> Result { + let maybe_ipv4_stack = iface.ipv4.map(|ip| PrivateIpv4Stack { + ip: ip.into(), + transit_ips: iface + .transit_ips_v4 + .iter() + .copied() + .map(Into::into) + .collect(), + }); + let maybe_ipv6_stack = iface.ipv6.map(|ip| PrivateIpv6Stack { + ip: ip.into(), + transit_ips: iface + .transit_ips_v6 + .iter() + .copied() + .map(Into::into) + .collect(), + }); + let ip_stack = match (maybe_ipv4_stack, maybe_ipv6_stack) { + (None, None) => { + return Err(Error::internal_error( + format!( + "Found a NIC in the database without any IP \ + addresses, ID='{}'", + iface.identity.id, + ) + .as_str(), + )); + } + (None, Some(v6)) => PrivateIpStack::V6(v6), + (Some(v4), None) => PrivateIpStack::V4(v4), + (Some(v4), Some(v6)) => PrivateIpStack::DualStack { v4, v6 }, + }; + Ok(Self { identity: iface.identity(), instance_id: iface.instance_id, vpc_id: iface.vpc_id, subnet_id: iface.subnet_id, - ip, + ip_stack, mac: *iface.mac, primary: iface.primary, - // https://github.com/oxidecomputer/omicron/issues/9248 Separate - // these into IP versions. - transit_ips: iface - .transit_ips_v4 - .into_iter() - .map(|v4| v4.0.into()) - .chain(iface.transit_ips_v6.into_iter().map(|v6| v6.0.into())) - .collect(), - } + }) } } diff --git a/nexus/db-queries/src/db/collection_attach.rs b/nexus/db-queries/src/db/collection_attach.rs index 73b256ba428..d039e741472 100644 --- a/nexus/db-queries/src/db/collection_attach.rs +++ b/nexus/db-queries/src/db/collection_attach.rs @@ -26,7 +26,7 @@ use diesel::query_builder::*; use diesel::query_dsl::methods as query_methods; use diesel::query_source::Table; use diesel::result::Error as DieselError; -use diesel::sql_types::{BigInt, Nullable, SingleValue}; +use diesel::sql_types::{BigInt, Bool, Nullable, SingleValue}; use nexus_db_lookup::DbConnection; use nexus_db_model::DatastoreAttachTargetConfig; use std::fmt::Debug; @@ -75,6 +75,22 @@ pub(crate) mod aliases { as diesel::Expression>::SqlType; pub type SerializedResourceForeignKey = as diesel::Expression>::SqlType; + + pub trait BooleanCondition: + super::Query + + super::QueryFragment + + Send + + 'static + { + } + + impl BooleanCondition for T where + T: super::Query + + super::QueryFragment + + Send + + 'static + { + } } use aliases::*; @@ -180,6 +196,133 @@ pub trait DatastoreAttachTarget: ExprSqlType: SingleValue, // Necessary to actually select the resource in the output type. ResourceType: Selectable, + { + Self::attach_resource_with_update_condition( + collection_id, + resource_id, + collection_query, + resource_query, + max_attached_resources, + update, + diesel::select(true.into_sql::()), + ) + } + + /// Attach a reesource, conditional on extra user-supplied query results. + /// + /// This method is identical to `attach_resource` in most respects. However, + /// there are some cases where it might not be flexible enough; for example, + /// if you need to attach a resource, conditional on some other resource + /// having some property. + /// + /// Users can supply any executable SQL query that returns a boolean. It + /// _should_ return a single value, but there aren't great ways to express + /// that in Diesel's trait system. The query will be used in a CTE like: + /// + /// ```text + /// WITH user_supplied_update_condition AS ( + /// SELECT COALESCE(EVERY(), FALSE) + /// ) + /// ``` + /// + /// That means: + /// + /// - If the condition query returns no rows, the result is `FALSE`. + /// - If the condition query returns one row, the result is the result of + /// that row. + /// - If the query returns multiple rows, they will be joined with `AND`, so + /// any false row will cause the update not to be applied. + /// + /// # Notes + /// + /// This is a bit of a hacky escape hatch. The `attach_resource` method is + /// pretty strict on its trait bounds (which is good), so we can't supply + /// any queries other than one directly on the resource or collection + /// tables. It also cannot be used in our transaction-retry wrappers, since + /// it returns a special error type that's incompatible with the retry + /// logic. + /// + /// Most folks shouldn't care about either of these, and so should stick + /// with the `attach_resource()` method. But this is a reasonably-safe + /// escape hatch should you need one. + fn attach_resource_with_update_condition( + collection_id: Self::Id, + resource_id: Self::Id, + + collection_query: BoxedQuery>, + resource_query: BoxedQuery>, + + max_attached_resources: u32, + + // We are intentionally picky about this update statement: + // - The second argument - the WHERE clause - must match the default + // for the table. This encourages the "resource_query" filter to be + // used instead, and makes it possible for the CTE to modify the + // filter here (ensuring "resource_id" is selected). + // - Additionally, UpdateStatement's fourth argument defaults to Ret = + // NoReturningClause. This enforces that the given input statement does + // not have a RETURNING clause, and also lets the CTE control this + // value. + update: UpdateStatement< + ResourceTable, + ResourceTableDefaultWhereClause, + V, + >, + + update_condition: Cond, + ) -> AttachToCollectionStatement + where + // Treat the collection and resource as boxed tables. + CollectionTable: BoxableTable, + ResourceTable: BoxableTable, + // Allows treating "collection_exists_query" as a boxed "dyn QueryFragment". + QueryFromClause>: + QueryFragment + Send, + // Allows treating "resource_exists_query" as a boxed "dyn QueryFragment". + QueryFromClause>: + QueryFragment + Send, + // Allows sending "collection_exists_query" between threads. + QuerySqlType>: Send, + // Allows sending "resource_exists_query" between threads. + QuerySqlType>: Send, + // Allows calling ".filter()" on the boxed collection table. + BoxedQuery>: FilterBy, Self::Id>> + + FilterBy>, + // Allows calling ".filter()" on the boxed resource table. + BoxedQuery>: FilterBy, Self::Id>> + + FilterBy> + + FilterBy> + + FilterBy>, + // Allows calling "update.into_boxed()" + UpdateStatement< + ResourceTable, + ResourceTableDefaultWhereClause, + V, + >: BoxableUpdateStatement, V>, + // Allows calling + // ".filter(resource_table().primary_key().eq(resource_id)" on the + // boxed update statement. + BoxedUpdateStatement<'static, Pg, ResourceTable, V>: + FilterBy, Self::Id>>, + // Allows using "id" in expressions (e.g. ".eq(...)") with... + Self::Id: AsExpression< + // ... The Collection table's PK + SerializedCollectionPrimaryKey, + > + AsExpression< + // ... The Resource table's PK + SerializedResourcePrimaryKey, + > + AsExpression< + // ... The Resource table's FK to the Collection table + SerializedResourceForeignKey, + >, + ExprSqlType>: SingleValue, + ExprSqlType>: SingleValue, + ExprSqlType: SingleValue, + // Necessary to actually select the resource in the output type. + ResourceType: Selectable, + // The condition evaluates to a boolean and can be inserted in an actual + // SQL query. + Cond: BooleanCondition, { let collection_table = || as HasTable>::table(); @@ -254,15 +397,17 @@ pub trait DatastoreAttachTarget: .filter(resource_table().primary_key().eq(resource_id)); let resource_returning_clause = ResourceType::as_returning(); + let update_condition = Box::new(update_condition); AttachToCollectionStatement { collection_exists_query, resource_exists_query, resource_count_query, collection_query, resource_query, - max_attached_resources, + max_attached_resources: i64::from(max_attached_resources), update_resource_statement, resource_returning_clause, + update_condition, } } } @@ -289,13 +434,15 @@ where // A (mostly) user-provided query for validating the resource. resource_query: Box + Send>, // The maximum number of resources which may be attached to the collection. - max_attached_resources: u32, + max_attached_resources: i64, // Update statement for the resource. update_resource_statement: BoxedUpdateStatement<'static, Pg, ResourceTable, V>, // Describes what should be returned after UPDATE-ing the resource. resource_returning_clause: AsSelect, + // Extra boolean condition gating the actual update. + update_condition: Box, } impl QueryId @@ -324,7 +471,12 @@ pub enum AttachError { /// The unchanged resource and collection are returned as a part of this /// error; it is the responsibility of the caller to determine which /// condition was not met. - NoUpdate { attached_count: i64, resource: ResourceType, collection: C }, + NoUpdate { + attached_count: i64, + update_condition_satisfied: bool, + resource: ResourceType, + collection: C, + }, /// Other database error DatabaseError(E), } @@ -332,7 +484,7 @@ pub enum AttachError { /// Describes the type returned from the actual CTE, which is parsed /// and interpreted before propagating it to users of the Rust API. pub type RawOutput = - (i64, Option, Option, Option); + (i64, bool, Option, Option, Option); impl AttachToCollectionStatement where @@ -368,6 +520,7 @@ where ) -> Result<(C, ResourceType), AttachError> { let ( attached_count, + update_condition_satisfied, collection_before_update, resource_before_update, resource_after_update, @@ -383,6 +536,7 @@ where Some(resource) => Ok((collection_before_update, resource)), None => Err(AttachError::NoUpdate { attached_count, + update_condition_satisfied, resource: resource_before_update, collection: collection_before_update, }), @@ -402,6 +556,8 @@ where type SqlType = ( // The number of resources attached to the collection before update. BigInt, + // Whether the user-supplied update condition was satisifed. + Bool, // If the collection exists, the value before update. Nullable>, // If the resource exists, the value before update. @@ -427,6 +583,10 @@ where /// the collection exceeds a threshold. /// 3. (collection_info, resource_info): Checks for arbitrary user-provided /// constraints on the collection and resource objects. +/// 4. Check an additional, arbitrary user-supplied update condition. This can +/// be on any tables or any SQL query, as long as it results in a boolean. If +/// the query returns any rows (EXISTS() is TRUE), then the update condition +/// is considered satisfied. /// 4. (do_update): IFF all previous checks succeeded, make a decision to perfom /// an update. /// 5. (updated_resource): Apply user-provided updates on the resource - @@ -467,12 +627,17 @@ where /// // IS NULL AND /// // FOR UPDATE /// // ), +/// // /* Check any user-supplied update conditions. */ +/// // satisfies_update_conditions AS (SELECT COALESCE(EVERY(( +/// // +/// // )), FALSE)), /// // /* Make a decision on whether or not to apply ANY updates */ /// // do_update AS ( /// // SELECT IF( /// // EXISTS(SELECT id FROM collection_info) AND /// // EXISTS(SELECT id FROM resource_info) AND -/// // (SELECT * FROM resource_count) < , +/// // (SELECT * FROM resource_count) < AND +/// // EVERY((SELECT * FROM satisfies_update_conditions)), /// // TRUE, FALSE), /// // ), /// // /* Update the resource */ @@ -483,6 +648,7 @@ where /// // ) /// // SELECT * FROM /// // (SELECT * FROM resource_count) +/// // LEFT JOIN (SELECT * FROM satisfies_update_conditions) ON TRUE /// // LEFT JOIN (SELECT * FROM collection_by_id) ON TRUE /// // LEFT JOIN (SELECT * FROM resource_by_id) ON TRUE /// // LEFT JOIN (SELECT * FROM updated_resource) ON TRUE; @@ -521,14 +687,23 @@ where self.resource_query.walk_ast(out.reborrow())?; out.push_sql(" FOR UPDATE), "); + out.push_sql( + "user_supplied_update_condition AS (SELECT COALESCE(EVERY((", + ); + self.update_condition.walk_ast(out.reborrow())?; + out.push_sql(")), FALSE)), "); + out.push_sql("do_update AS (SELECT IF(EXISTS(SELECT "); out.push_identifier(CollectionIdColumn::::NAME)?; out.push_sql(" FROM collection_info) AND EXISTS(SELECT "); out.push_identifier(ResourceIdColumn::::NAME)?; out.push_sql( - &format!(" FROM resource_info) AND (SELECT * FROM resource_count) < {}, TRUE,FALSE)), ", - self.max_attached_resources) + " FROM resource_info) AND (SELECT * FROM resource_count) < ", ); + out.push_bind_param::(&self.max_attached_resources)?; + out.push_sql( + " AND EVERY((SELECT * FROM user_supplied_update_condition)), TRUE, FALSE)), + "); out.push_sql("updated_resource AS ("); self.update_resource_statement.walk_ast(out.reborrow())?; @@ -561,6 +736,7 @@ where out.push_sql( "SELECT * FROM \ (SELECT * FROM resource_count) \ + LEFT JOIN (SELECT * FROM user_supplied_update_condition) ON TRUE \ LEFT JOIN (SELECT * FROM collection_by_id) ON TRUE \ LEFT JOIN (SELECT * FROM resource_by_id) ON TRUE \ LEFT JOIN (SELECT * FROM updated_resource) ON TRUE;", @@ -827,12 +1003,16 @@ mod test { (\"test_schema\".\"resource\".\"collection_id\" IS NULL)\ ) FOR UPDATE\ ), \ + user_supplied_update_condition AS (\ + SELECT COALESCE(EVERY((SELECT $6)), FALSE) \ + ), \ do_update AS (\ SELECT IF(\ EXISTS(SELECT \"id\" FROM collection_info) AND \ EXISTS(SELECT \"id\" FROM resource_info) AND \ + EVERY((SELECT * FROM user_supplied_update_condition)) AND \ (SELECT * FROM resource_count) < 12345, \ - TRUE,\ + TRUE, \ FALSE)\ ), \ updated_resource AS (\ @@ -854,9 +1034,10 @@ mod test { ) \ SELECT * FROM \ (SELECT * FROM resource_count) \ + LEFT JOIN (SELECT * FROM user_supplied_update_condition) ON TRUE \ LEFT JOIN (SELECT * FROM collection_by_id) ON TRUE \ LEFT JOIN (SELECT * FROM resource_by_id) ON TRUE \ - LEFT JOIN (SELECT * FROM updated_resource) ON TRUE; -- binds: [cccccccc-cccc-cccc-cccc-cccccccccccc, aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa, cccccccc-cccc-cccc-cccc-cccccccccccc, cccccccc-cccc-cccc-cccc-cccccccccccc, aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa, cccccccc-cccc-cccc-cccc-cccccccccccc, aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa]"; + LEFT JOIN (SELECT * FROM updated_resource) ON TRUE; -- binds: [cccccccc-cccc-cccc-cccc-cccccccccccc, aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa, cccccccc-cccc-cccc-cccc-cccccccccccc, cccccccc-cccc-cccc-cccc-cccccccccccc, aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa, true, 12345, cccccccc-cccc-cccc-cccc-cccccccccccc, aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa]"; assert_eq!(query, expected_query); } @@ -1120,8 +1301,14 @@ mod test { let err = attach.expect_err("Should have failed to attach"); match err { - AttachError::NoUpdate { attached_count, resource, collection } => { + AttachError::NoUpdate { + attached_count, + update_condition_satisfied, + resource, + collection, + } => { assert_eq!(attached_count, 1); + assert!(update_condition_satisfied); assert_eq!(resource, get_resource(resource_id2, &conn).await); assert_eq!( collection, @@ -1184,8 +1371,14 @@ mod test { // is already set. This should provide enough context to identify "the // resource is already attached". match err { - AttachError::NoUpdate { attached_count, resource, collection } => { + AttachError::NoUpdate { + attached_count, + update_condition_satisfied, + resource, + collection, + } => { assert_eq!(attached_count, 1); + assert!(update_condition_satisfied); assert_eq!( *resource .collection_id @@ -1218,8 +1411,14 @@ mod test { // Even when at capacity, the same information should be propagated back // to the caller. match err { - AttachError::NoUpdate { attached_count, resource, collection } => { + AttachError::NoUpdate { + attached_count, + update_condition_satisfied, + resource, + collection, + } => { assert_eq!(attached_count, 1); + assert!(update_condition_satisfied); assert_eq!( *resource .collection_id @@ -1388,4 +1587,61 @@ mod test { db.terminate().await; logctx.cleanup_successful(); } + + #[tokio::test] + async fn fail_attach_if_update_condition_not_met() { + let logctx = + dev::test_setup_log("fail_attach_if_user_condition_not_met"); + let db = TestDatabase::new_with_pool(&logctx.log).await; + let pool = db.pool(); + let conn = setup_db(pool).await; + + let collection_id = uuid::Uuid::new_v4(); + let resource_id = uuid::Uuid::new_v4(); + + // Create the collection and resource. + let _collection = + insert_collection(collection_id, "collection", &conn).await; + let _resource = insert_resource(resource_id, "resource", &conn).await; + + // Attach the resource to the collection. + // + // Note that we are also filtering for specific conditions on the + // collection and resource - admittedly, just the name, but this could + // also be used to check the state of a disk, instance, etc. + let attach = Collection::attach_resource_with_update_condition( + collection_id, + resource_id, + collection::table + .filter(collection::name.eq("collection")) + .into_boxed(), + resource::table.filter(resource::name.eq("resource")).into_boxed(), + 10, + // When actually performing the update, update the collection ID + // as well as an auxiliary field - the description. + // + // This provides an example of how one could attach an ID and update + // the state of a resource simultaneously. + diesel::update(resource::table).set(( + resource::dsl::collection_id.eq(collection_id), + resource::dsl::description.eq("new description".to_string()), + )), + diesel::select(false.into_sql::()), + ) + .attach_and_get_result_async(&conn) + .await; + + let err = attach.expect_err("Attach should have failed"); + let AttachError::NoUpdate { update_condition_satisfied, .. } = &err + else { + panic!("Expected a NoUpdate error, found: {:#?}", err); + }; + assert!( + !update_condition_satisfied, + "Update condition should not have been satisfied" + ); + + db.terminate().await; + logctx.cleanup_successful(); + } } diff --git a/nexus/db-queries/src/db/datastore/deployment/external_networking.rs b/nexus/db-queries/src/db/datastore/deployment/external_networking.rs index dbb879a3c3c..a8c87f3be9f 100644 --- a/nexus/db-queries/src/db/datastore/deployment/external_networking.rs +++ b/nexus/db-queries/src/db/datastore/deployment/external_networking.rs @@ -13,10 +13,10 @@ use crate::db::fixed_data::vpc_subnet::NTP_VPC_SUBNET; use nexus_db_errors::TransactionError; use nexus_db_lookup::DbConnection; use nexus_db_model::IncompleteNetworkInterface; -use nexus_db_model::IpConfig; use nexus_db_model::IpPool; use nexus_types::deployment::BlueprintZoneConfig; use nexus_types::deployment::OmicronZoneExternalIp; +use nexus_types::external_api::params::PrivateIpStackCreate; use omicron_common::api::external::Error; use omicron_common::api::external::IdentityMetadataCreateParams; use omicron_common::api::external::IpVersion; @@ -440,10 +440,14 @@ impl DataStore { } let ip_config = match &nic.ip_config { - PrivateIpConfig::V4(ipv4) => IpConfig::from_ipv4(*ipv4.ip()), - PrivateIpConfig::V6(ipv6) => IpConfig::from_ipv6(*ipv6.ip()), + PrivateIpConfig::V4(ipv4) => { + PrivateIpStackCreate::from_ipv4(*ipv4.ip()) + } + PrivateIpConfig::V6(ipv6) => { + PrivateIpStackCreate::from_ipv6(*ipv6.ip()) + } PrivateIpConfig::DualStack { v4, v6 } => { - IpConfig::new_dual_stack(*v4.ip(), *v6.ip()) + PrivateIpStackCreate::new_dual_stack(*v4.ip(), *v6.ip()) } }; let nic_arg = IncompleteNetworkInterface::new_service( diff --git a/nexus/db-queries/src/db/datastore/disk.rs b/nexus/db-queries/src/db/datastore/disk.rs index a0365aa76e7..1fd16aee11d 100644 --- a/nexus/db-queries/src/db/datastore/disk.rs +++ b/nexus/db-queries/src/db/datastore/disk.rs @@ -937,6 +937,7 @@ impl DataStore { } AttachError::NoUpdate { attached_count, + update_condition_satisfied: _, resource, collection, } => { diff --git a/nexus/db-queries/src/db/datastore/external_ip.rs b/nexus/db-queries/src/db/datastore/external_ip.rs index cb6f55786fd..42a1fbaf98e 100644 --- a/nexus/db-queries/src/db/datastore/external_ip.rs +++ b/nexus/db-queries/src/db/datastore/external_ip.rs @@ -40,6 +40,7 @@ use nexus_db_model::FloatingIpUpdate; use nexus_db_model::Instance; use nexus_db_model::IpAttachState; use nexus_db_model::IpVersion; +use nexus_db_model::NetworkInterfaceKind; use nexus_types::deployment::OmicronZoneExternalIp; use nexus_types::identity::Resource; use omicron_common::api::external::CreateResult; @@ -142,12 +143,17 @@ impl DataStore { return Ok((eip, false)); } let temp_ip = temp_ip?; + let ip_version = match temp_ip.ip { + ipnetwork::IpNetwork::V4(_) => IpVersion::V4, + ipnetwork::IpNetwork::V6(_) => IpVersion::V6, + }; match self .begin_attach_ip( opctx, temp_ip.id, instance_id, + ip_version, IpKind::Ephemeral, creating_instance, ) @@ -427,6 +433,7 @@ impl DataStore { opctx: &OpContext, ip_id: Uuid, instance_id: InstanceUuid, + ip_version: IpVersion, kind: IpKind, creating_instance: bool, ) -> Result, Error> { @@ -436,6 +443,8 @@ impl DataStore { use nexus_db_schema::schema::external_ip::table; use nexus_db_schema::schema::instance::dsl as inst_dsl; use nexus_db_schema::schema::instance::table as inst_table; + use nexus_db_schema::schema::network_interface::dsl as nic_dsl; + use nexus_db_schema::schema::network_interface::table as nic_table; let safe_states = if creating_instance { &SAFE_TO_ATTACH_INSTANCE_STATES_CREATING[..] @@ -443,7 +452,17 @@ impl DataStore { &SAFE_TO_ATTACH_INSTANCE_STATES[..] }; - let query = Instance::attach_resource( + let base_nic_query = nic_table + .into_boxed() + .filter(nic_dsl::parent_id.eq(instance_id.into_untyped_uuid())) + .filter(nic_dsl::time_deleted.is_null()) + .filter(nic_dsl::kind.eq(NetworkInterfaceKind::Instance)); + let has_matching_ip_stack = match ip_version { + IpVersion::V4 => base_nic_query.select(nic_dsl::ip.is_not_null()), + IpVersion::V6 => base_nic_query.select(nic_dsl::ipv6.is_not_null()), + }; + + let query = Instance::attach_resource_with_update_condition( instance_id.into_untyped_uuid(), ip_id, inst_table @@ -461,6 +480,7 @@ impl DataStore { dsl::time_modified.eq(Utc::now()), dsl::state.eq(IpAttachState::Attaching), )), + has_matching_ip_stack, ); let mut do_saga = true; @@ -484,7 +504,7 @@ impl DataStore { ) }) }, - AttachError::NoUpdate { attached_count, resource, collection } => { + AttachError::NoUpdate { attached_count, update_condition_satisfied, resource, collection } => { match resource.state { // Idempotent errors: is in progress or complete for same resource pair -- this is fine. IpAttachState::Attaching if resource.parent_id == Some(instance_id.into_untyped_uuid()) => @@ -525,6 +545,19 @@ impl DataStore { "an instance may not have more than \ {MAX_EXTERNAL_IPS_PER_INSTANCE} external IP addresses", )) + } else if !update_condition_satisfied { + Error::invalid_request(&format!( + "The {} external IP is an IP{} address, but \ + the instance with ID {} does not have a \ + primary network interface with a VPC-private IP{} \ + address. Add a VPC-private IP{} address to the \ + interface, or attach a different IP address", + kind, + ip_version, + instance_id, + ip_version, + ip_version, + )) } else { Error::internal_error(&format!("failed to attach {kind} IP")) } @@ -973,6 +1006,7 @@ impl DataStore { &self, opctx: &OpContext, authz_fip: &authz::FloatingIp, + ip_version: IpVersion, instance_id: InstanceUuid, creating_instance: bool, ) -> UpdateResult<(ExternalIp, bool)> { @@ -988,6 +1022,7 @@ impl DataStore { opctx, authz_fip.id(), instance_id, + ip_version, IpKind::Floating, creating_instance, ) diff --git a/nexus/db-queries/src/db/datastore/network_interface.rs b/nexus/db-queries/src/db/datastore/network_interface.rs index a38d6ef9a53..2b583242958 100644 --- a/nexus/db-queries/src/db/datastore/network_interface.rs +++ b/nexus/db-queries/src/db/datastore/network_interface.rs @@ -1016,7 +1016,7 @@ mod tests { use crate::db::pub_test_utils::TestDatabase; use nexus_config::NUM_INITIAL_RESERVED_IP_ADDRESSES; use nexus_db_fixed_data::vpc_subnet::NEXUS_VPC_SUBNET; - use nexus_db_model::IpConfig; + use nexus_types::external_api::params::PrivateIpStackCreate; use omicron_common::address::NEXUS_OPTE_IPV4_SUBNET; use omicron_test_utils::dev; use std::collections::BTreeSet; @@ -1068,7 +1068,7 @@ mod tests { name: name.parse().unwrap(), description: name, }, - IpConfig::from_ipv4(ip), + PrivateIpStackCreate::from_ipv4(ip), macs.next().unwrap(), 0, ) diff --git a/nexus/db-queries/src/db/datastore/probe.rs b/nexus/db-queries/src/db/datastore/probe.rs index 61157a13f99..90549d7b92f 100644 --- a/nexus/db-queries/src/db/datastore/probe.rs +++ b/nexus/db-queries/src/db/datastore/probe.rs @@ -19,9 +19,9 @@ 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::IpConfig; use nexus_db_model::Probe; use nexus_db_model::VpcSubnet; +use nexus_types::external_api::params::PrivateIpStackCreate; use nexus_types::external_api::shared::ProbeInfo; use nexus_types::identity::Resource; use omicron_common::api::external::CreateResult; @@ -270,7 +270,7 @@ impl super::DataStore { probe.name(), ), }, - IpConfig::auto_ipv4(), + PrivateIpStackCreate::auto_ipv4(), None, //Request MAC address assignment )?; diff --git a/nexus/db-queries/src/db/datastore/rack.rs b/nexus/db-queries/src/db/datastore/rack.rs index b51556e4b34..22a9f3cd2fa 100644 --- a/nexus/db-queries/src/db/datastore/rack.rs +++ b/nexus/db-queries/src/db/datastore/rack.rs @@ -38,7 +38,6 @@ use nexus_db_lookup::DbConnection; use nexus_db_lookup::LookupPath; use nexus_db_model::IncompleteNetworkInterface; use nexus_db_model::InitialDnsGroup; -use nexus_db_model::IpConfig; use nexus_db_model::IpVersion; use nexus_db_model::PasswordHashString; use nexus_db_model::SiloUser; @@ -53,6 +52,7 @@ use nexus_types::deployment::BlueprintZoneType; use nexus_types::deployment::OmicronZoneExternalIp; use nexus_types::deployment::blueprint_zone_type; use nexus_types::external_api::params as external_params; +use nexus_types::external_api::params::PrivateIpStackCreate; use nexus_types::external_api::shared; use nexus_types::external_api::shared::IpRange; use nexus_types::external_api::shared::SiloRole; @@ -541,13 +541,13 @@ impl DataStore { // TODO-completeness: Support dual-stack NICs for services. See // https://github.com/oxidecomputer/omicron/issues/9313. let extract_ip_config = - |nic: &NetworkInterface| -> Result { + |nic: &NetworkInterface| -> Result { match &nic.ip_config { PrivateIpConfig::V4(ipv4) => { - Ok(IpConfig::from_ipv4(*ipv4.ip())) + Ok(PrivateIpStackCreate::from_ipv4(*ipv4.ip())) } PrivateIpConfig::V6(ipv6) => { - Ok(IpConfig::from_ipv6(*ipv6.ip())) + Ok(PrivateIpStackCreate::from_ipv6(*ipv6.ip())) } PrivateIpConfig::DualStack { .. } => { Err(Error::invalid_request( diff --git a/nexus/db-queries/src/db/datastore/vpc.rs b/nexus/db-queries/src/db/datastore/vpc.rs index fe2fb4a5280..40e570c9dde 100644 --- a/nexus/db-queries/src/db/datastore/vpc.rs +++ b/nexus/db-queries/src/db/datastore/vpc.rs @@ -2977,7 +2977,6 @@ mod tests { use nexus_db_fixed_data::silo::DEFAULT_SILO; use nexus_db_fixed_data::vpc_subnet::NEXUS_VPC_SUBNET; use nexus_db_model::IncompleteNetworkInterface; - use nexus_db_model::IpConfig; use nexus_reconfigurator_planning::blueprint_builder::BlueprintBuilder; use nexus_reconfigurator_planning::blueprint_editor::ExternalNetworkingAllocator; use nexus_reconfigurator_planning::planner::Planner; @@ -2991,6 +2990,7 @@ mod tests { use nexus_types::deployment::BlueprintZoneDisposition; use nexus_types::deployment::BlueprintZoneImageSource; use nexus_types::external_api::params; + use nexus_types::external_api::params::PrivateIpStackCreate; use nexus_types::identity::Asset; use omicron_common::api::external; use omicron_common::api::external::Generation; @@ -3303,7 +3303,7 @@ mod tests { name: nic.name.clone(), description: nic.name.to_string(), }, - IpConfig::from_ipv4(*ip), + PrivateIpStackCreate::from_ipv4(*ip), nic.mac, nic.slot, ) @@ -4059,7 +4059,7 @@ mod tests { name: "nic".parse().unwrap(), description: "A NIC...".into(), }, - IpConfig::auto_ipv4(), + PrivateIpStackCreate::auto_ipv4(), ) .unwrap(), ) diff --git a/nexus/db-queries/src/db/queries/external_ip.rs b/nexus/db-queries/src/db/queries/external_ip.rs index 71e595a8ea7..3c2c9dd4ffd 100644 --- a/nexus/db-queries/src/db/queries/external_ip.rs +++ b/nexus/db-queries/src/db/queries/external_ip.rs @@ -67,10 +67,17 @@ const EXTERNAL_IP_FROM_CLAUSE: ExternalIpFromClause = const REALLOCATION_WITH_DIFFERENT_IP_SENTINEL: &'static str = "Reallocation of IP with different value"; +const NIC_IS_MISSING_IP_STACK_SENTINEL: &'static str = "nic-missing-ip-stack"; +const NIC_IS_MISSING_IP_STACK_ERROR_MESSAGE: &'static str = "Cannot assign an external IP address from this IP \ + Pool because the network interface does not have an \ + IP stack of the same IP version as the pool."; /// Translates a generic pool error to an external error. pub fn from_diesel(e: DieselError) -> external::Error { - let sentinels = [REALLOCATION_WITH_DIFFERENT_IP_SENTINEL]; + let sentinels = [ + REALLOCATION_WITH_DIFFERENT_IP_SENTINEL, + NIC_IS_MISSING_IP_STACK_SENTINEL, + ]; if let Some(sentinel) = matches_sentinel(&e, &sentinels) { match sentinel { REALLOCATION_WITH_DIFFERENT_IP_SENTINEL => { @@ -78,6 +85,11 @@ pub fn from_diesel(e: DieselError) -> external::Error { "Re-allocating IP address with a different value", ); } + NIC_IS_MISSING_IP_STACK_SENTINEL => { + return external::Error::invalid_request( + NIC_IS_MISSING_IP_STACK_ERROR_MESSAGE, + ); + } // Fall-through to the generic error conversion. _ => {} } @@ -690,6 +702,53 @@ impl NextExternalIp { ); Ok(()) } + + // Push the subquery that fetches the primary NIC for the object we're + // allocating the address for, and ensures that it has an IP stack with the + // same version as the IP Pool we're selecting the address from. + fn push_ensure_parent_has_matching_ip_stack<'a>( + &'a self, + mut out: AstPass<'_, 'a, Pg>, + parent_id: &'a Uuid, + ) -> QueryResult<()> { + out.push_sql("\ + SELECT CAST(\ + CASE \ + WHEN ip.ip_version = 'v4' AND nic.has_ipv4_stack THEN 'TRUE' \ + WHEN ip.ip_version = 'v6' and nic.has_ipv6_stack THEN 'TRUE' \ + ELSE " + ); + out.push_bind_param::( + NIC_IS_MISSING_IP_STACK_SENTINEL, + )?; + out.push_sql( + " \ + END \ + AS BOOL) \ + FROM \ + (SELECT ip_version FROM ip_pool WHERE id = ", + ); + out.push_bind_param::(self.ip.pool_id())?; + out.push_sql( + " AND time_deleted IS NULL) AS ip \ + CROSS JOIN (\ + SELECT \ + ip IS NOT NULL AS has_ipv4_stack, \ + ipv6 IS NOT NULL AS has_ipv6_stack \ + FROM \ + network_interface \ + WHERE \ + parent_id = ", + ); + out.push_bind_param::(parent_id)?; + out.push_sql( + " \ + AND time_deleted IS NULL \ + AND is_primary\ + ) AS nic", + ); + Ok(()) + } } impl QueryId for NextExternalIp { @@ -783,6 +842,26 @@ impl QueryFragment for NextExternalIp { self.push_update_ip_pool_range_subquery(out.reborrow())?; out.push_sql(") "); + // Push the subquery that ensures that the primary NIC of the object + // we're allocating the address for (instance or service) actually has + // an IP stack of the same version as the IP Pool we're selecting from. + // + // Note that today, Ephemeral and Floating IPs are allocated without a + // parent, and then attached to an instance with + // `DataStore::begin_attach_ip()`. This check is here to ensure we still + // handle SNAT addresses, and as a safeguard in the event that we remove + // the two-step attach process. + if let Some(parent_id) = self.ip.parent_id().as_ref() { + out.push_sql( + ", parent_primary_nic_matches_version AS MATERIALIZED(", + ); + self.push_ensure_parent_has_matching_ip_stack( + out.reborrow(), + parent_id, + )?; + out.push_sql(") "); + } + // Select the contents of the actual record that was created or updated. out.push_sql("SELECT * FROM external_ip"); @@ -810,6 +889,7 @@ mod tests { use crate::db::queries::external_ip::NextExternalIp; use crate::db::raw_query_builder::expectorate_query_contents; use async_bb8_diesel::AsyncRunQueryDsl; + use diesel::sql_types; use diesel::{ExpressionMethods, QueryDsl, SelectableHelper}; use dropshot::test_util::LogContext; use nexus_db_lookup::LookupPath; @@ -821,12 +901,14 @@ mod tests { use nexus_db_model::IpPoolReservationType; use nexus_db_model::IpPoolResource; use nexus_db_model::IpPoolResourceType; - use nexus_db_model::IpVersion; use nexus_db_model::Name; + use nexus_db_model::NetworkInterfaceKind; + use nexus_db_schema::enums::NetworkInterfaceKindEnum; use nexus_types::deployment::OmicronZoneExternalFloatingIp; use nexus_types::deployment::OmicronZoneExternalIp; use nexus_types::deployment::OmicronZoneExternalSnatIp; use nexus_types::external_api::params::InstanceCreate; + use nexus_types::external_api::params::InstanceNetworkInterfaceAttachment; use nexus_types::external_api::shared::IpRange; use nexus_types::inventory::SourceNatConfigGeneric; use omicron_common::address::NUM_SOURCE_NAT_PORTS; @@ -863,12 +945,13 @@ mod tests { range: IpRange, is_default: bool, ) -> (authz::IpPool, IpPool) { + let ip_version = range.version(); let pool = IpPool::new( &IdentityMetadataCreateParams { name: name.parse().unwrap(), description: format!("ip pool {}", name), }, - IpVersion::V4, + ip_version.into(), IpPoolReservationType::ExternalSilos, ); @@ -2232,4 +2315,154 @@ mod tests { context.success().await; } + + #[tokio::test] + async fn cannot_allocate_instance_ip_from_pool_with_different_version() { + let context = TestContext::new( + "cannot_allocate_instance_ip_from_pool_with_different_version", + ) + .await; + + // Create an IPv6 pool, as the default in the silo. + let addrs = [ + Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 0), + Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 0xff), + ]; + let range1 = IpRange::try_from((addrs[0], addrs[0])).unwrap(); + let range2 = IpRange::try_from((addrs[1], addrs[1])).unwrap(); + let (authz_pool, db_pool) = + context.create_ip_pool("default", range1, true).await; + let _ = context + .db + .datastore() + .ip_pool_add_range( + context.db.opctx(), + &authz_pool, + &db_pool, + &range2, + ) + .await + .expect("able to add a second range to the pool"); + + let opctx = context.db.opctx(); + + // Create an instance with only an IPv4 stack. + let instance_id = InstanceUuid::new_v4(); + let project_id = Uuid::new_v4(); + let instance = Instance::new(instance_id, project_id, &InstanceCreate { + identity: IdentityMetadataCreateParams { + name: String::from("inst").parse().unwrap(), + description: String::from("test instance"), + }, + ncpus: InstanceCpuCount(omicron_common::api::external::InstanceCpuCount(1)).into(), + memory: ByteCount(omicron_common::api::external::ByteCount::from_gibibytes_u32(1)).into(), + hostname: "test".parse().unwrap(), + ssh_public_keys: None, + user_data: vec![], + network_interfaces: InstanceNetworkInterfaceAttachment::DefaultIpv4, + external_ips: vec![], + disks: vec![], + boot_disk: None, + cpu_platform: None, + start: false, + auto_restart_policy: Default::default(), + anti_affinity_groups: Vec::new(), + multicast_groups: Vec::new(), + }); + + let conn = context + .db + .datastore() + .pool_connection_authorized(opctx) + .await + .unwrap(); + + use nexus_db_schema::schema::instance::dsl as instance_dsl; + diesel::insert_into(instance_dsl::instance) + .values(instance.clone()) + .execute_async(&*conn) + .await + .expect("Failed to create Instance"); + + // Note that this query relies on the NIC itself also existing, so + // insert that too. This is pretty annoying -- we have to create an + // actual project / VPC / VPC Subnet, because the NetworkInterface type + // isn't `Insertable`. That is intentional, since we use a custom query + // to ensure VPC Subnet IP address constraints. + // + // We could create the project, VPC, and Subnet, but that's all verbose + // and mechanical at this layer in the code. Instead, insert the record + // directly using raw SQL. + let count = diesel::sql_query( + "INSERT INTO omicron.public.network_interface VALUES ( + $1, + $2, + $3, + now(), + now(), + NULL, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + ARRAY[]::INET[], + NULL + );", + ) + .bind::(uuid::uuid!( + "ab09167e-1a3a-45e4-9384-2c54e03a5c8f" + )) + .bind::("net0") + .bind::("description") + .bind::(NetworkInterfaceKind::Instance) + .bind::(instance_id.into_untyped_uuid()) + .bind::(uuid::uuid!( + "ab09167e-1a3a-45e4-9384-2c54e03a5c8f" + )) + .bind::(uuid::uuid!( + "ab09167e-1a3a-45e4-9384-2c54e03a5c8f" + )) + .bind::(0xa84025070707) + .bind::(nexus_db_model::Ipv4Addr::from( + "172.31.0.0".parse::().unwrap(), + )) + .bind::(0) + .bind::(true) + .execute_async(&*conn) + .await + .expect("Insert a NetworkInterface"); + assert_eq!(count, 1, "failed to insert NIC"); + + // This should fail, because the NIC has no private IP stack to NAT the + // external IPv6 address to. + let res = context + .db + .datastore() + .allocate_instance_snat_ip( + context.db.opctx(), + Uuid::new_v4(), + instance_id, + db_pool.id(), + ) + .await; + let Err(e) = &res else { + panic!( + "Expected to fail allocating an IPv4 SNAT address \ + from an IPv6-only pool, found {res:#?}" + ); + }; + let Error::InvalidRequest { message } = &e else { + panic!("Expected an InvalidRequest, found {e:#?}"); + }; + assert_eq!( + message.external_message(), + super::NIC_IS_MISSING_IP_STACK_ERROR_MESSAGE, + ); + + context.success().await; + } } diff --git a/nexus/db-queries/src/db/queries/network_interface.rs b/nexus/db-queries/src/db/queries/network_interface.rs index a5b656010b6..229eccaec6b 100644 --- a/nexus/db-queries/src/db/queries/network_interface.rs +++ b/nexus/db-queries/src/db/queries/network_interface.rs @@ -26,10 +26,13 @@ use ipnetwork::{IpNetwork, Ipv6Network}; use nexus_config::NUM_INITIAL_RESERVED_IP_ADDRESSES; use nexus_db_errors::{ErrorHandler, public_error_from_diesel}; use nexus_db_lookup::DbConnection; -use nexus_db_model::{Ip, IpAssignment, Ipv4Addr, SqlU8}; +use nexus_db_model::Ipv4Addr; +use nexus_db_model::SqlU8; use nexus_db_model::{MAX_NICS_PER_INSTANCE, NetworkInterfaceKind}; use nexus_db_schema::enums::NetworkInterfaceKindEnum; use nexus_db_schema::schema::network_interface::dsl; +use nexus_types::external_api::params::IpAssignment; +use omicron_common::address::ConcreteIp; use omicron_common::api::external::MacAddr; use omicron_common::api::external::{self, Error}; use slog_error_chain::SlogInlineError; @@ -1060,7 +1063,7 @@ where { fn new(assignment: Option<&IpAssignment>, auto: Q) -> Self where - T: Ip, + T: ConcreteIp, { match assignment { None => AutoOrOptionalIp::Nullable(None), @@ -1960,13 +1963,13 @@ mod tests { use dropshot::test_util::LogContext; use model::NetworkInterfaceKind; use nexus_db_lookup::LookupPath; - use nexus_db_model::IpConfig; use nexus_db_model::IpVersion; - use nexus_db_model::Ipv4Assignment; - use nexus_db_model::Ipv4Config; use nexus_types::external_api::params; use nexus_types::external_api::params::InstanceCreate; use nexus_types::external_api::params::InstanceNetworkInterfaceAttachment; + use nexus_types::external_api::params::Ipv4Assignment; + use nexus_types::external_api::params::PrivateIpStackCreate; + use nexus_types::external_api::params::PrivateIpv4StackCreate; use omicron_common::api::external; use omicron_common::api::external::ByteCount; use omicron_common::api::external::Error; @@ -2237,7 +2240,7 @@ mod tests { name: "service-nic".parse().unwrap(), description: String::from("service nic"), }, - IpConfig::from_ipv4(ip), + PrivateIpStackCreate::from_ipv4(ip), MacAddr::random_system(), 0, ) @@ -2309,7 +2312,7 @@ mod tests { name: "interface-a".parse().unwrap(), description: String::from("description"), }, - IpConfig::from_ipv4(requested_ip), + PrivateIpStackCreate::from_ipv4(requested_ip), ) .unwrap(); let err = context.datastore() @@ -2345,11 +2348,11 @@ mod tests { let (requested_ip, ip_config) = match ip_version { IpVersion::V4 => { let addr = context.net1.subnets[0].ipv4_block.nth(5).unwrap(); - (IpAddr::V4(addr), IpConfig::from_ipv4(addr)) + (IpAddr::V4(addr), PrivateIpStackCreate::from_ipv4(addr)) } IpVersion::V6 => { let addr = context.net1.subnets[0].ipv6_block.nth(5).unwrap(); - (IpAddr::V6(addr), IpConfig::from_ipv6(addr)) + (IpAddr::V6(addr), PrivateIpStackCreate::from_ipv6(addr)) } }; let interface = IncompleteNetworkInterface::new_instance( @@ -2409,7 +2412,7 @@ mod tests { name: "interface-b".parse().unwrap(), description: String::from("description"), }, - IpConfig::auto_ipv4(), + PrivateIpStackCreate::auto_ipv4(), ) .unwrap(); let err = context.datastore() @@ -2447,7 +2450,7 @@ mod tests { name: format!("interface-{}", i).parse().unwrap(), description: String::from("description"), }, - IpConfig::auto_ipv4(), + PrivateIpStackCreate::auto_ipv4(), ) .unwrap(); let inserted_interface = context @@ -2497,7 +2500,7 @@ mod tests { name: "interface-c".parse().unwrap(), description: String::from("description"), }, - IpConfig::auto_ipv4(), + PrivateIpStackCreate::auto_ipv4(), ) .unwrap(); let inserted_interface = context @@ -2517,7 +2520,7 @@ mod tests { name: "interface-c".parse().unwrap(), description: String::from("description"), }, - IpConfig::from_ipv4(ip.into()), + PrivateIpStackCreate::from_ipv4(ip.into()), ) .unwrap(); let result = context @@ -2554,7 +2557,7 @@ mod tests { name: "service-nic".parse().unwrap(), description: String::from("service nic"), }, - IpConfig::from_ipv4(ip), + PrivateIpStackCreate::from_ipv4(ip), mac, 0, ) @@ -2594,7 +2597,7 @@ mod tests { name: "service-nic".parse().unwrap(), description: String::from("service nic"), }, - IpConfig::from_ipv4(ip), + PrivateIpStackCreate::from_ipv4(ip), mac, slot, ) @@ -2634,7 +2637,9 @@ mod tests { name: "service-nic".parse().unwrap(), description: String::from("service nic"), }, - IpConfig::from_ipv4(ips.next().expect("exhausted test subnet")), + PrivateIpStackCreate::from_ipv4( + ips.next().expect("exhausted test subnet"), + ), mac, 0, ) @@ -2657,7 +2662,9 @@ mod tests { name: "new-service-nic".parse().unwrap(), description: String::from("new-service nic"), }, - IpConfig::from_ipv4(ips.next().expect("exhausted test subnet")), + PrivateIpStackCreate::from_ipv4( + ips.next().expect("exhausted test subnet"), + ), mac, 0, ) @@ -2713,7 +2720,7 @@ mod tests { name: "service-nic".parse().unwrap(), description: String::from("service nic"), }, - IpConfig::from_ipv4(ip0), + PrivateIpStackCreate::from_ipv4(ip0), next_mac(), 0, ) @@ -2734,7 +2741,7 @@ mod tests { name: "new-service-nic".parse().unwrap(), description: String::from("new-service nic"), }, - IpConfig::from_ipv4(ip1), + PrivateIpStackCreate::from_ipv4(ip1), next_mac(), 0, ) @@ -2767,7 +2774,7 @@ mod tests { name: "interface-c".parse().unwrap(), description: String::from("description"), }, - IpConfig::auto_ipv4(), + PrivateIpStackCreate::auto_ipv4(), ) .unwrap(); let _ = context @@ -2786,7 +2793,7 @@ mod tests { name: "interface-c".parse().unwrap(), description: String::from("description"), }, - IpConfig::auto_ipv4(), + PrivateIpStackCreate::auto_ipv4(), ) .unwrap(); let result = context @@ -2817,7 +2824,7 @@ mod tests { name: "interface-c".parse().unwrap(), description: String::from("description"), }, - IpConfig::auto_ipv4(), + PrivateIpStackCreate::auto_ipv4(), ) .unwrap(); let _ = context @@ -2833,7 +2840,7 @@ mod tests { name: "interface-d".parse().unwrap(), description: String::from("description"), }, - IpConfig::auto_ipv4(), + PrivateIpStackCreate::auto_ipv4(), ) .unwrap(); let result = context @@ -2861,7 +2868,7 @@ mod tests { name: "interface-c".parse().unwrap(), description: String::from("description"), }, - IpConfig::auto_ipv4(), + PrivateIpStackCreate::auto_ipv4(), ) .unwrap(); let _ = context @@ -2903,7 +2910,7 @@ mod tests { name: "interface-c".parse().unwrap(), description: String::from("description"), }, - IpConfig::auto_ipv4(), + PrivateIpStackCreate::auto_ipv4(), ) .unwrap(); let _ = context @@ -2912,9 +2919,10 @@ mod tests { .await .expect("Failed to insert interface"); let expected_address = "172.30.0.5".parse().unwrap(); - for ip_config in - [IpConfig::from_ipv4(expected_address), IpConfig::auto_ipv4()] - { + for ip_config in [ + PrivateIpStackCreate::from_ipv4(expected_address), + PrivateIpStackCreate::auto_ipv4(), + ] { let interface = IncompleteNetworkInterface::new_instance( Uuid::new_v4(), instance_id, @@ -2962,7 +2970,7 @@ mod tests { name: "interface-c".parse().unwrap(), description: String::from("description"), }, - IpConfig::auto_ipv4(), + PrivateIpStackCreate::auto_ipv4(), ) .unwrap(); let _ = context @@ -2991,7 +2999,7 @@ mod tests { name: "interface-d".parse().unwrap(), description: String::from("description"), }, - IpConfig::auto_ipv4(), + PrivateIpStackCreate::auto_ipv4(), ) .unwrap(); let result = context @@ -3024,7 +3032,7 @@ mod tests { name: format!("if{}", i).parse().unwrap(), description: String::from("description"), }, - IpConfig::auto_ipv4(), + PrivateIpStackCreate::auto_ipv4(), ) .unwrap(); let result = context @@ -3110,7 +3118,7 @@ mod tests { name: format!("interface-{}", slot).parse().unwrap(), description: String::from("description"), }, - IpConfig::auto_ipv4(), + PrivateIpStackCreate::auto_ipv4(), ) .unwrap(); let inserted_interface = context @@ -3145,7 +3153,7 @@ mod tests { name: "interface-8".parse().unwrap(), description: String::from("description"), }, - IpConfig::auto_ipv4(), + PrivateIpStackCreate::auto_ipv4(), ) .unwrap(); let result = context @@ -3198,8 +3206,8 @@ mod tests { const N_INSTANCES: usize = 3; let mut instances = Vec::with_capacity(N_INSTANCES); let ip_config = match ip_version { - IpVersion::V4 => IpConfig::auto_ipv4(), - IpVersion::V6 => IpConfig::auto_ipv6(), + IpVersion::V4 => PrivateIpStackCreate::auto_ipv4(), + IpVersion::V6 => PrivateIpStackCreate::auto_ipv6(), }; for _ in 0..N_INSTANCES { let instance = context.create_stopped_instance().await; @@ -3340,7 +3348,7 @@ mod tests { name: "interface-c".parse().unwrap(), description: String::from("description"), }, - IpConfig::from_ipv4(addr), + PrivateIpStackCreate::from_ipv4(addr), ) .unwrap(); let _ = context @@ -3360,7 +3368,7 @@ mod tests { name: "interface-c".parse().unwrap(), description: String::from("description"), }, - IpConfig::auto_ipv4(), + PrivateIpStackCreate::auto_ipv4(), ) .unwrap(); @@ -3430,7 +3438,7 @@ mod tests { "172.16.0.0/16".parse().unwrap(), ]; - let ip_config = IpConfig::V4(Ipv4Config { + let ip_config = PrivateIpStackCreate::V4(PrivateIpv4StackCreate { ip: Ipv4Assignment::Auto, transit_ips: transit_ips.clone(), }); @@ -3485,7 +3493,7 @@ mod tests { name: "net0".parse().unwrap(), description: String::from("description"), }, - IpConfig::auto_ipv6(), + PrivateIpStackCreate::auto_ipv6(), ) .unwrap(); let intf = context diff --git a/nexus/external-api/Cargo.toml b/nexus/external-api/Cargo.toml index 6c4032534c8..bf378ac67b3 100644 --- a/nexus/external-api/Cargo.toml +++ b/nexus/external-api/Cargo.toml @@ -9,18 +9,21 @@ workspace = true [dependencies] anyhow.workspace = true +api_identity.workspace = true chrono.workspace = true dropshot-api-manager-types.workspace = true dropshot.workspace = true http.workspace = true hyper.workspace = true ipnetwork.workspace = true +itertools.workspace = true nexus-types.workspace = true omicron-common.workspace = true omicron-uuid-kinds.workspace = true omicron-workspace-hack.workspace = true openapiv3.workspace = true oximeter-types.workspace = true +oxnet.workspace = true oxql-types.workspace = true schemars.workspace = true scim2-rs.workspace = true diff --git a/nexus/external-api/src/lib.rs b/nexus/external-api/src/lib.rs index 97ab104cef1..3be2b77111b 100644 --- a/nexus/external-api/src/lib.rs +++ b/nexus/external-api/src/lib.rs @@ -33,9 +33,10 @@ use omicron_common::api::external::{ }; use openapiv3::OpenAPI; -/// Copies of data types that changed between versions mod v2025112000; mod v2025120300; +/// Copies of data types that changed between versions +mod v2025121800; api_versions!([ // API versions are in the format YYYYMMDDNN.0.0, defined below as @@ -65,6 +66,7 @@ api_versions!([ // | date-based version should be at the top of the list. // v // (next_yyyymmddnn, IDENT), + (2025121800, DUAL_STACK_NICS), (2025121200, BGP_PEER_COLLISION_STATE), (2025120300, LOCAL_STORAGE), (2025112000, INITIAL), @@ -1601,7 +1603,7 @@ pub trait NexusExternalApi { /// Create instance #[endpoint { - operation_id = "disk_create", + operation_id = "instance_create", method = POST, path = "/v1/instances", tags = ["instances"], @@ -1612,16 +1614,41 @@ pub trait NexusExternalApi { query_params: Query, new_instance: TypedBody, ) -> Result, HttpError> { - Self::instance_create(rqctx, query_params, new_instance.map(Into::into)) - .await + Self::v2025121800_instance_create( + rqctx, + query_params, + new_instance.map(Into::into), + ) + .await } /// Create instance #[endpoint { + operation_id = "instance_create", method = POST, path = "/v1/instances", tags = ["instances"], - versions = VERSION_LOCAL_STORAGE.., + versions = VERSION_LOCAL_STORAGE..VERSION_DUAL_STACK_NICS, + }] + async fn v2025121800_instance_create( + rqctx: RequestContext, + query_params: Query, + new_instance: TypedBody, + ) -> Result, HttpError> { + Self::instance_create( + rqctx, + query_params, + new_instance.try_map(TryInto::try_into)?, + ) + .await + } + + /// Create instance + #[endpoint { + method = POST, + path = "/v1/instances", + tags = ["instances"], + versions = VERSION_DUAL_STACK_NICS.. }] async fn instance_create( rqctx: RequestContext, @@ -2757,17 +2784,44 @@ pub trait NexusExternalApi { method = GET, path = "/v1/network-interfaces", tags = ["instances"], + versions = VERSION_DUAL_STACK_NICS.., }] async fn instance_network_interface_list( rqctx: RequestContext, query_params: Query>, ) -> Result>, HttpError>; + /// List network interfaces + #[endpoint { + method = GET, + operation_id = "instance_network_interface_list", + path = "/v1/network-interfaces", + tags = ["instances"], + versions = ..VERSION_DUAL_STACK_NICS, + }] + async fn v2025121800_instance_network_interface_list( + rqctx: RequestContext, + query_params: Query>, + ) -> Result< + HttpResponseOk>, + HttpError, + > { + let HttpResponseOk(ResultsPage { next_page, items }) = + Self::instance_network_interface_list(rqctx, query_params).await?; + items + .into_iter() + .map(v2025121800::InstanceNetworkInterface::try_from) + .collect::>() + .map(|items| HttpResponseOk(ResultsPage { next_page, items })) + .map_err(HttpError::from) + } + /// Create network interface #[endpoint { method = POST, path = "/v1/network-interfaces", tags = ["instances"], + versions = VERSION_DUAL_STACK_NICS.. }] async fn instance_network_interface_create( rqctx: RequestContext, @@ -2775,6 +2829,34 @@ pub trait NexusExternalApi { interface_params: TypedBody, ) -> Result, HttpError>; + /// Create network interface + #[endpoint { + method = POST, + operation_id = "instance_network_interface_create", + path = "/v1/network-interfaces", + tags = ["instances"], + versions = ..VERSION_DUAL_STACK_NICS, + }] + async fn v2025121800_instance_network_interface_create( + rqctx: RequestContext, + query_params: Query, + interface_params: TypedBody< + v2025121800::InstanceNetworkInterfaceCreate, + >, + ) -> Result< + HttpResponseCreated, + HttpError, + > { + let interface_params = interface_params.try_map(TryInto::try_into)?; + let HttpResponseCreated(nic) = Self::instance_network_interface_create( + rqctx, + query_params, + interface_params, + ) + .await?; + nic.try_into().map(HttpResponseCreated).map_err(HttpError::from) + } + /// Delete network interface /// /// Note that the primary interface for an instance cannot be deleted if there @@ -2797,6 +2879,7 @@ pub trait NexusExternalApi { method = GET, path = "/v1/network-interfaces/{interface}", tags = ["instances"], + versions = VERSION_DUAL_STACK_NICS.. }] async fn instance_network_interface_view( rqctx: RequestContext, @@ -2804,11 +2887,35 @@ pub trait NexusExternalApi { query_params: Query, ) -> Result, HttpError>; + /// Fetch network interface + #[endpoint { + method = GET, + operation_id = "instance_network_interface_view", + path = "/v1/network-interfaces/{interface}", + tags = ["instances"], + versions = ..VERSION_DUAL_STACK_NICS, + }] + async fn v2025121800_instance_network_interface_view( + rqctx: RequestContext, + path_params: Path, + query_params: Query, + ) -> Result, HttpError> + { + let HttpResponseOk(nic) = Self::instance_network_interface_view( + rqctx, + path_params, + query_params, + ) + .await?; + nic.try_into().map(HttpResponseOk).map_err(HttpError::from) + } + /// Update network interface #[endpoint { method = PUT, path = "/v1/network-interfaces/{interface}", tags = ["instances"], + versions = VERSION_DUAL_STACK_NICS.., }] async fn instance_network_interface_update( rqctx: RequestContext, @@ -2817,6 +2924,32 @@ pub trait NexusExternalApi { updated_iface: TypedBody, ) -> Result, HttpError>; + /// Fetch network interface + #[endpoint { + method = PUT, + operation_id = "instance_network_interface_update", + path = "/v1/network-interfaces/{interface}", + tags = ["instances"], + versions = ..VERSION_DUAL_STACK_NICS, + }] + async fn v2025121800_instance_network_interface_update( + rqctx: RequestContext, + path_params: Path, + query_params: Query, + updated_iface: TypedBody, + ) -> Result, HttpError> + { + let updated_iface = updated_iface.try_map(TryInto::try_into)?; + let HttpResponseOk(nic) = Self::instance_network_interface_update( + rqctx, + path_params, + query_params, + updated_iface, + ) + .await?; + nic.try_into().map(HttpResponseOk).map_err(HttpError::from) + } + // External IP addresses for instances /// List external IP addresses diff --git a/nexus/external-api/src/v2025112000.rs b/nexus/external-api/src/v2025112000.rs index 88b6ea7b23f..cdd0568450a 100644 --- a/nexus/external-api/src/v2025112000.rs +++ b/nexus/external-api/src/v2025112000.rs @@ -4,8 +4,14 @@ //! Nexus external types that changed from 2025112000 to 2025120300 +use std::net::IpAddr; + +use crate::v2025121800; use nexus_types::external_api::params; use omicron_common::api::external; +use omicron_common::api::external::IdentityMetadataCreateParams; +use omicron_common::api::external::Name; +use oxnet::IpNet; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; @@ -183,6 +189,86 @@ impl From for params::InstanceDiskAttachment { } } +/// Describes an attachment of an `InstanceNetworkInterface` to an `Instance`, +/// at the time the instance is created. +// NOTE: VPC's are an organizing concept for networking resources, not for +// instances. It's true that all networking resources for an instance must +// belong to a single VPC, but we don't consider instances to be "scoped" to a +// VPC in the same way that they are scoped to projects, for example. +// +// This is slightly different than some other cloud providers, such as AWS, +// which use VPCs as both a networking concept, and a container more similar to +// our concept of a project. One example for why this is useful is that "moving" +// an instance to a new VPC can be done by detaching any interfaces in the +// original VPC and attaching interfaces in the new VPC. +// +// This type then requires the VPC identifiers, exactly because instances are +// _not_ scoped to a VPC, and so the VPC and/or VPC Subnet names are not present +// in the path of endpoints handling instance operations. +#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema)] +#[serde(tag = "type", content = "params", rename_all = "snake_case")] +pub enum InstanceNetworkInterfaceAttachment { + /// Create one or more `InstanceNetworkInterface`s for the `Instance`. + /// + /// If more than one interface is provided, then the first will be + /// designated the primary interface for the instance. + Create(Vec), + + /// The default networking configuration for an instance is to create a + /// single primary interface with an automatically-assigned IP address. The + /// IP will be pulled from the Project's default VPC / VPC Subnet. + #[default] + Default, + + /// No network interfaces at all will be created for the instance. + None, +} + +impl From + for v2025121800::InstanceNetworkInterfaceAttachment +{ + fn from(value: InstanceNetworkInterfaceAttachment) -> Self { + match value { + InstanceNetworkInterfaceAttachment::Create(nics) => { + Self::Create(nics.into_iter().map(Into::into).collect()) + } + InstanceNetworkInterfaceAttachment::Default => Self::Default, + InstanceNetworkInterfaceAttachment::None => Self::None, + } + } +} + +/// Create-time parameters for an `InstanceNetworkInterface` +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct InstanceNetworkInterfaceCreate { + #[serde(flatten)] + pub identity: IdentityMetadataCreateParams, + /// The VPC in which to create the interface. + pub vpc_name: Name, + /// The VPC Subnet in which to create the interface. + pub subnet_name: Name, + /// The IP address for the interface. One will be auto-assigned if not provided. + pub ip: Option, + /// A set of additional networks that this interface may send and + /// receive traffic on. + #[serde(default)] + pub transit_ips: Vec, +} + +impl From + for v2025121800::InstanceNetworkInterfaceCreate +{ + fn from(value: InstanceNetworkInterfaceCreate) -> Self { + Self { + identity: value.identity, + vpc_name: value.vpc_name, + subnet_name: value.subnet_name, + ip: value.ip, + transit_ips: value.transit_ips, + } + } +} + /// Create-time parameters for an `Instance` #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] pub struct InstanceCreate { @@ -211,7 +297,7 @@ pub struct InstanceCreate { /// The network interfaces to be created for this instance. #[serde(default)] - pub network_interfaces: params::InstanceNetworkInterfaceAttachment, + pub network_interfaces: InstanceNetworkInterfaceAttachment, /// The external IP addresses provided to this instance. /// @@ -299,15 +385,15 @@ pub struct InstanceCreate { pub cpu_platform: Option, } -impl From for params::InstanceCreate { - fn from(old: InstanceCreate) -> params::InstanceCreate { - params::InstanceCreate { +impl From for v2025121800::InstanceCreate { + fn from(old: InstanceCreate) -> v2025121800::InstanceCreate { + v2025121800::InstanceCreate { identity: old.identity, ncpus: old.ncpus, memory: old.memory, hostname: old.hostname, user_data: old.user_data, - network_interfaces: old.network_interfaces, + network_interfaces: old.network_interfaces.into(), external_ips: old.external_ips, multicast_groups: old.multicast_groups, disks: old.disks.into_iter().map(Into::into).collect(), diff --git a/nexus/external-api/src/v2025121800.rs b/nexus/external-api/src/v2025121800.rs new file mode 100644 index 00000000000..43a05501886 --- /dev/null +++ b/nexus/external-api/src/v2025121800.rs @@ -0,0 +1,474 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Nexus external types that changed from 2025120300 to 2025121100 + +use api_identity::ObjectIdentity; +use itertools::Either; +use itertools::Itertools as _; +use nexus_types::external_api::params; +use nexus_types::external_api::params::IpAssignment; +use nexus_types::external_api::params::PrivateIpStackCreate; +use nexus_types::external_api::params::PrivateIpv4StackCreate; +use nexus_types::external_api::params::PrivateIpv6StackCreate; +use omicron_common::api::external; +use omicron_common::api::external::IdentityMetadata; +use omicron_common::api::external::IdentityMetadataCreateParams; +use omicron_common::api::external::IdentityMetadataUpdateParams; +use omicron_common::api::external::MacAddr; +use omicron_common::api::external::Name; +use omicron_common::api::external::ObjectIdentity; +use omicron_common::api::external::PrivateIpStack; +use omicron_common::api::external::PrivateIpv4Stack; +use omicron_common::api::external::PrivateIpv6Stack; +use oxnet::IpNet; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use std::net::IpAddr; +use uuid::Uuid; + +/// Describes an attachment of an `InstanceNetworkInterface` to an `Instance`, +/// at the time the instance is created. +// NOTE: VPC's are an organizing concept for networking resources, not for +// instances. It's true that all networking resources for an instance must +// belong to a single VPC, but we don't consider instances to be "scoped" to a +// VPC in the same way that they are scoped to projects, for example. +// +// This is slightly different than some other cloud providers, such as AWS, +// which use VPCs as both a networking concept, and a container more similar to +// our concept of a project. One example for why this is useful is that "moving" +// an instance to a new VPC can be done by detaching any interfaces in the +// original VPC and attaching interfaces in the new VPC. +// +// This type then requires the VPC identifiers, exactly because instances are +// _not_ scoped to a VPC, and so the VPC and/or VPC Subnet names are not present +// in the path of endpoints handling instance operations. +#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema)] +#[serde(tag = "type", content = "params", rename_all = "snake_case")] +pub enum InstanceNetworkInterfaceAttachment { + /// Create one or more `InstanceNetworkInterface`s for the `Instance`. + /// + /// If more than one interface is provided, then the first will be + /// designated the primary interface for the instance. + Create(Vec), + + /// The default networking configuration for an instance is to create a + /// single primary interface with an automatically-assigned IP address. The + /// IP will be pulled from the Project's default VPC / VPC Subnet. + #[default] + Default, + + /// No network interfaces at all will be created for the instance. + None, +} + +impl TryFrom + for params::InstanceNetworkInterfaceAttachment +{ + type Error = external::Error; + + fn try_from( + value: InstanceNetworkInterfaceAttachment, + ) -> Result { + match value { + InstanceNetworkInterfaceAttachment::Create(nics) => nics + .into_iter() + .map(TryInto::try_into) + .collect::>() + .map(Self::Create), + InstanceNetworkInterfaceAttachment::Default => { + Ok(Self::DefaultDualStack) + } + InstanceNetworkInterfaceAttachment::None => Ok(Self::None), + } + } +} + +/// An `InstanceNetworkInterface` represents a virtual network interface device +/// attached to an instance. +#[derive(ObjectIdentity, Clone, Debug, Deserialize, JsonSchema, Serialize)] +pub struct InstanceNetworkInterface { + /// common identifying metadata + #[serde(flatten)] + pub identity: IdentityMetadata, + + /// The Instance to which the interface belongs. + pub instance_id: Uuid, + + /// The VPC to which the interface belongs. + pub vpc_id: Uuid, + + /// The subnet to which the interface belongs. + pub subnet_id: Uuid, + + /// The MAC address assigned to this interface. + pub mac: MacAddr, + + /// The IP address assigned to this interface. + pub ip: IpAddr, + + /// True if this interface is the primary for the instance to which it's + /// attached. + pub primary: bool, + + /// A set of additional networks that this interface may send and + /// receive traffic on. + #[serde(default)] + pub transit_ips: Vec, +} + +impl TryFrom for external::InstanceNetworkInterface { + type Error = external::Error; + + fn try_from(value: InstanceNetworkInterface) -> Result { + let ip_stack = match value.ip { + IpAddr::V4(ip) => { + let transit_ips = value + .transit_ips + .into_iter() + .map(|ipnet| match ipnet { + IpNet::V4(v4) => Ok(v4), + IpNet::V6(_) => Err(external::Error::invalid_request( + "A network interface cannot have an IPv4 \ + address and IPv6 transit IPs", + )), + }) + .collect::>()?; + PrivateIpStack::V4(PrivateIpv4Stack { ip, transit_ips }) + } + IpAddr::V6(ip) => { + let transit_ips = value + .transit_ips + .into_iter() + .map(|ipnet| match ipnet { + IpNet::V6(v6) => Ok(v6), + IpNet::V4(_) => Err(external::Error::invalid_request( + "A network interface cannot have an IPv6 \ + address and IPv4 transit IPs", + )), + }) + .collect::>()?; + PrivateIpStack::V6(PrivateIpv6Stack { ip, transit_ips }) + } + }; + Ok(external::InstanceNetworkInterface { + identity: value.identity, + instance_id: value.instance_id, + vpc_id: value.vpc_id, + subnet_id: value.subnet_id, + mac: value.mac, + primary: value.primary, + ip_stack, + }) + } +} + +impl TryFrom for InstanceNetworkInterface { + type Error = external::Error; + + fn try_from( + value: external::InstanceNetworkInterface, + ) -> Result { + let (ip, transit_ips) = match value.ip_stack { + PrivateIpStack::V4(v4) => ( + v4.ip.into(), + v4.transit_ips.into_iter().map(Into::into).collect(), + ), + PrivateIpStack::V6(v6) => ( + v6.ip.into(), + v6.transit_ips.into_iter().map(Into::into).collect(), + ), + // TODO(ben): Should we just return the V4 addr here? + PrivateIpStack::DualStack { v4, v6 } => { + return Err(external::Error::invalid_request(format!( + "The network interface with ID '{}' is \ + a dual-stack NIC, with IPv4 address '{}' \ + and IPv6 address '{}'. However, the version \ + of the client being used is unable to fully \ + represent both IPv4 and IPv6 addresses. \ + Update your client and retry the request.", + value.identity.id, v4.ip, v6.ip, + ))); + } + }; + Ok(Self { + identity: value.identity, + instance_id: value.instance_id, + vpc_id: value.vpc_id, + subnet_id: value.subnet_id, + mac: value.mac, + ip, + primary: value.primary, + transit_ips, + }) + } +} + +/// Create-time parameters for an `InstanceNetworkInterface` +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct InstanceNetworkInterfaceCreate { + #[serde(flatten)] + pub identity: IdentityMetadataCreateParams, + /// The VPC in which to create the interface. + pub vpc_name: Name, + /// The VPC Subnet in which to create the interface. + pub subnet_name: Name, + /// The IP address for the interface. One will be auto-assigned if not provided. + pub ip: Option, + /// A set of additional networks that this interface may send and + /// receive traffic on. + #[serde(default)] + pub transit_ips: Vec, +} + +impl TryFrom + for params::InstanceNetworkInterfaceCreate +{ + type Error = external::Error; + + fn try_from( + value: InstanceNetworkInterfaceCreate, + ) -> Result { + let (ipv4_transit_ips, ipv6_transit_ips): (Vec<_>, Vec<_>) = + value.transit_ips.into_iter().partition_map(|net| match net { + IpNet::V4(ipv4) => Either::Left(ipv4), + IpNet::V6(ipv6) => Either::Right(ipv6), + }); + if !ipv4_transit_ips.is_empty() && !ipv6_transit_ips.is_empty() { + return Err(external::Error::invalid_request( + "Cannot specify both IPv4 and IPv6 transit IPs", + )); + } + let ip_config = match value.ip { + None => { + if !ipv4_transit_ips.is_empty() { + PrivateIpStackCreate::V4(PrivateIpv4StackCreate { + ip: IpAssignment::Auto, + transit_ips: ipv4_transit_ips, + }) + } else if !ipv6_transit_ips.is_empty() { + PrivateIpStackCreate::V6(PrivateIpv6StackCreate { + ip: IpAssignment::Auto, + transit_ips: ipv6_transit_ips, + }) + } else { + PrivateIpStackCreate::auto_dual_stack() + } + } + Some(IpAddr::V4(ipv4)) => { + if !ipv6_transit_ips.is_empty() { + return Err(external::Error::invalid_request( + "Cannot specify IPv6 transit IPs with an IPv4 address", + )); + } + PrivateIpStackCreate::V4(PrivateIpv4StackCreate { + ip: IpAssignment::Explicit(ipv4), + transit_ips: ipv4_transit_ips, + }) + } + Some(IpAddr::V6(ipv6)) => { + if !ipv4_transit_ips.is_empty() { + return Err(external::Error::invalid_request( + "Cannot specify IPv4 transit IPs with an IPv6 address", + )); + } + PrivateIpStackCreate::V6(PrivateIpv6StackCreate { + ip: IpAssignment::Explicit(ipv6), + transit_ips: ipv6_transit_ips, + }) + } + }; + Ok(Self { + identity: value.identity, + vpc_name: value.vpc_name, + subnet_name: value.subnet_name, + ip_config, + }) + } +} + +/// Create-time parameters for an `Instance` +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct InstanceCreate { + #[serde(flatten)] + pub identity: external::IdentityMetadataCreateParams, + /// The number of vCPUs to be allocated to the instance + pub ncpus: external::InstanceCpuCount, + /// The amount of RAM (in bytes) to be allocated to the instance + pub memory: external::ByteCount, + /// The hostname to be assigned to the instance + pub hostname: external::Hostname, + + /// User data for instance initialization systems (such as cloud-init). + /// Must be a Base64-encoded string, as specified in RFC 4648 § 4 (+ and / + /// characters with padding). Maximum 32 KiB unencoded data. + // While serde happily accepts #[serde(with = "")] as a shorthand for + // specifying `serialize_with` and `deserialize_with`, schemars requires the + // argument to `with` to be a type rather than merely a path prefix (i.e. a + // mod or type). It's admittedly a bit tricky for schemars to address; + // unlike `serialize` or `deserialize`, `JsonSchema` requires several + // functions working together. It's unfortunate that schemars has this + // built-in incompatibility, exacerbated by its glacial rate of progress + // and immunity to offers of help. + #[serde(default, with = "params::UserData")] + pub user_data: Vec, + + /// The network interfaces to be created for this instance. + #[serde(default)] + pub network_interfaces: InstanceNetworkInterfaceAttachment, + + /// The external IP addresses provided to this instance. + /// + /// By default, all instances have outbound connectivity, but no inbound + /// connectivity. These external addresses can be used to provide a fixed, + /// known IP address for making inbound connections to the instance. + #[serde(default)] + pub external_ips: Vec, + + /// The multicast groups this instance should join. + /// + /// The instance will be automatically added as a member of the specified + /// multicast groups during creation, enabling it to send and receive + /// multicast traffic for those groups. + #[serde(default)] + pub multicast_groups: Vec, + + /// A list of disks to be attached to the instance. + /// + /// Disk attachments of type "create" will be created, while those of type + /// "attach" must already exist. + /// + /// The order of this list does not guarantee a boot order for the instance. + /// Use the boot_disk attribute to specify a boot disk. When boot_disk is + /// specified it will count against the disk attachment limit. + #[serde(default)] + pub disks: Vec, + + /// The disk the instance is configured to boot from. + /// + /// This disk can either be attached if it already exists or created along + /// with the instance. + /// + /// Specifying a boot disk is optional but recommended to ensure predictable + /// boot behavior. The boot disk can be set during instance creation or + /// later if the instance is stopped. The boot disk counts against the disk + /// attachment limit. + /// + /// An instance that does not have a boot disk set will use the boot + /// options specified in its UEFI settings, which are controlled by both the + /// instance's UEFI firmware and the guest operating system. Boot options + /// can change as disks are attached and detached, which may result in an + /// instance that only boots to the EFI shell until a boot disk is set. + #[serde(default)] + pub boot_disk: Option, + + /// An allowlist of SSH public keys to be transferred to the instance via + /// cloud-init during instance creation. + /// + /// If not provided, all SSH public keys from the user's profile will be sent. + /// If an empty list is provided, no public keys will be transmitted to the + /// instance. + pub ssh_public_keys: Option>, + + /// Should this instance be started upon creation; true by default. + #[serde(default = "params::bool_true")] + pub start: bool, + + /// The auto-restart policy for this instance. + /// + /// This policy determines whether the instance should be automatically + /// restarted by the control plane on failure. If this is `null`, no + /// auto-restart policy will be explicitly configured for this instance, and + /// the control plane will select the default policy when determining + /// whether the instance can be automatically restarted. + /// + /// Currently, the global default auto-restart policy is "best-effort", so + /// instances with `null` auto-restart policies will be automatically + /// restarted. However, in the future, the default policy may be + /// configurable through other mechanisms, such as on a per-project basis. + /// In that case, any configured default policy will be used if this is + /// `null`. + #[serde(default)] + pub auto_restart_policy: Option, + + /// Anti-Affinity groups which this instance should be added. + #[serde(default)] + pub anti_affinity_groups: Vec, + + /// The CPU platform to be used for this instance. If this is `null`, the + /// instance requires no particular CPU platform; when it is started the + /// instance will have the most general CPU platform supported by the sled + /// it is initially placed on. + #[serde(default)] + pub cpu_platform: Option, +} + +impl TryFrom for params::InstanceCreate { + type Error = external::Error; + + fn try_from(value: InstanceCreate) -> Result { + let network_interfaces = value.network_interfaces.try_into()?; + Ok(Self { + identity: value.identity, + ncpus: value.ncpus, + memory: value.memory, + hostname: value.hostname, + user_data: value.user_data, + network_interfaces, + external_ips: value.external_ips, + multicast_groups: value.multicast_groups, + disks: value.disks, + boot_disk: value.boot_disk, + ssh_public_keys: value.ssh_public_keys, + start: value.start, + auto_restart_policy: value.auto_restart_policy, + anti_affinity_groups: value.anti_affinity_groups, + cpu_platform: value.cpu_platform, + }) + } +} + +/// Parameters for updating an `InstanceNetworkInterface` +/// +/// Note that modifying IP addresses for an interface is not yet supported, a +/// new interface must be created instead. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct InstanceNetworkInterfaceUpdate { + #[serde(flatten)] + pub identity: IdentityMetadataUpdateParams, + + /// Make a secondary interface the instance's primary interface. + /// + /// If applied to a secondary interface, that interface will become the + /// primary on the next reboot of the instance. Note that this may have + /// implications for routing between instances, as the new primary interface + /// will be on a distinct subnet from the previous primary interface. + /// + /// Note that this can only be used to select a new primary interface for an + /// instance. Requests to change the primary interface into a secondary will + /// return an error. + // TODO-completeness TODO-doc When we get there, this should note that a + // change in the primary interface will result in changes to the DNS records + // for the instance, though not the name. + #[serde(default)] + pub primary: bool, + + /// A set of additional networks that this interface may send and + /// receive traffic on. + #[serde(default)] + pub transit_ips: Vec, +} + +impl TryFrom + for params::InstanceNetworkInterfaceUpdate +{ + type Error = external::Error; + + fn try_from( + _value: InstanceNetworkInterfaceUpdate, + ) -> Result { + todo!() + } +} diff --git a/nexus/src/app/background/tasks/abandoned_vmm_reaper.rs b/nexus/src/app/background/tasks/abandoned_vmm_reaper.rs index cc93af086d7..5c0937fad34 100644 --- a/nexus/src/app/background/tasks/abandoned_vmm_reaper.rs +++ b/nexus/src/app/background/tasks/abandoned_vmm_reaper.rs @@ -224,7 +224,7 @@ mod tests { datastore: &Arc, opctx: &OpContext, ) -> Self { - resource_helpers::create_default_ip_pool(&client).await; + resource_helpers::create_default_ip_pools(&client).await; let _project = resource_helpers::create_project(client, PROJECT_NAME).await; diff --git a/nexus/src/app/background/tasks/instance_reincarnation.rs b/nexus/src/app/background/tasks/instance_reincarnation.rs index 38ad6bb4633..d2f4a8b2325 100644 --- a/nexus/src/app/background/tasks/instance_reincarnation.rs +++ b/nexus/src/app/background/tasks/instance_reincarnation.rs @@ -321,7 +321,7 @@ mod test { use nexus_db_model::VmmState; use nexus_db_queries::authz; use nexus_test_utils::resource_helpers::{ - create_default_ip_pool, create_project, object_create, + create_default_ip_pools, create_project, object_create, }; use nexus_test_utils_macros::nexus_test; use omicron_common::api::external::ByteCount; @@ -341,7 +341,7 @@ mod test { cptestctx: &ControlPlaneTestContext, opctx: &OpContext, ) -> authz::Project { - create_default_ip_pool(&cptestctx.external_client).await; + create_default_ip_pools(&cptestctx.external_client).await; let project = create_project(&cptestctx.external_client, PROJECT_NAME).await; diff --git a/nexus/src/app/external_ip.rs b/nexus/src/app/external_ip.rs index 1ec6ff2d8dc..37f91ab846f 100644 --- a/nexus/src/app/external_ip.rs +++ b/nexus/src/app/external_ip.rs @@ -11,6 +11,7 @@ use crate::external_api::views::FloatingIp; use nexus_db_lookup::LookupPath; use nexus_db_lookup::lookup; use nexus_db_model::IpAttachState; +use nexus_db_model::IpVersion; use nexus_db_queries::authz; use nexus_db_queries::context::OpContext; use nexus_types::external_api::params; @@ -168,8 +169,8 @@ impl super::Nexus { target: params::FloatingIpAttach, ) -> UpdateResult { let fip_lookup = self.floating_ip_lookup(opctx, fip_selector)?; - let (.., authz_project, authz_fip) = - fip_lookup.lookup_for(authz::Action::Modify).await?; + let (.., authz_project, authz_fip, db_fip) = + fip_lookup.fetch_for(authz::Action::Modify).await?; match target.kind { params::FloatingIpParentKind::Instance => { @@ -188,10 +189,15 @@ impl super::Nexus { let instance = self.instance_lookup(opctx, instance_selector)?; + let ip_version = match db_fip.ip { + ipnetwork::IpNetwork::V4(_) => IpVersion::V4, + ipnetwork::IpNetwork::V6(_) => IpVersion::V6, + }; self.instance_attach_floating_ip( opctx, &instance, authz_fip, + ip_version, authz_project, ) .await diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 01248f80da1..314cfb97ef1 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -28,6 +28,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::IpVersion; use nexus_db_model::Vmm as DbVmm; use nexus_db_model::VmmRuntimeState; use nexus_db_model::VmmState as DbVmmState; @@ -40,6 +41,7 @@ use nexus_db_queries::db::datastore::InstanceAndActiveVmm; use nexus_db_queries::db::datastore::InstanceStateComputer; use nexus_db_queries::db::identity::Resource; use nexus_types::external_api::views; +use omicron_common::address::ConcreteIp; use omicron_common::api::external::ByteCount; use omicron_common::api::external::CreateResult; use omicron_common::api::external::DataPageParams; @@ -58,7 +60,6 @@ use omicron_common::api::internal::nexus; use omicron_common::api::internal::shared::ExternalIpConfig; use omicron_common::api::internal::shared::ExternalIpConfigBuilder; use omicron_common::api::internal::shared::ExternalIps; -use omicron_common::api::internal::shared::external_ip::ConcreteIp; use omicron_common::api::internal::shared::external_ip::SourceNatConfig; use omicron_uuid_kinds::GenericUuid; use omicron_uuid_kinds::InstanceUuid; @@ -2203,12 +2204,13 @@ impl super::Nexus { .await } - /// Attach an ephemeral IP to an instance. + /// Attach a Floating IP to an instance. pub(crate) async fn instance_attach_floating_ip( self: &Arc, opctx: &OpContext, instance_lookup: &lookup::Instance<'_>, authz_fip: authz::FloatingIp, + ip_version: IpVersion, authz_fip_project: authz::Project, ) -> UpdateResult { let (.., authz_project, authz_instance) = @@ -2224,7 +2226,7 @@ impl super::Nexus { opctx, authz_instance, authz_project.id(), - ExternalIpAttach::Floating { floating_ip: authz_fip }, + ExternalIpAttach::Floating { floating_ip: authz_fip, ip_version }, ) .await } diff --git a/nexus/src/app/ip_pool.rs b/nexus/src/app/ip_pool.rs index a550246aef7..e5cc391494f 100644 --- a/nexus/src/app/ip_pool.rs +++ b/nexus/src/app/ip_pool.rs @@ -152,18 +152,8 @@ impl super::Nexus { opctx: &OpContext, pool_params: ¶ms::IpPoolCreate, ) -> CreateResult { - // https://github.com/oxidecomputer/omicron/issues/8881 let ip_version = pool_params.ip_version.into(); - // IPv6 is not yet supported for unicast pools - if matches!(pool_params.pool_type, shared::IpPoolType::Unicast) - && matches!(ip_version, IpVersion::V6) - { - return Err(Error::invalid_request( - "IPv6 pools are not yet supported for unicast pools", - )); - } - let pool = match pool_params.pool_type.clone() { shared::IpPoolType::Unicast => IpPool::new( &pool_params.identity, @@ -414,21 +404,6 @@ impl super::Nexus { return Err(not_found_from_lookup(pool_lookup)); } - // Disallow V6 ranges until IPv6 is fully supported by the networking - // subsystem. Instead of changing the API to reflect that (making this - // endpoint inconsistent with the rest) and changing it back when we - // add support, we accept them at the API layer and error here. It - // would be nice if we could do it in the datastore layer, but we'd - // have no way of creating IPv6 ranges for the purpose of testing IP - // pool utilization. - // - // See https://github.com/oxidecomputer/omicron/issues/8761. - if matches!(range, shared::IpRange::V6(_)) { - return Err(Error::invalid_request( - "IPv6 ranges are not allowed yet", - )); - } - // Validate uniformity and pool type constraints. // Extract first/last addresses once and reuse for all validation checks. match range { diff --git a/nexus/src/app/network_interface.rs b/nexus/src/app/network_interface.rs index a7e7bec174b..4be22aa46be 100644 --- a/nexus/src/app/network_interface.rs +++ b/nexus/src/app/network_interface.rs @@ -1,9 +1,10 @@ -use std::net::IpAddr; +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Nexus methods for operating on `InstanceNetworkInterface`s. use nexus_db_lookup::lookup; -use nexus_db_model::IpConfig; -use nexus_db_model::Ipv4Assignment; -use nexus_db_model::Ipv4Config; use nexus_db_queries::authz::ApiResource; use nexus_db_queries::context::OpContext; use nexus_db_queries::db::queries::network_interface; @@ -77,33 +78,6 @@ impl super::Nexus { instance_lookup: &lookup::Instance<'_>, params: ¶ms::InstanceNetworkInterfaceCreate, ) -> CreateResult { - // TODO-completeness: Support creating dual-stack NICs in the public - // API. See https://github.com/oxidecomputer/omicron/issues/9248. - let ipv4_assignment = match params.ip { - Some(IpAddr::V4(ip)) => Ipv4Assignment::Explicit(ip), - Some(IpAddr::V6(_)) => { - return Err(Error::invalid_request( - "IPv6 addressing is not yet suported for network interfaces", - )); - } - None => Ipv4Assignment::Auto, - }; - let transit_ips = params - .transit_ips - .iter() - .map(|ipnet| { - let oxnet::IpNet::V4(net) = ipnet else { - return Err(Error::invalid_request( - "IPv6 transit IPs are not yet supported \ - for network interfaces", - )); - }; - Ok(*net) - }) - .collect::>()?; - let ip_config = - IpConfig::V4(Ipv4Config { ip: ipv4_assignment, transit_ips }); - let (.., authz_project, authz_instance) = instance_lookup.lookup_for(authz::Action::Modify).await?; @@ -124,7 +98,7 @@ impl super::Nexus { InstanceUuid::from_untyped_uuid(authz_instance.id()), db_subnet, params.identity.clone(), - ip_config, + params.ip_config.clone(), )?; self.db_datastore .instance_create_network_interface( diff --git a/nexus/src/app/sagas/instance_common.rs b/nexus/src/app/sagas/instance_common.rs index 50e2eb01aa6..9af07242b3c 100644 --- a/nexus/src/app/sagas/instance_common.rs +++ b/nexus/src/app/sagas/instance_common.rs @@ -9,7 +9,7 @@ use std::net::{IpAddr, Ipv6Addr}; use crate::Nexus; use nexus_db_lookup::LookupPath; use nexus_db_model::{ - ByteCount, ExternalIp, InstanceState, IpAttachState, NatEntry, + ByteCount, ExternalIp, InstanceState, IpAttachState, IpVersion, NatEntry, SledReservationConstraints, SledResourceVmm, VmmCpuPlatform, VmmState, }; use nexus_db_queries::authz; @@ -535,5 +535,5 @@ pub(super) async fn instance_ip_remove_opte( #[derive(Clone, Debug, Deserialize, Serialize)] pub enum ExternalIpAttach { Ephemeral { pool: Option }, - Floating { floating_ip: authz::FloatingIp }, + Floating { floating_ip: authz::FloatingIp, ip_version: IpVersion }, } diff --git a/nexus/src/app/sagas/instance_create.rs b/nexus/src/app/sagas/instance_create.rs index 16981e25e99..c1b437f3ffd 100644 --- a/nexus/src/app/sagas/instance_create.rs +++ b/nexus/src/app/sagas/instance_create.rs @@ -15,13 +15,14 @@ use crate::app::{ }; use crate::external_api::params; use nexus_db_lookup::LookupPath; -use nexus_db_model::{ - ExternalIp, IpConfig, Ipv4Assignment, Ipv4Config, NetworkInterfaceKind, -}; +use nexus_db_model::NetworkInterfaceKind; +use nexus_db_model::{ExternalIp, IpVersion}; use nexus_db_queries::db::queries::network_interface::InsertError as InsertNicError; use nexus_db_queries::{authn, authz, db}; use nexus_defaults::DEFAULT_PRIMARY_NIC_NAME; -use nexus_types::external_api::params::InstanceDiskAttachment; +use nexus_types::external_api::params::{ + InstanceDiskAttachment, PrivateIpStackCreate, +}; use nexus_types::identity::Resource; use omicron_common::api::external::IdentityMetadataCreateParams; use omicron_common::api::external::Name; @@ -38,7 +39,6 @@ use slog::{info, warn}; use std::collections::HashSet; use std::convert::TryFrom; use std::fmt::Debug; -use std::net::IpAddr; use steno::ActionError; use steno::Node; use steno::{DagBuilder, SagaName}; @@ -101,9 +101,13 @@ declare_saga_actions! { + sic_create_network_interface - sic_create_network_interface_undo } - CREATE_SNAT_IP -> "snat_ip" { - + sic_allocate_instance_snat_ip - - sic_allocate_instance_snat_ip_undo + CREATE_SNAT_IPV4 -> "snat_ipv4" { + + sic_allocate_instance_snat_ipv4 + - sic_allocate_instance_snat_ipv4_undo + } + CREATE_SNAT_IPV6 -> "snat_ipv6" { + + sic_allocate_instance_snat_ipv6 + - sic_allocate_instance_snat_ipv6_undo } CREATE_EXTERNAL_IP -> "output" { + sic_allocate_instance_external_ip @@ -239,13 +243,72 @@ impl NexusSaga for SagaInstanceCreate { )?; } - // Allocate an external IP address for the default outbound connectivity - builder.append(Node::action( - "snat_ip_id", - "CreateSnatIpId", - ACTION_GENERATE_ID.as_ref(), - )); - builder.append(create_snat_ip_action()); + // Allocate an SNAT IP address for each IP stack in the instance's + // primary NIC. + // + // NOTE: This is really going in the wrong direction. As described in + // https://github.com/oxidecomputer/omicron/issues/4317, we want to only + // allocate these addresses if there aren't any others. In fixing that, + // we should also allow VPC-only networking (which isn't possible + // today), attaching / detaching an SNAT IP (you can only do Ephemeral + // or Floating today), and moving IP address allocation to the instance + // start saga from here. + // + // All of these together are a pretty big chunk of work, and should be + // tackled on their own. So we're deferring that for now. + match ¶ms.create_params.network_interfaces { + params::InstanceNetworkInterfaceAttachment::Create(nics) => { + if let Some(primary) = nics.first() { + if primary.ip_config.has_ipv4_stack() { + builder.append(Node::action( + "snat_ipv4_id", + "CreateSnatIpv4Id", + ACTION_GENERATE_ID.as_ref(), + )); + builder.append(create_snat_ipv4_action()); + } + if primary.ip_config.has_ipv6_stack() { + builder.append(Node::action( + "snat_ipv6_id", + "CreateSnatIpv6Id", + ACTION_GENERATE_ID.as_ref(), + )); + builder.append(create_snat_ipv6_action()); + } + } + } + params::InstanceNetworkInterfaceAttachment::DefaultIpv4 => { + builder.append(Node::action( + "snat_ipv4_id", + "CreateSnatIpv4Id", + ACTION_GENERATE_ID.as_ref(), + )); + builder.append(create_snat_ipv4_action()); + } + params::InstanceNetworkInterfaceAttachment::DefaultIpv6 => { + builder.append(Node::action( + "snat_ipv6_id", + "CreateSnatIpv6Id", + ACTION_GENERATE_ID.as_ref(), + )); + builder.append(create_snat_ipv6_action()); + } + params::InstanceNetworkInterfaceAttachment::DefaultDualStack => { + builder.append(Node::action( + "snat_ipv4_id", + "CreateSnatIpv4Id", + ACTION_GENERATE_ID.as_ref(), + )); + builder.append(create_snat_ipv4_action()); + builder.append(Node::action( + "snat_ipv6_id", + "CreateSnatIpv6Id", + ACTION_GENERATE_ID.as_ref(), + )); + builder.append(create_snat_ipv6_action()); + } + params::InstanceNetworkInterfaceAttachment::None => {} + } // See the comment above where we add nodes for creating NICs. We use // the same pattern here. @@ -462,6 +525,23 @@ async fn sic_add_to_anti_affinity_group( Ok(()) } +/// Convert an `InstanceNetworkInterfaceAttachment` to an `IpConfig`. +/// +/// # Panics +/// +/// This panics if the attachment isn't one of the "default" variants. +fn nic_attachment_to_ip_config( + attachment: ¶ms::InstanceNetworkInterfaceAttachment, +) -> PrivateIpStackCreate { + use params::InstanceNetworkInterfaceAttachment::*; + match attachment { + DefaultIpv4 => PrivateIpStackCreate::auto_ipv4(), + DefaultIpv6 => PrivateIpStackCreate::auto_ipv6(), + DefaultDualStack => PrivateIpStackCreate::auto_dual_stack(), + Create(_) | None => panic!("Only works for default variants"), + } +} + /// Create a network interface for an instance, using the parameters at index /// `nic_index`, returning the UUID for the NIC (or None). async fn sic_create_network_interface( @@ -475,13 +555,17 @@ async fn sic_create_network_interface( let interface_params = &saga_params.create_params.network_interfaces; match interface_params { params::InstanceNetworkInterfaceAttachment::None => Ok(()), - params::InstanceNetworkInterfaceAttachment::Default => { + params::InstanceNetworkInterfaceAttachment::DefaultIpv4 + | params::InstanceNetworkInterfaceAttachment::DefaultIpv6 + | params::InstanceNetworkInterfaceAttachment::DefaultDualStack => { + let ip_config = nic_attachment_to_ip_config(interface_params); create_default_primary_network_interface( &sagactx, &saga_params, nic_index, instance_id, interface_id, + ip_config, ) .await } @@ -602,40 +686,12 @@ async fn create_custom_network_interface( .await .map_err(ActionError::action_failed)?; - // TODO-completeness: Support IPv6 addressing in the public API, see - // https://github.com/oxidecomputer/omicron/issues/9248. - let ipv4_assignment = match interface_params.ip { - Some(IpAddr::V4(ip)) => Ipv4Assignment::Explicit(ip), - Some(IpAddr::V6(_)) => { - return Err(ActionError::action_failed(Error::invalid_request( - "IPv6 addressing is not yet suported for network interfaces", - ))); - } - None => Ipv4Assignment::Auto, - }; - let transit_ips = interface_params - .transit_ips - .iter() - .map(|ipnet| { - let oxnet::IpNet::V4(net) = ipnet else { - return Err(ActionError::action_failed( - Error::invalid_request( - "IPv6 transit IPs are not yet supported \ - for network interfaces", - ), - )); - }; - Ok(*net) - }) - .collect::>()?; - let ip_config = - IpConfig::V4(Ipv4Config { ip: ipv4_assignment, transit_ips }); let interface = db::model::IncompleteNetworkInterface::new_instance( interface_id, instance_id, db_subnet.clone(), interface_params.identity.clone(), - ip_config, + interface_params.ip_config.clone(), ) .map_err(ActionError::action_failed)?; datastore @@ -664,12 +720,16 @@ async fn create_custom_network_interface( /// Create a default primary network interface for an instance during the create /// saga. +/// +/// Note that this is used to create any of the possible "default" interface +/// types, IPv4-only, IPv6-only, and dual-stack. async fn create_default_primary_network_interface( sagactx: &NexusActionContext, saga_params: &Params, nic_index: usize, instance_id: InstanceUuid, interface_id: Uuid, + ip_config: PrivateIpStackCreate, ) -> Result<(), ActionError> { // We're statically creating up to MAX_NICS_PER_INSTANCE saga nodes, but // this method only applies to the case where there's exactly one parameter @@ -707,8 +767,7 @@ async fn create_default_primary_network_interface( }, vpc_name: default_name.clone(), subnet_name: default_name.clone(), - ip: None, // Request an IP address allocation - transit_ips: vec![], // Default interfaces don't use transit IPs + ip_config, }; // Lookup authz objects, used in the call to actually create the NIC. @@ -724,41 +783,12 @@ async fn create_default_primary_network_interface( .fetch() .await .map_err(ActionError::action_failed)?; - - // TODO-completeness: Support IPv6 addressing in the public API, see - // https://github.com/oxidecomputer/omicron/issues/9248. - let ipv4_assignment = match interface_params.ip { - Some(IpAddr::V4(ip)) => Ipv4Assignment::Explicit(ip), - Some(IpAddr::V6(_)) => { - return Err(ActionError::action_failed(Error::invalid_request( - "IPv6 addressing is not yet suported for network interfaces", - ))); - } - None => Ipv4Assignment::Auto, - }; - let transit_ips = interface_params - .transit_ips - .iter() - .map(|ipnet| { - let oxnet::IpNet::V4(net) = ipnet else { - return Err(ActionError::action_failed( - Error::invalid_request( - "IPv6 transit IPs are not yet supported \ - for network interfaces", - ), - )); - }; - Ok(*net) - }) - .collect::>()?; - let ip_config = - IpConfig::V4(Ipv4Config { ip: ipv4_assignment, transit_ips }); let interface = db::model::IncompleteNetworkInterface::new_instance( interface_id, instance_id, db_subnet.clone(), interface_params.identity.clone(), - ip_config, + interface_params.ip_config.clone(), ) .map_err(ActionError::action_failed)?; datastore @@ -785,9 +815,23 @@ async fn create_default_primary_network_interface( Ok(()) } -/// Create an external IP address for instance source NAT. -async fn sic_allocate_instance_snat_ip( +/// Create an external IPv4 address for instance source NAT. +async fn sic_allocate_instance_snat_ipv4( + sagactx: NexusActionContext, +) -> Result<(), ActionError> { + sic_allocate_instance_snat_ip_impl(sagactx, IpVersion::V4).await +} + +/// Create an external IPv4 address for instance source NAT. +async fn sic_allocate_instance_snat_ipv6( + sagactx: NexusActionContext, +) -> Result<(), ActionError> { + sic_allocate_instance_snat_ip_impl(sagactx, IpVersion::V6).await +} + +async fn sic_allocate_instance_snat_ip_impl( sagactx: NexusActionContext, + ip_version: IpVersion, ) -> Result<(), ActionError> { let osagactx = sagactx.user_data(); let datastore = osagactx.datastore(); @@ -797,10 +841,13 @@ async fn sic_allocate_instance_snat_ip( &saga_params.serialized_authn, ); let instance_id = sagactx.lookup::("instance_id")?; - let ip_id = sagactx.lookup::("snat_ip_id")?; + let ancestor_node_name = format!("snat_ip{}_id", ip_version); + let ip_id = sagactx.lookup::(&ancestor_node_name)?; + // TODO(ben): Merge Zeeshan's work, and then fetch the default for the + // provided version. let (.., pool) = datastore - .ip_pools_fetch_default(&opctx) + .ip_pools_fetch_default(&opctx /*, ip_version */) .await .map_err(ActionError::action_failed)?; let pool_id = pool.identity.id; @@ -812,9 +859,23 @@ async fn sic_allocate_instance_snat_ip( Ok(()) } -/// Destroy an allocated SNAT IP address for the instance. -async fn sic_allocate_instance_snat_ip_undo( +/// Destroy an allocated SNAT IPv4 address for the instance. +async fn sic_allocate_instance_snat_ipv4_undo( + sagactx: NexusActionContext, +) -> Result<(), anyhow::Error> { + sic_allocate_instance_snat_ip_undo_impl(sagactx, "snat_ipv4_id").await +} + +/// Destroy an allocated SNAT IPv6 address for the instance. +async fn sic_allocate_instance_snat_ipv6_undo( + sagactx: NexusActionContext, +) -> Result<(), anyhow::Error> { + sic_allocate_instance_snat_ip_undo_impl(sagactx, "snat_ipv6_id").await +} + +async fn sic_allocate_instance_snat_ip_undo_impl( sagactx: NexusActionContext, + ip_name: &str, ) -> Result<(), anyhow::Error> { let osagactx = sagactx.user_data(); let datastore = osagactx.datastore(); @@ -823,12 +884,12 @@ async fn sic_allocate_instance_snat_ip_undo( &sagactx, &saga_params.serialized_authn, ); - let ip_id = sagactx.lookup::("snat_ip_id")?; + let ip_id = sagactx.lookup::(ip_name)?; datastore.deallocate_external_ip(&opctx, ip_id).await?; Ok(()) } -/// Create an external IPs for the instance, using the request parameters at +/// Create external IPs for the instance, using the request parameters at /// index `ip_index`, and return its ID if one is created (or None). async fn sic_allocate_instance_external_ip( sagactx: NexusActionContext, @@ -889,7 +950,7 @@ async fn sic_allocate_instance_external_ip( } // Set the parent of an existing floating IP to the new instance's ID. params::ExternalIpCreate::Floating { floating_ip } => { - let (.., authz_project, authz_fip) = match floating_ip { + let (.., authz_project, authz_fip, db_fip) = match floating_ip { NameOrId::Name(name) => LookupPath::new(&opctx, datastore) .project_id(saga_params.project_id) .floating_ip_name(db::model::Name::ref_cast(name)), @@ -897,7 +958,7 @@ async fn sic_allocate_instance_external_ip( LookupPath::new(&opctx, datastore).floating_ip_id(*id) } } - .lookup_for(authz::Action::Modify) + .fetch_for(authz::Action::Modify) .await .map_err(ActionError::action_failed)?; @@ -909,8 +970,19 @@ async fn sic_allocate_instance_external_ip( )); } + let ip_version = match db_fip.ip { + ipnetwork::IpNetwork::V4(_) => IpVersion::V4, + ipnetwork::IpNetwork::V6(_) => IpVersion::V6, + }; + datastore - .floating_ip_begin_attach(&opctx, &authz_fip, instance_id, true) + .floating_ip_begin_attach( + &opctx, + &authz_fip, + ip_version, + instance_id, + true, + ) .await .map_err(ActionError::action_failed)? .0 @@ -1428,7 +1500,7 @@ pub mod test { use nexus_db_queries::context::OpContext; use nexus_db_queries::db::datastore::DataStore; use nexus_test_utils::resource_helpers::DiskTest; - use nexus_test_utils::resource_helpers::create_default_ip_pool; + use nexus_test_utils::resource_helpers::create_default_ip_pools; use nexus_test_utils::resource_helpers::create_disk; use nexus_test_utils::resource_helpers::create_project; use nexus_test_utils_macros::nexus_test; @@ -1448,7 +1520,7 @@ pub mod test { const DISK_NAME: &str = "my-disk"; async fn create_org_project_and_disk(client: &ClientTestContext) -> Uuid { - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; let project = create_project(client, PROJECT_NAME).await; create_disk(&client, PROJECT_NAME, DISK_NAME).await; project.identity.id @@ -1470,7 +1542,7 @@ pub mod test { user_data: vec![], ssh_public_keys: None, network_interfaces: - params::InstanceNetworkInterfaceAttachment::Default, + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![params::ExternalIpCreate::Ephemeral { pool: None, }], diff --git a/nexus/src/app/sagas/instance_delete.rs b/nexus/src/app/sagas/instance_delete.rs index 0edc640cdc5..311d323676f 100644 --- a/nexus/src/app/sagas/instance_delete.rs +++ b/nexus/src/app/sagas/instance_delete.rs @@ -214,7 +214,7 @@ mod test { use nexus_db_lookup::LookupPath; use nexus_db_queries::{authn::saga::Serialized, context::OpContext, db}; use nexus_test_utils::resource_helpers::DiskTest; - use nexus_test_utils::resource_helpers::create_default_ip_pool; + use nexus_test_utils::resource_helpers::create_default_ip_pools; use nexus_test_utils::resource_helpers::create_disk; use nexus_test_utils::resource_helpers::create_project; use nexus_test_utils_macros::nexus_test; @@ -234,7 +234,7 @@ mod test { const DISK_NAME: &str = "my-disk"; async fn create_org_project_and_disk(client: &ClientTestContext) -> Uuid { - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; let project = create_project(client, PROJECT_NAME).await; create_disk(&client, PROJECT_NAME, DISK_NAME).await; project.identity.id @@ -273,7 +273,7 @@ mod test { user_data: vec![], ssh_public_keys: Some(Vec::new()), network_interfaces: - params::InstanceNetworkInterfaceAttachment::Default, + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![params::ExternalIpCreate::Ephemeral { pool: None, }], diff --git a/nexus/src/app/sagas/instance_ip_attach.rs b/nexus/src/app/sagas/instance_ip_attach.rs index 11a0a18f19a..ff648b6531b 100644 --- a/nexus/src/app/sagas/instance_ip_attach.rs +++ b/nexus/src/app/sagas/instance_ip_attach.rs @@ -126,8 +126,14 @@ async fn siia_begin_attach_ip( }) } // Set the parent of an existing floating IP to the new instance's ID. - ExternalIpAttach::Floating { floating_ip } => datastore - .floating_ip_begin_attach(&opctx, &floating_ip, instance_id, false) + ExternalIpAttach::Floating { floating_ip, ip_version } => datastore + .floating_ip_begin_attach( + &opctx, + floating_ip, + *ip_version, + instance_id, + false, + ) .await .map_err(ActionError::action_failed) .map(|(external_ip, do_saga)| ModifyStateForExternalIp { @@ -331,10 +337,10 @@ pub(crate) mod test { }; use dropshot::test_util::ClientTestContext; use nexus_db_lookup::LookupPath; - use nexus_db_model::{ExternalIp, IpKind}; + use nexus_db_model::{ExternalIp, IpKind, IpVersion}; use nexus_db_queries::context::OpContext; use nexus_test_utils::resource_helpers::{ - create_default_ip_pool, create_floating_ip, create_instance, + create_default_ip_pools, create_floating_ip, create_instance, create_project, }; use nexus_test_utils_macros::nexus_test; @@ -349,7 +355,7 @@ pub(crate) mod test { const FIP_NAME: &str = "affogato"; pub async fn ip_manip_test_setup(client: &ClientTestContext) -> Uuid { - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; let project = create_project(client, PROJECT_NAME).await; create_floating_ip( client, @@ -370,13 +376,17 @@ pub(crate) mod test { ) -> Params { let project_name = db::model::Name(PROJECT_NAME.parse().unwrap()); let create_params = if use_floating { - let (.., floating_ip) = LookupPath::new(opctx, datastore) + let (.., floating_ip, db_fip) = LookupPath::new(opctx, datastore) .project_name(&project_name) .floating_ip_name(&db::model::Name(FIP_NAME.parse().unwrap())) - .lookup_for(authz::Action::Modify) + .fetch_for(authz::Action::Modify) .await .unwrap(); - ExternalIpAttach::Floating { floating_ip } + let ip_version = match db_fip.ip { + ipnetwork::IpNetwork::V4(_) => IpVersion::V4, + ipnetwork::IpNetwork::V6(_) => IpVersion::V6, + }; + ExternalIpAttach::Floating { floating_ip, ip_version } } else { ExternalIpAttach::Ephemeral { pool: None } }; diff --git a/nexus/src/app/sagas/instance_migrate.rs b/nexus/src/app/sagas/instance_migrate.rs index 955cfa29e5d..81d4af9a441 100644 --- a/nexus/src/app/sagas/instance_migrate.rs +++ b/nexus/src/app/sagas/instance_migrate.rs @@ -622,7 +622,7 @@ mod tests { use crate::external_api::params; use dropshot::test_util::ClientTestContext; use nexus_test_utils::resource_helpers::{ - create_default_ip_pool, create_project, object_create, + create_default_ip_pools, create_project, object_create, }; use nexus_test_utils_macros::nexus_test; use omicron_common::api::external::{ @@ -636,7 +636,7 @@ mod tests { const INSTANCE_NAME: &str = "test-instance"; async fn setup_test_project(client: &ClientTestContext) -> Uuid { - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; let project = create_project(&client, PROJECT_NAME).await; project.identity.id } diff --git a/nexus/src/app/sagas/instance_start.rs b/nexus/src/app/sagas/instance_start.rs index 50c4d60d557..9284a28288c 100644 --- a/nexus/src/app/sagas/instance_start.rs +++ b/nexus/src/app/sagas/instance_start.rs @@ -1102,7 +1102,7 @@ mod test { use dropshot::test_util::ClientTestContext; use nexus_db_queries::authn; use nexus_test_utils::resource_helpers::{ - create_default_ip_pool, create_project, object_create, + create_default_ip_pools, create_project, object_create, }; use nexus_test_utils_macros::nexus_test; use nexus_types::identity::Resource; @@ -1122,7 +1122,7 @@ mod test { const INSTANCE_NAME: &str = "test-instance"; async fn setup_test_project(client: &ClientTestContext) -> Uuid { - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; let project = create_project(&client, PROJECT_NAME).await; project.identity.id } @@ -1145,7 +1145,7 @@ mod test { user_data: b"#cloud-config".to_vec(), ssh_public_keys: Some(Vec::new()), network_interfaces: - params::InstanceNetworkInterfaceAttachment::Default, + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], disks: vec![], boot_disk: None, diff --git a/nexus/src/app/sagas/instance_update/mod.rs b/nexus/src/app/sagas/instance_update/mod.rs index 3c96508882d..95df7a2b214 100644 --- a/nexus/src/app/sagas/instance_update/mod.rs +++ b/nexus/src/app/sagas/instance_update/mod.rs @@ -1538,7 +1538,7 @@ mod test { use nexus_db_lookup::LookupPath; use nexus_db_queries::db::datastore::InstanceAndActiveVmm; use nexus_test_utils::resource_helpers::{ - create_default_ip_pool, create_project, object_create, + create_default_ip_pools, create_project, object_create, }; use nexus_test_utils_macros::nexus_test; use nexus_types::internal_api::params::InstanceMigrateRequest; @@ -1578,7 +1578,7 @@ mod test { // 6. migration source failed async fn setup_test_project(client: &ClientTestContext) -> Uuid { - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; let project = create_project(&client, PROJECT_NAME).await; project.identity.id } diff --git a/nexus/src/app/sagas/multicast_group_dpd_ensure.rs b/nexus/src/app/sagas/multicast_group_dpd_ensure.rs index 365d1615c6e..1d27e3fb805 100644 --- a/nexus/src/app/sagas/multicast_group_dpd_ensure.rs +++ b/nexus/src/app/sagas/multicast_group_dpd_ensure.rs @@ -309,7 +309,7 @@ mod test { use nexus_db_queries::authn::saga::Serialized; use nexus_db_queries::db::fixed_data::silo::DEFAULT_SILO; use nexus_test_utils::resource_helpers::{ - create_default_ip_pool, link_ip_pool, object_create, + create_default_ip_pools, link_ip_pool, object_create, }; use nexus_test_utils_macros::nexus_test; use nexus_types::external_api::params::{ @@ -436,7 +436,7 @@ mod test { let opctx = test_helpers::test_opctx(cptestctx); // Setup: Create IP pools - create_default_ip_pool(client).await; + create_default_ip_pools(client).await; // Create multicast IP pool let pool_name = "saga-state-pool"; diff --git a/nexus/src/app/sagas/snapshot_create.rs b/nexus/src/app/sagas/snapshot_create.rs index ab8f09facd7..6342a75e9e6 100644 --- a/nexus/src/app/sagas/snapshot_create.rs +++ b/nexus/src/app/sagas/snapshot_create.rs @@ -1731,7 +1731,7 @@ mod test { use nexus_db_queries::db::DataStore; use nexus_db_queries::db::datastore::Disk; use nexus_db_queries::db::datastore::InstanceAndActiveVmm; - use nexus_test_utils::resource_helpers::create_default_ip_pool; + use nexus_test_utils::resource_helpers::create_default_ip_pools; use nexus_test_utils::resource_helpers::create_disk; use nexus_test_utils::resource_helpers::create_project; use nexus_test_utils::resource_helpers::delete_disk; @@ -1956,7 +1956,7 @@ mod test { async fn create_project_and_disk_and_pool( client: &ClientTestContext, ) -> Uuid { - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; create_project(client, PROJECT_NAME).await; create_disk(client, PROJECT_NAME, DISK_NAME).await.identity.id } diff --git a/nexus/src/app/sagas/vpc_create.rs b/nexus/src/app/sagas/vpc_create.rs index b79cbf3063f..dacb81a90e4 100644 --- a/nexus/src/app/sagas/vpc_create.rs +++ b/nexus/src/app/sagas/vpc_create.rs @@ -662,7 +662,7 @@ pub(crate) mod test { authn::saga::Serialized, authz, context::OpContext, db::datastore::DataStore, db::fixed_data::vpc::SERVICES_VPC_ID, }; - use nexus_test_utils::resource_helpers::create_default_ip_pool; + use nexus_test_utils::resource_helpers::create_default_ip_pools; use nexus_test_utils::resource_helpers::create_project; use nexus_test_utils_macros::nexus_test; use omicron_common::api::external::IdentityMetadataCreateParams; @@ -676,7 +676,7 @@ pub(crate) mod test { const PROJECT_NAME: &str = "springfield-squidport"; async fn create_org_and_project(client: &ClientTestContext) -> Uuid { - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; let project = create_project(client, PROJECT_NAME).await; project.identity.id } diff --git a/nexus/src/app/sagas/vpc_subnet_create.rs b/nexus/src/app/sagas/vpc_subnet_create.rs index 6da5ad0f09c..03723cec928 100644 --- a/nexus/src/app/sagas/vpc_subnet_create.rs +++ b/nexus/src/app/sagas/vpc_subnet_create.rs @@ -357,7 +357,7 @@ pub(crate) mod test { authn::saga::Serialized, authz, context::OpContext, db::datastore::DataStore, db::fixed_data::vpc::SERVICES_VPC_ID, }; - use nexus_test_utils::resource_helpers::create_default_ip_pool; + use nexus_test_utils::resource_helpers::create_default_ip_pools; use nexus_test_utils::resource_helpers::create_project; use nexus_test_utils_macros::nexus_test; use nexus_types::external_api::params::VpcSelector; @@ -373,7 +373,7 @@ pub(crate) mod test { const PROJECT_NAME: &str = "springfield-squidport"; async fn create_org_and_project(client: &ClientTestContext) -> Uuid { - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; let project = create_project(client, PROJECT_NAME).await; project.identity.id } diff --git a/nexus/src/app/sagas/vpc_subnet_delete.rs b/nexus/src/app/sagas/vpc_subnet_delete.rs index 7ed3f9cb989..3645a5139a8 100644 --- a/nexus/src/app/sagas/vpc_subnet_delete.rs +++ b/nexus/src/app/sagas/vpc_subnet_delete.rs @@ -129,7 +129,7 @@ pub(crate) mod test { }; use dropshot::test_util::ClientTestContext; use nexus_db_queries::{authn::saga::Serialized, context::OpContext}; - use nexus_test_utils::resource_helpers::create_default_ip_pool; + use nexus_test_utils::resource_helpers::create_default_ip_pools; use nexus_test_utils::resource_helpers::create_project; use nexus_test_utils_macros::nexus_test; use omicron_common::api::external::NameOrId; @@ -141,7 +141,7 @@ pub(crate) mod test { const PROJECT_NAME: &str = "springfield-squidport"; async fn create_org_and_project(client: &ClientTestContext) -> Uuid { - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; let project = create_project(client, PROJECT_NAME).await; project.identity.id } diff --git a/nexus/src/app/sagas/vpc_subnet_update.rs b/nexus/src/app/sagas/vpc_subnet_update.rs index 31136799fd6..d87a2b325c4 100644 --- a/nexus/src/app/sagas/vpc_subnet_update.rs +++ b/nexus/src/app/sagas/vpc_subnet_update.rs @@ -112,7 +112,7 @@ pub(crate) mod test { use dropshot::test_util::ClientTestContext; use nexus_db_queries::db; use nexus_db_queries::{authn::saga::Serialized, context::OpContext}; - use nexus_test_utils::resource_helpers::create_default_ip_pool; + use nexus_test_utils::resource_helpers::create_default_ip_pools; use nexus_test_utils::resource_helpers::create_project; use nexus_test_utils::resource_helpers::create_router; use nexus_test_utils_macros::nexus_test; @@ -128,7 +128,7 @@ pub(crate) mod test { async fn create_org_and_project( client: &ClientTestContext, ) -> (Uuid, Uuid) { - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; let project = create_project(client, PROJECT_NAME).await; let router = create_router(client, PROJECT_NAME, "default", ROUTER_NAME).await; diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 2c08c457fb0..72fc28d121f 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -5479,8 +5479,8 @@ impl NexusExternalApi for NexusExternalApiImpl { ) .await? .into_iter() - .map(|d| d.into()) - .collect(); + .map(TryInto::try_into) + .collect::>()?; Ok(HttpResponseOk(ScanByNameOrId::results_page( &query, interfaces, @@ -5513,7 +5513,7 @@ impl NexusExternalApi for NexusExternalApiImpl { &interface_params.into_inner(), ) .await?; - Ok(HttpResponseCreated(iface.into())) + iface.try_into().map(HttpResponseCreated).map_err(HttpError::from) }; apictx .context @@ -5576,7 +5576,7 @@ impl NexusExternalApi for NexusExternalApiImpl { .instance_network_interface_lookup(&opctx, interface_selector)? .fetch() .await?; - Ok(HttpResponseOk(interface.into())) + interface.try_into().map(HttpResponseOk).map_err(HttpError::from) }; apictx .context @@ -5617,7 +5617,7 @@ impl NexusExternalApi for NexusExternalApiImpl { updated_iface, ) .await?; - Ok(HttpResponseOk(InstanceNetworkInterface::from(interface))) + interface.try_into().map(HttpResponseOk).map_err(HttpError::from) }; apictx .context @@ -6297,8 +6297,8 @@ impl NexusExternalApi for NexusExternalApiImpl { ) .await? .into_iter() - .map(|interfaces| interfaces.into()) - .collect(); + .map(TryInto::try_into) + .collect::>()?; Ok(HttpResponseOk(ScanByNameOrId::results_page( &query, interfaces, diff --git a/nexus/test-utils/src/resource_helpers.rs b/nexus/test-utils/src/resource_helpers.rs index e80ad7b3fdb..8dac278ece5 100644 --- a/nexus/test-utils/src/resource_helpers.rs +++ b/nexus/test-utils/src/resource_helpers.rs @@ -347,13 +347,33 @@ pub async fn link_ip_pool( .await; } +/// Create a default IPv4 and IPv6 IP Pool. +/// /// What you want for any test that is not testing IP logic specifically -pub async fn create_default_ip_pool( +pub async fn create_default_ip_pools( client: &ClientTestContext, -) -> views::IpPool { - let (pool, ..) = create_ip_pool(&client, "default", None).await; - link_ip_pool(&client, "default", &DEFAULT_SILO.id(), true).await; - pool +) -> (views::IpPool, views::IpPool) { + let ranges = [ + IpRange::try_from(( + std::net::Ipv4Addr::new(10, 0, 0, 0), + std::net::Ipv4Addr::new(10, 0, 255, 255), + )) + .unwrap(), + IpRange::try_from(( + std::net::Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 0), + std::net::Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 0xffff), + )) + .unwrap(), + ]; + let (v4_pool, ..) = + create_ip_pool(&client, "default-v4", Some(ranges[0])).await; + link_ip_pool(&client, "default-v4", &DEFAULT_SILO.id(), true).await; + let (v6_pool, ..) = + create_ip_pool(&client, "default-v6", Some(ranges[1])).await; + // TODO(ben) default = false here is not what we want. Need to merge in + // Zeeshan's work. + link_ip_pool(&client, "default-v6", &DEFAULT_SILO.id(), false).await; + (v4_pool, v6_pool) } pub async fn create_floating_ip( @@ -697,7 +717,7 @@ pub async fn create_instance( client, project_name, instance_name, - ¶ms::InstanceNetworkInterfaceAttachment::Default, + ¶ms::InstanceNetworkInterfaceAttachment::DefaultIpv4, // Disks= Vec::::new(), // External IPs= diff --git a/nexus/tests/integration_tests/affinity.rs b/nexus/tests/integration_tests/affinity.rs index a786979410b..d1b1c60c7e2 100644 --- a/nexus/tests/integration_tests/affinity.rs +++ b/nexus/tests/integration_tests/affinity.rs @@ -10,7 +10,7 @@ use http::StatusCode; use nexus_test_utils::http_testing::AuthnMode; use nexus_test_utils::http_testing::NexusRequest; use nexus_test_utils::http_testing::RequestBuilder; -use nexus_test_utils::resource_helpers::create_default_ip_pool; +use nexus_test_utils::resource_helpers::create_default_ip_pools; use nexus_test_utils::resource_helpers::create_instance_with; use nexus_test_utils::resource_helpers::create_project; use nexus_test_utils::resource_helpers::object_create; @@ -466,7 +466,7 @@ async fn test_affinity_group_usage(cptestctx: &ControlPlaneTestContext) { } // Create an IP pool and project that we'll use for testing. - create_default_ip_pool(&external_client).await; + create_default_ip_pools(&external_client).await; api.create_project(PROJECT_NAME).await; let project_api = api.use_project::(PROJECT_NAME); @@ -577,7 +577,7 @@ async fn test_anti_affinity_group_usage(cptestctx: &ControlPlaneTestContext) { } // Create an IP pool and project that we'll use for testing. - create_default_ip_pool(&external_client).await; + create_default_ip_pools(&external_client).await; api.create_project(PROJECT_NAME).await; let project_api = api.use_project::(PROJECT_NAME); @@ -689,7 +689,7 @@ async fn test_group_crud(client: &ClientTestContext) { let api = ApiHelper::new(client); // Create an IP pool and project that we'll use for testing. - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; api.create_project(PROJECT_NAME).await; let project_api = api.use_project::(PROJECT_NAME); @@ -797,7 +797,7 @@ async fn test_instance_group_list( let api = ApiHelper::new(client); // Create an IP pool and project that we'll use for testing. - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; api.create_project(PROJECT_NAME).await; let project_api = api.use_project::(PROJECT_NAME); @@ -841,7 +841,7 @@ async fn test_group_project_selector( let api = ApiHelper::new(client); // Create an IP pool and project that we'll use for testing. - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; api.create_project(PROJECT_NAME).await; // All requests use the "?project={PROJECT_NAME}" query parameter diff --git a/nexus/tests/integration_tests/audit_log.rs b/nexus/tests/integration_tests/audit_log.rs index 72ed115cf1a..5eafbf92034 100644 --- a/nexus/tests/integration_tests/audit_log.rs +++ b/nexus/tests/integration_tests/audit_log.rs @@ -8,7 +8,7 @@ use http::{Method, StatusCode, header}; use nexus_db_queries::authn::USER_TEST_PRIVILEGED; use nexus_test_utils::http_testing::RequestBuilder; use nexus_test_utils::resource_helpers::{ - DiskTest, create_console_session, create_default_ip_pool, create_disk, + DiskTest, create_console_session, create_default_ip_pools, create_disk, create_instance_with, create_local_user, create_project, create_silo, object_create_error, object_delete, objects_list_page_authz, test_params, }; @@ -315,13 +315,13 @@ async fn test_audit_log_create_delete_ops(ctx: &ControlPlaneTestContext) { // Set up disk test infrastructure and create resources with audit logging DiskTest::new(&ctx).await; - create_default_ip_pool(client).await; + create_default_ip_pools(client).await; let _project = create_project(client, "test-project").await; let _instance = create_instance_with( client, "test-project", "test-instance", - ¶ms::InstanceNetworkInterfaceAttachment::Default, + ¶ms::InstanceNetworkInterfaceAttachment::DefaultIpv4, Vec::::new(), Vec::::new(), false, // start=false, so instance is created in stopped state diff --git a/nexus/tests/integration_tests/crucible_replacements.rs b/nexus/tests/integration_tests/crucible_replacements.rs index a6f4a8e89a2..67cf4a94551 100644 --- a/nexus/tests/integration_tests/crucible_replacements.rs +++ b/nexus/tests/integration_tests/crucible_replacements.rs @@ -21,7 +21,7 @@ use nexus_lockstep_client::types::LastResult; use nexus_test_utils::background::*; use nexus_test_utils::http_testing::AuthnMode; use nexus_test_utils::http_testing::NexusRequest; -use nexus_test_utils::resource_helpers::create_default_ip_pool; +use nexus_test_utils::resource_helpers::create_default_ip_pools; use nexus_test_utils::resource_helpers::create_disk; use nexus_test_utils::resource_helpers::create_disk_from_snapshot; use nexus_test_utils::resource_helpers::create_project; @@ -76,7 +76,7 @@ fn get_snapshots_url() -> String { } async fn create_project_and_pool(client: &ClientTestContext) -> Uuid { - create_default_ip_pool(client).await; + create_default_ip_pools(client).await; let project = create_project(client, PROJECT_NAME).await; project.identity.id } diff --git a/nexus/tests/integration_tests/disks.rs b/nexus/tests/integration_tests/disks.rs index 66adb606918..3433badac66 100644 --- a/nexus/tests/integration_tests/disks.rs +++ b/nexus/tests/integration_tests/disks.rs @@ -23,7 +23,7 @@ use nexus_test_utils::http_testing::AuthnMode; use nexus_test_utils::http_testing::NexusRequest; use nexus_test_utils::http_testing::RequestBuilder; use nexus_test_utils::identity_eq; -use nexus_test_utils::resource_helpers::create_default_ip_pool; +use nexus_test_utils::resource_helpers::create_default_ip_pools; use nexus_test_utils::resource_helpers::create_disk; use nexus_test_utils::resource_helpers::create_instance; use nexus_test_utils::resource_helpers::create_project; @@ -111,7 +111,7 @@ fn get_disk_detach_url(instance: &NameOrId) -> String { } async fn create_project_and_pool(client: &ClientTestContext) -> Uuid { - create_default_ip_pool(client).await; + create_default_ip_pools(client).await; let project = create_project(client, PROJECT_NAME).await; project.identity.id } @@ -1110,7 +1110,7 @@ async fn test_disk_virtual_provisioning_collection( let _test = DiskTest::new(&cptestctx).await; - create_default_ip_pool(client).await; + create_default_ip_pools(client).await; let project_id1 = create_project(client, PROJECT_NAME).await.identity.id; let project_id2 = create_project(client, PROJECT_NAME_2).await.identity.id; diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index 92a09964937..4f42e73a5ee 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -20,6 +20,7 @@ use nexus_test_utils::SLED_AGENT_UUID; use nexus_test_utils::SWITCH_UUID; use nexus_test_utils::resource_helpers::test_params; use nexus_types::external_api::params; +use nexus_types::external_api::params::PrivateIpStackCreate; use nexus_types::external_api::shared; use nexus_types::external_api::shared::IpRange; use nexus_types::external_api::shared::IpVersion; @@ -656,7 +657,8 @@ pub static DEMO_INSTANCE_CREATE: LazyLock = hostname: "demo-instance".parse().unwrap(), user_data: vec![], ssh_public_keys: Some(Vec::new()), - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![params::ExternalIpCreate::Ephemeral { pool: Some(DEMO_IP_POOL_NAME.clone().into()), }], @@ -679,7 +681,8 @@ pub static DEMO_STOPPED_INSTANCE_CREATE: LazyLock = hostname: "demo-instance".parse().unwrap(), user_data: vec![], ssh_public_keys: Some(Vec::new()), - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![params::ExternalIpCreate::Ephemeral { pool: Some(DEMO_IP_POOL_NAME.clone().into()), }], @@ -719,8 +722,7 @@ pub static DEMO_INSTANCE_NIC_CREATE: LazyLock< }, vpc_name: DEMO_VPC_NAME.clone(), subnet_name: DEMO_VPC_SUBNET_NAME.clone(), - ip: None, - transit_ips: vec![], + ip_config: PrivateIpStackCreate::auto_ipv4(), }); pub static DEMO_INSTANCE_NIC_PUT: LazyLock< params::InstanceNetworkInterfaceUpdate, diff --git a/nexus/tests/integration_tests/external_ips.rs b/nexus/tests/integration_tests/external_ips.rs index 83594fe8184..cce0b65ac64 100644 --- a/nexus/tests/integration_tests/external_ips.rs +++ b/nexus/tests/integration_tests/external_ips.rs @@ -6,6 +6,7 @@ use std::net::IpAddr; use std::net::Ipv4Addr; +use std::net::Ipv6Addr; use crate::integration_tests::instances::fetch_instance_external_ips; use crate::integration_tests::instances::instance_simulate; @@ -19,7 +20,7 @@ use nexus_test_utils::http_testing::AuthnMode; use nexus_test_utils::http_testing::NexusRequest; use nexus_test_utils::http_testing::RequestBuilder; use nexus_test_utils::resource_helpers::assert_ip_pool_utilization; -use nexus_test_utils::resource_helpers::create_default_ip_pool; +use nexus_test_utils::resource_helpers::create_default_ip_pools; use nexus_test_utils::resource_helpers::create_floating_ip; use nexus_test_utils::resource_helpers::create_instance_with; use nexus_test_utils::resource_helpers::create_ip_pool; @@ -37,6 +38,7 @@ use nexus_test_utils::resource_helpers::object_put; use nexus_test_utils::resource_helpers::test_params; use nexus_test_utils_macros::nexus_test; use nexus_types::external_api::params; +use nexus_types::external_api::params::InstanceNetworkInterfaceAttachment; use nexus_types::external_api::shared; use nexus_types::external_api::shared::SiloRole; use nexus_types::external_api::views; @@ -115,7 +117,7 @@ pub fn get_floating_ip_by_id_url(fip_id: &Uuid) -> String { async fn test_floating_ip_access(cptestctx: &ControlPlaneTestContext) { let client = &cptestctx.external_client; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; let project = create_project(client, PROJECT_NAME).await; // Create a floating IP from the default pool. @@ -163,7 +165,8 @@ async fn test_floating_ip_create(cptestctx: &ControlPlaneTestContext) { let client = &cptestctx.external_client; // automatically linked to current silo - let default_pool = create_default_ip_pool(&client).await; + let (default_pool, _default_v6_pool) = + create_default_ip_pools(&client).await; const CAPACITY: f64 = 65536.0; assert_ip_pool_utilization(client, "default", 0, CAPACITY).await; @@ -454,7 +457,7 @@ async fn test_floating_ip_create_ip_in_use( ) { let client = &cptestctx.external_client; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; let project = create_project(client, PROJECT_NAME).await; let contested_ip = "10.0.0.0".parse().unwrap(); @@ -502,7 +505,7 @@ async fn test_floating_ip_create_name_in_use( ) { let client = &cptestctx.external_client; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; let project = create_project(client, PROJECT_NAME).await; let contested_name = FIP_NAMES[0]; @@ -551,7 +554,7 @@ async fn test_floating_ip_create_name_in_use( async fn test_floating_ip_update(cptestctx: &ControlPlaneTestContext) { let client = &cptestctx.external_client; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; let project = create_project(client, PROJECT_NAME).await; // Create the Floating IP @@ -603,7 +606,7 @@ async fn test_floating_ip_update(cptestctx: &ControlPlaneTestContext) { async fn test_floating_ip_delete(cptestctx: &ControlPlaneTestContext) { let client = &cptestctx.external_client; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; let project = create_project(client, PROJECT_NAME).await; let fip = create_floating_ip( @@ -643,7 +646,7 @@ async fn test_floating_ip_create_attachment( let apictx = &cptestctx.server.server_context(); let nexus = &apictx.nexus; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; let project = create_project(client, PROJECT_NAME).await; let fip = create_floating_ip( @@ -742,7 +745,7 @@ async fn test_external_ip_live_attach_detach( let apictx = &cptestctx.server.server_context(); let nexus = &apictx.nexus; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; let project = create_project(client, PROJECT_NAME).await; const CAPACITY: f64 = 65536.0; @@ -983,7 +986,7 @@ async fn test_floating_ip_attach_fail_between_projects( let apictx = &cptestctx.server.server_context(); let _nexus = &apictx.nexus; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; let _project = create_project(client, PROJECT_NAME).await; let _project2 = create_project(client, "proj2").await; @@ -1034,7 +1037,7 @@ async fn test_floating_ip_attach_fail_between_projects( .to_vec(), ssh_public_keys: Some(Vec::new()), network_interfaces: - params::InstanceNetworkInterfaceAttachment::Default, + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![params::ExternalIpCreate::Floating { floating_ip: fip.identity.id.into(), }], @@ -1063,7 +1066,7 @@ async fn test_external_ip_attach_fail_if_in_use_by_other( let apictx = &cptestctx.server.server_context(); let nexus = &apictx.nexus; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; let project = create_project(client, PROJECT_NAME).await; // Create 2 instances, bind a FIP to each. @@ -1121,7 +1124,7 @@ async fn test_external_ip_attach_fails_after_maximum( ) { let client = &cptestctx.external_client; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; let project = create_project(client, PROJECT_NAME).await; // Create 33 floating IPs, and bind the first 32 to an instance. @@ -1199,7 +1202,7 @@ async fn test_external_ip_attach_ephemeral_at_pool_exhaustion( ) { let client = &cptestctx.external_client; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; let other_pool_range = IpRange::V4( Ipv4Range::new(Ipv4Addr::new(10, 1, 0, 1), Ipv4Addr::new(10, 1, 0, 1)) .unwrap(), @@ -1259,11 +1262,221 @@ async fn test_external_ip_attach_ephemeral_at_pool_exhaustion( assert_eq!(eph_resp_2, eph_resp); } +#[nexus_test] +async fn cannot_attach_floating_ipv4_to_instance_missing_ipv4_stack( + cptestctx: &ControlPlaneTestContext, +) { + let client = &cptestctx.external_client; + + let (v4_pool, _v6_pool) = create_default_ip_pools(&client).await; + create_project(client, PROJECT_NAME).await; + + // Create an instance with only a private IPv6 address. + let instance_name = &INSTANCE_NAMES[0]; + create_instance_with( + client, + PROJECT_NAME, + instance_name, + &InstanceNetworkInterfaceAttachment::DefaultIpv6, + vec![], + vec![], + false, + None, + None, + vec![], + ) + .await; + + // Create and attempt to attach a Floating IPv4 address. + let fip_name = "my-fip"; + let _fip = create_floating_ip( + client, + fip_name, + PROJECT_NAME, + None, + Some(v4_pool.identity.name.as_str()), + ) + .await; + let url = attach_floating_ip_url(fip_name, PROJECT_NAME); + NexusRequest::new( + RequestBuilder::new(client, Method::POST, &url) + .body(Some(¶ms::FloatingIpAttach { + kind: params::FloatingIpParentKind::Instance, + parent: instance_name.parse::().unwrap().into(), + })) + .expect_status(Some(StatusCode::BAD_REQUEST)), + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .expect_err( + "Should fail to attach IPv4 Floating IP to instance without private IPv4 stack" + ); + todo!("TODO(ben): Finish this test"); +} + +#[nexus_test] +async fn cannot_attach_floating_ipv6_to_instance_missing_ipv6_stack( + cptestctx: &ControlPlaneTestContext, +) { + let client = &cptestctx.external_client; + + let (_v4_pool, v6_pool) = create_default_ip_pools(&client).await; + create_project(client, PROJECT_NAME).await; + + // Create an instance with only a private IPv4 address. + let instance_name = &INSTANCE_NAMES[0]; + let inst = create_instance_with( + client, + PROJECT_NAME, + instance_name, + &InstanceNetworkInterfaceAttachment::DefaultIpv4, + vec![], + vec![], + false, + None, + None, + vec![], + ) + .await; + + // Create and attempt to attach a Floating IPv6 address. + let fip_name = "my-fip"; + let _fip = create_floating_ip( + client, + fip_name, + PROJECT_NAME, + None, + Some(v6_pool.identity.name.as_str()), + ) + .await; + let url = attach_floating_ip_url(fip_name, PROJECT_NAME); + let result = NexusRequest::new( + RequestBuilder::new(client, Method::POST, &url) + .body(Some(¶ms::FloatingIpAttach { + kind: params::FloatingIpParentKind::Instance, + parent: instance_name.parse::().unwrap().into(), + })) + .expect_status(Some(StatusCode::BAD_REQUEST)), + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap() + .parsed_body::() + .expect("an HTTP error making this API call"); + assert_eq!( + result.message, + format!( + "The floating external IP is an IPv6 address, but \ + the instance with ID {} does not have a primary \ + network interface with a VPC-private IPv6 address. \ + Add a VPC-private IPv6 address to the interface, \ + or attach a different IP address", + inst.identity.id, + ), + ); +} + +#[nexus_test] +async fn cannot_attach_ephemeral_ipv4_to_instance_missing_ipv4_stack( + cptestctx: &ControlPlaneTestContext, +) { + let client = &cptestctx.external_client; + + let (v4_pool, _v6_pool) = create_default_ip_pools(&client).await; + create_project(client, PROJECT_NAME).await; + + // Create an instance with only a private IPv6 address. + let instance_name = &INSTANCE_NAMES[0]; + create_instance_with( + client, + PROJECT_NAME, + instance_name, + &InstanceNetworkInterfaceAttachment::DefaultIpv6, + vec![], + vec![], + false, + None, + None, + vec![], + ) + .await; + + // Now try to attach an Ephemeral IPv4 address. + let url = instance_ephemeral_ip_url(instance_name, PROJECT_NAME); + let _err = NexusRequest::new( + RequestBuilder::new(client, Method::POST, &url) + .body(Some(¶ms::EphemeralIpCreate { + pool: Some(v4_pool.identity.name.clone().into()), + })) + .expect_status(Some(StatusCode::BAD_REQUEST)), + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .expect_err("Should fail attaching Ephemeral IPv4 address"); + todo!("TODO(ben): Finish this test"); +} + +#[nexus_test] +async fn cannot_attach_ephemeral_ipv6_to_instance_missing_ipv6_stack( + cptestctx: &ControlPlaneTestContext, +) { + let client = &cptestctx.external_client; + + let (_v4_pool, v6_pool) = create_default_ip_pools(&client).await; + create_project(client, PROJECT_NAME).await; + + // Create an instance with only a private IPv4 address. + let instance_name = &INSTANCE_NAMES[0]; + let inst = create_instance_with( + client, + PROJECT_NAME, + instance_name, + &InstanceNetworkInterfaceAttachment::DefaultIpv4, + vec![], + vec![], + false, + None, + None, + vec![], + ) + .await; + + // Now try to attach an Ephemeral IPv4 address. + let url = instance_ephemeral_ip_url(instance_name, PROJECT_NAME); + let result = NexusRequest::new( + RequestBuilder::new(client, Method::POST, &url) + .body(Some(¶ms::EphemeralIpCreate { + pool: Some(v6_pool.identity.name.clone().into()), + })) + .expect_status(Some(StatusCode::BAD_REQUEST)), + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap() + .parsed_body::() + .unwrap(); + assert_eq!( + result.message, + format!( + "The ephemeral external IP is an IPv6 address, but \ + the instance with ID {} does not have a primary \ + network interface with a VPC-private IPv6 address. \ + Add a VPC-private IPv6 address to the interface, \ + or attach a different IP address", + inst.identity.id, + ), + ); +} + #[nexus_test] async fn can_list_instance_snat_ip(cptestctx: &ControlPlaneTestContext) { let client = &cptestctx.external_client; - let pool = create_default_ip_pool(&client).await; + let (pool, _v6_pool) = create_default_ip_pools(&client).await; let _project = create_project(client, PROJECT_NAME).await; // Get the first address in the pool. @@ -1327,6 +1540,219 @@ async fn can_list_instance_snat_ip(cptestctx: &ControlPlaneTestContext) { assert_eq!(*last_port, NUM_SOURCE_NAT_PORTS - 1); } +// Sanity check that we can just attach an IPv6 Ephemeral address to an instance +// with a private IPv6 stack. +#[nexus_test] +async fn can_create_instance_with_ephemeral_ipv6_address( + cptestctx: &ControlPlaneTestContext, +) { + let client = &cptestctx.external_client; + + let (_pool, v6_pool) = create_default_ip_pools(&client).await; + let _project = create_project(client, PROJECT_NAME).await; + + // Get the first address in the IPv6 pool. + let range = NexusRequest::object_get( + client, + &format!("/v1/system/ip-pools/{}/ranges", v6_pool.identity.id), + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap_or_else(|e| panic!("failed to get IP pool range: {e}")) + .parsed_body::() + .unwrap_or_else(|e| panic!("failed to parse IP pool range: {e}")); + assert_eq!(range.items.len(), 1, "Should have 1 range in the pool"); + let oxide_client::types::IpRange::V6(oxide_client::types::Ipv6Range { + first, + .. + }) = &range.items[0].range + else { + panic!("Expected IPv6 range, found {:?}", &range.items[0]); + }; + let expected_ip = IpAddr::V6(*first); + + // Create a running instance with an Ephemeral IPv6 address. + let instance_name = INSTANCE_NAMES[0]; + let instance = create_instance_with( + &client, + PROJECT_NAME, + instance_name, + ¶ms::InstanceNetworkInterfaceAttachment::DefaultIpv6, + /* disks = */ vec![], + vec![params::ExternalIpCreate::Ephemeral { + pool: Some(v6_pool.identity.id.into()), + }], + /* start = */ false, + /* auto_restart_policy = */ Default::default(), + /* instance_cpu_platform = */ None, + /* multicast_groups = */ vec![], + ) + .await; + + // First, sanity check the SNAT IPv6 address. These are currently created + // unconditionally, but see + // https://github.com/oxidecomputer/omicron/issues/4317 for more details. + let url = format!("/v1/instances/{}/external-ips", instance.identity.id); + let page = NexusRequest::object_get(client, &url) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap_or_else(|e| { + panic!("failed to make \"get\" request to {url}: {e}") + }) + .parsed_body::() + .unwrap_or_else(|e| { + panic!("failed to make \"get\" request to {url}: {e}") + }); + let ips = page.items; + assert_eq!( + ips.len(), + 2, + "Instance should have been created with exactly 2 external IPs" + ); + let oxide_client::types::ExternalIp::Snat { + ip, + ip_pool_id, + first_port, + last_port, + } = &ips[0] + else { + panic!("Expected an SNAT external IP, found {:?}", &ips[0]); + }; + assert_eq!(ip_pool_id, &v6_pool.identity.id); + assert_eq!(ip, &expected_ip); + + // Port ranges are half-open on the right, e.g., [0, 16384). + assert_eq!(*first_port, 0); + assert_eq!(*last_port, NUM_SOURCE_NAT_PORTS - 1); + + // Now check the Ephemeral IPv6 address. + let oxide_client::types::ExternalIp::Ephemeral { ip, ip_pool_id } = &ips[1] + else { + panic!("Expected an Ephemeral external IP, found {:?}", &ips[1]); + }; + assert_eq!(ip_pool_id, &v6_pool.identity.id); + let expected_ip = IpAddr::V6(Ipv6Addr::from_bits(first.to_bits() + 1)); + assert_eq!(ip, &expected_ip); +} + +// Sanity check that we can just attach an IPv6 Floating address to an instance +// with a private IPv6 stack. +#[nexus_test] +async fn can_create_instance_with_floating_ipv6_address( + cptestctx: &ControlPlaneTestContext, +) { + let client = &cptestctx.external_client; + + let (_pool, v6_pool) = create_default_ip_pools(&client).await; + let project = create_project(client, PROJECT_NAME).await; + + // Get the first address in the IPv6 pool. + let range = NexusRequest::object_get( + client, + &format!("/v1/system/ip-pools/{}/ranges", v6_pool.identity.id), + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap_or_else(|e| panic!("failed to get IP pool range: {e}")) + .parsed_body::() + .unwrap_or_else(|e| panic!("failed to parse IP pool range: {e}")); + assert_eq!(range.items.len(), 1, "Should have 1 range in the pool"); + let oxide_client::types::IpRange::V6(oxide_client::types::Ipv6Range { + first, + .. + }) = &range.items[0].range + else { + panic!("Expected IPv6 range, found {:?}", &range.items[0]); + }; + let expected_ip = IpAddr::V6(*first); + + // Create a floating IP, from the IPv6 Pool. + let fip_name = FIP_NAMES[0]; + let fip = create_floating_ip( + client, + fip_name, + &project.identity.id.to_string(), + None, + Some(v6_pool.identity.name.as_str()), + ) + .await; + + // Create a running instance with that Floating IPv6 address. + let instance_name = INSTANCE_NAMES[0]; + let instance = create_instance_with( + &client, + PROJECT_NAME, + instance_name, + ¶ms::InstanceNetworkInterfaceAttachment::DefaultIpv6, + /* disks = */ vec![], + vec![params::ExternalIpCreate::Floating { + floating_ip: NameOrId::Id(fip.identity.id), + }], + /* start = */ false, + /* auto_restart_policy = */ Default::default(), + /* instance_cpu_platform = */ None, + /* multicast_groups = */ vec![], + ) + .await; + + // First, sanity check the SNAT IPv6 address. These are currently created + // unconditionally, but see + // https://github.com/oxidecomputer/omicron/issues/4317 for more details. + let url = format!("/v1/instances/{}/external-ips", instance.identity.id); + let page = NexusRequest::object_get(client, &url) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap_or_else(|e| { + panic!("failed to make \"get\" request to {url}: {e}") + }) + .parsed_body::() + .unwrap_or_else(|e| { + panic!("failed to make \"get\" request to {url}: {e}") + }); + let ips = page.items; + assert_eq!( + ips.len(), + 2, + "Instance should have been created with exactly 2 external IPs" + ); + let oxide_client::types::ExternalIp::Snat { + ip, + ip_pool_id, + first_port, + last_port, + } = &ips[0] + else { + panic!("Expected an SNAT external IP, found {:?}", &ips[0]); + }; + assert_eq!(ip_pool_id, &v6_pool.identity.id); + assert_eq!(ip, &expected_ip); + + // Port ranges are half-open on the right, e.g., [0, 16384). + assert_eq!(*first_port, 0); + assert_eq!(*last_port, NUM_SOURCE_NAT_PORTS - 1); + + // Now check the Floating IPv6 address. + let oxide_client::types::ExternalIp::Floating { + id, + instance_id, + ip, + ip_pool_id, + .. + } = &ips[1] + else { + panic!("Expected a Floating external IP, found {:?}", &ips[1]); + }; + assert_eq!(id, &fip.identity.id); + assert_eq!(instance_id, &Some(instance.identity.id)); + assert_eq!(ip_pool_id, &v6_pool.identity.id); + let expected_ip = IpAddr::V6(Ipv6Addr::from_bits(first.to_bits() + 1)); + assert_eq!(ip, &expected_ip); +} + pub async fn floating_ip_get( client: &ClientTestContext, fip_url: &str, @@ -1372,7 +1798,7 @@ async fn instance_for_external_ips( &client, PROJECT_NAME, instance_name, - ¶ms::InstanceNetworkInterfaceAttachment::Default, + ¶ms::InstanceNetworkInterfaceAttachment::DefaultIpv4, vec![], fips, start, diff --git a/nexus/tests/integration_tests/instances.rs b/nexus/tests/integration_tests/instances.rs index ff0484bb04d..88444697958 100644 --- a/nexus/tests/integration_tests/instances.rs +++ b/nexus/tests/integration_tests/instances.rs @@ -22,7 +22,7 @@ use nexus_test_utils::http_testing::NexusRequest; use nexus_test_utils::http_testing::RequestBuilder; use nexus_test_utils::resource_helpers::DiskTest; use nexus_test_utils::resource_helpers::assert_ip_pool_utilization; -use nexus_test_utils::resource_helpers::create_default_ip_pool; +use nexus_test_utils::resource_helpers::create_default_ip_pools; use nexus_test_utils::resource_helpers::create_disk; use nexus_test_utils::resource_helpers::create_floating_ip; use nexus_test_utils::resource_helpers::create_ip_pool; @@ -41,6 +41,10 @@ use nexus_test_utils::resource_helpers::objects_list_page_authz; use nexus_test_utils::resource_helpers::test_params; use nexus_test_utils::start_sled_agent_with_config; use nexus_test_utils::wait_for_producer; +use nexus_types::external_api::params::IpAssignment; +use nexus_types::external_api::params::PrivateIpStackCreate; +use nexus_types::external_api::params::PrivateIpv4StackCreate; +use nexus_types::external_api::params::PrivateIpv6StackCreate; use nexus_types::external_api::params::SshKeyCreate; use nexus_types::external_api::shared::IpKind; use nexus_types::external_api::shared::IpRange; @@ -48,6 +52,7 @@ use nexus_types::external_api::shared::Ipv4Range; use nexus_types::external_api::shared::SiloIdentityMode; use nexus_types::external_api::views::Sled; use nexus_types::external_api::views::SshKey; +use nexus_types::external_api::views::VpcSubnet; use nexus_types::external_api::{params, views}; use nexus_types::identity::Resource; use nexus_types::internal_api::params::InstanceMigrateRequest; @@ -69,6 +74,7 @@ use omicron_common::api::external::InstanceState; use omicron_common::api::external::Name; use omicron_common::api::external::NameOrId; use omicron_common::api::external::Nullable; +use omicron_common::api::external::PrivateIpStack; use omicron_common::api::external::Vni; use omicron_common::api::internal::shared::ResolvedVpcRoute; use omicron_common::api::internal::shared::RouterId; @@ -86,6 +92,7 @@ use omicron_uuid_kinds::{GenericUuid, InstanceUuid}; use sled_agent_client::TestInterfaces as _; use std::collections::HashSet; use std::convert::TryFrom; +use std::net::IpAddr; use std::net::Ipv4Addr; use std::sync::Arc; use std::time::Duration; @@ -146,7 +153,7 @@ const SLEDS_URL: &'static str = "/v1/system/hardware/sleds"; pub async fn create_project_and_pool( client: &ClientTestContext, ) -> views::Project { - create_default_ip_pool(client).await; + create_default_ip_pools(client).await; create_project(client, PROJECT_NAME).await } @@ -350,7 +357,7 @@ async fn test_instances_create_reboot_halt( user_data: vec![], ssh_public_keys: None, network_interfaces: - params::InstanceNetworkInterfaceAttachment::Default, + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], disks: vec![], boot_disk: None, @@ -764,7 +771,7 @@ async fn test_instance_migrate(cptestctx: &ControlPlaneTestContext) { client, PROJECT_NAME, instance_name, - ¶ms::InstanceNetworkInterfaceAttachment::Default, + ¶ms::InstanceNetworkInterfaceAttachment::DefaultIpv4, Vec::::new(), Vec::::new(), true, @@ -937,7 +944,7 @@ async fn test_instance_migrate_v2p_and_routes( client, PROJECT_NAME, instance_name, - ¶ms::InstanceNetworkInterfaceAttachment::Default, + ¶ms::InstanceNetworkInterfaceAttachment::DefaultIpv4, // Omit disks: simulated sled agent assumes that disks are always co- // located with their instances. Vec::::new(), @@ -1154,7 +1161,7 @@ async fn test_instance_migration_compatible_cpu_platforms( client, PROJECT_NAME, instance_name, - ¶ms::InstanceNetworkInterfaceAttachment::Default, + ¶ms::InstanceNetworkInterfaceAttachment::DefaultIpv4, Vec::::new(), Vec::::new(), true, @@ -1343,7 +1350,7 @@ async fn test_instance_migration_incompatible_cpu_platforms( client, PROJECT_NAME, instance_name, - ¶ms::InstanceNetworkInterfaceAttachment::Default, + ¶ms::InstanceNetworkInterfaceAttachment::DefaultIpv4, Vec::::new(), Vec::::new(), true, @@ -1420,7 +1427,7 @@ async fn test_instance_migration_unknown_sled_type( client, PROJECT_NAME, instance_name, - ¶ms::InstanceNetworkInterfaceAttachment::Default, + ¶ms::InstanceNetworkInterfaceAttachment::DefaultIpv4, Vec::::new(), Vec::::new(), true, @@ -1676,7 +1683,7 @@ async fn test_instance_failed_when_on_expunged_sled( client, PROJECT_NAME, name, - ¶ms::InstanceNetworkInterfaceAttachment::Default, + ¶ms::InstanceNetworkInterfaceAttachment::DefaultIpv4, // Disks= Vec::::new(), // External IPs= @@ -2027,7 +2034,7 @@ async fn make_forgotten_instance( client, PROJECT_NAME, instance_name, - ¶ms::InstanceNetworkInterfaceAttachment::Default, + ¶ms::InstanceNetworkInterfaceAttachment::DefaultIpv4, // Disks= Vec::::new(), // External IPs= @@ -2260,7 +2267,7 @@ async fn test_instance_metrics_with_migration( client, PROJECT_NAME, instance_name, - ¶ms::InstanceNetworkInterfaceAttachment::Default, + ¶ms::InstanceNetworkInterfaceAttachment::DefaultIpv4, Vec::::new(), Vec::::new(), true, @@ -2429,7 +2436,7 @@ async fn test_instances_create_stopped_start( user_data: vec![], ssh_public_keys: None, network_interfaces: - params::InstanceNetworkInterfaceAttachment::Default, + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], disks: vec![], boot_disk: None, @@ -2604,7 +2611,7 @@ async fn test_instance_using_image_from_other_project_fails( user_data: vec![], ssh_public_keys: None, network_interfaces: - params::InstanceNetworkInterfaceAttachment::Default, + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], disks: vec![params::InstanceDiskAttachment::Create( params::DiskCreate { @@ -2659,7 +2666,8 @@ async fn test_instance_create_saga_removes_instance_database_record( // The network interface parameters. let default_name = "default".parse::().unwrap(); - let requested_address = "172.30.0.10".parse::().unwrap(); + let requested_address = + "172.30.0.10".parse::().unwrap(); let if0_params = params::InstanceNetworkInterfaceCreate { identity: IdentityMetadataCreateParams { name: Name::try_from(String::from("if0")).unwrap(), @@ -2667,8 +2675,7 @@ async fn test_instance_create_saga_removes_instance_database_record( }, vpc_name: default_name.clone(), subnet_name: default_name.clone(), - ip: Some(requested_address), - transit_ips: vec![], + ip_config: PrivateIpStackCreate::from_ipv4(requested_address), }; let interface_params = params::InstanceNetworkInterfaceAttachment::Create(vec![ @@ -2745,7 +2752,8 @@ async fn test_instance_create_saga_removes_instance_database_record( // Update the IP address to one that will succeed, but leave the other data // as-is. This would fail with a conflict on the instance name, if we don't // fully unwind the saga and delete the instance database record. - let requested_address = "172.30.0.11".parse::().unwrap(); + let requested_address = + "172.30.0.11".parse::().unwrap(); let if0_params = params::InstanceNetworkInterfaceCreate { identity: IdentityMetadataCreateParams { name: Name::try_from(String::from("if0")).unwrap(), @@ -2753,8 +2761,7 @@ async fn test_instance_create_saga_removes_instance_database_record( }, vpc_name: default_name.clone(), subnet_name: default_name.clone(), - ip: Some(requested_address), - transit_ips: vec![], + ip_config: PrivateIpStackCreate::from_ipv4(requested_address), }; let interface_params = params::InstanceNetworkInterfaceAttachment::Create(vec![ @@ -2777,18 +2784,78 @@ async fn test_instance_create_saga_removes_instance_database_record( assert_eq!(instance.identity.name, instance_params.identity.name); } -// Basic test requesting an interface with a specific IP address. #[nexus_test] -async fn test_instance_with_single_explicit_ip_address( +async fn test_instance_with_single_explicit_ipv4_address( cptestctx: &ControlPlaneTestContext, ) { let client = &cptestctx.external_client; + let _ = create_project_and_pool(&client).await; + let cfg = PrivateIpStackCreate::from_ipv4("172.30.0.10".parse().unwrap()); + test_instance_with_single_explicit_ip_address_impl(client, cfg).await; +} - create_project_and_pool(&client).await; +#[nexus_test] +async fn test_instance_with_single_explicit_ipv6_address( + cptestctx: &ControlPlaneTestContext, +) { + let client = &cptestctx.external_client; + + // Need to fetch the VPC Subnet's IPv6 prefix, to create an address in it. + let project = create_project_and_pool(&client).await; + let url = format!( + "/v1/vpc-subnets/default?project={}&vpc=default", + project.identity.name + ); + let subnet = NexusRequest::object_get(client, &url) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .expect("Failed to get VPC Subnet") + .parsed_body::() + .expect("Failed to parse a VPC Subnet"); + let cfg = PrivateIpStackCreate::from_ipv6( + subnet.ipv6_block.iter().nth(100).unwrap(), + ); + test_instance_with_single_explicit_ip_address_impl(client, cfg).await; +} +#[nexus_test] +async fn test_instance_with_explicit_dual_stack_address( + cptestctx: &ControlPlaneTestContext, +) { + let client = &cptestctx.external_client; + + // Need to fetch the VPC Subnet's IPv6 prefix, to create an address in it. + let project = create_project_and_pool(&client).await; + let url = format!( + "/v1/vpc-subnets/default?project={}&vpc=default", + project.identity.name + ); + let subnet = NexusRequest::object_get(client, &url) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .expect("Failed to get VPC Subnet") + .parsed_body::() + .expect("Failed to parse a VPC Subnet"); + let v4 = PrivateIpv4StackCreate { + ip: IpAssignment::Explicit("172.30.0.10".parse().unwrap()), + transit_ips: vec![], + }; + let v6 = PrivateIpv6StackCreate { + ip: IpAssignment::Explicit(subnet.ipv6_block.iter().nth(100).unwrap()), + transit_ips: vec![], + }; + let cfg = PrivateIpStackCreate::DualStack { v4, v6 }; + test_instance_with_single_explicit_ip_address_impl(client, cfg).await; +} + +async fn test_instance_with_single_explicit_ip_address_impl( + client: &ClientTestContext, + ip_config: PrivateIpStackCreate, +) { // Create the parameters for the interface. let default_name = "default".parse::().unwrap(); - let requested_address = "172.30.0.10".parse::().unwrap(); let if0_params = params::InstanceNetworkInterfaceCreate { identity: IdentityMetadataCreateParams { name: Name::try_from(String::from("if0")).unwrap(), @@ -2796,8 +2863,7 @@ async fn test_instance_with_single_explicit_ip_address( }, vpc_name: default_name.clone(), subnet_name: default_name.clone(), - ip: Some(requested_address), - transit_ips: vec![], + ip_config: ip_config.clone(), }; let interface_params = params::InstanceNetworkInterfaceAttachment::Create(vec![ @@ -2853,10 +2919,74 @@ async fn test_instance_with_single_explicit_ip_address( .expect("Failed to parse a network interface"); assert_eq!(interface.instance_id, instance.identity.id); assert_eq!(interface.identity.name, if0_params.identity.name); - assert_eq!( - interface.ip, requested_address, - "Interface was not assigned the requested IP address" - ); + + let ip_stack = &interface.ip_stack; + match (ip_stack, &ip_config) { + (PrivateIpStack::V4(stack), PrivateIpStackCreate::V4(config)) => { + let IpAssignment::Explicit(requested_ip) = config.ip else { + panic!("Expected an explicit requested address"); + }; + assert_eq!( + stack.ip, requested_ip, + "Interface was not assigned the requested IP address", + ); + assert_eq!( + stack.transit_ips, config.transit_ips, + "Interface was not assigned the requested transit IPs", + ); + } + (PrivateIpStack::V6(stack), PrivateIpStackCreate::V6(config)) => { + let IpAssignment::Explicit(requested_ip) = config.ip else { + panic!("Expected an explicit requested address"); + }; + assert_eq!( + stack.ip, requested_ip, + "Interface was not assigned the requested IP address", + ); + assert_eq!( + stack.transit_ips, config.transit_ips, + "Interface was not assigned the requested transit IPs", + ); + } + ( + PrivateIpStack::DualStack { v4: v4_stack, v6: v6_stack }, + PrivateIpStackCreate::DualStack { v4: v4_config, v6: v6_config }, + ) => { + let IpAssignment::Explicit(requested_ip) = v4_config.ip else { + panic!("Expected an explicit requested address"); + }; + assert_eq!( + v4_stack.ip, requested_ip, + "Interface was not assigned the requested IP address", + ); + assert_eq!( + v4_stack.transit_ips, v4_config.transit_ips, + "Interface was not assigned the requested transit IPs", + ); + let IpAssignment::Explicit(requested_ip) = v6_config.ip else { + panic!("Expected an explicit requested address"); + }; + assert_eq!( + v6_stack.ip, requested_ip, + "Interface was not assigned the requested IP address", + ); + assert_eq!( + v6_stack.transit_ips, v6_config.transit_ips, + "Interface was not assigned the requested transit IPs", + ); + } + (PrivateIpStack::V4(_), PrivateIpStackCreate::DualStack { .. }) + | (PrivateIpStack::V4(_), PrivateIpStackCreate::V6(_)) + | (PrivateIpStack::V6(_), PrivateIpStackCreate::V4(_)) + | (PrivateIpStack::V6(_), PrivateIpStackCreate::DualStack { .. }) + | (PrivateIpStack::DualStack { .. }, PrivateIpStackCreate::V4(_)) + | (PrivateIpStack::DualStack { .. }, PrivateIpStackCreate::V6(_)) => { + panic!( + "Created IP stack does not match requested config: \ + config = {ip_config:#?}, stack = {ip_stack:#?}" + ) + } + } } // Test creating two new interfaces for an instance, at creation time. @@ -2883,7 +3013,7 @@ async fn test_instance_with_new_custom_network_interfaces( ipv6_block: None, custom_router: None, }; - let _response = NexusRequest::objects_post( + let non_default_vpc_subnet = NexusRequest::objects_post( client, &default_vpc_subnets_url(), &vpc_subnet_params, @@ -2891,13 +3021,9 @@ async fn test_instance_with_new_custom_network_interfaces( .authn_as(AuthnMode::PrivilegedUser) .execute() .await - .expect("Failed to create custom VPC Subnet"); - - // TODO-coverage: We'd like to assert things about this VPC Subnet we just - // created, but the `vpc_subnets_post` endpoint in Nexus currently returns - // the "private" `omicron_nexus::db::model::VpcSubnet` type. That should be - // converted to return the public `omicron_common::external` type, which is - // work tracked in https://github.com/oxidecomputer/omicron/issues/388. + .expect("Failed to create custom VPC Subnet") + .parsed_body::() + .unwrap(); // Create the parameters for the interfaces. These will be created during // the saga for instance creation. @@ -2908,8 +3034,7 @@ async fn test_instance_with_new_custom_network_interfaces( }, vpc_name: default_name.clone(), subnet_name: default_name.clone(), - ip: None, - transit_ips: vec![], + ip_config: PrivateIpStackCreate::auto_ipv4(), }; let if1_params = params::InstanceNetworkInterfaceCreate { identity: IdentityMetadataCreateParams { @@ -2918,8 +3043,7 @@ async fn test_instance_with_new_custom_network_interfaces( }, vpc_name: default_name.clone(), subnet_name: non_default_subnet_name.clone(), - ip: None, - transit_ips: vec![], + ip_config: PrivateIpStackCreate::auto_dual_stack(), }; let interface_params = params::InstanceNetworkInterfaceAttachment::Create(vec![ @@ -2984,7 +3108,9 @@ async fn test_instance_with_new_custom_network_interfaces( assert_eq!(if0.identity.name, if0_params.identity.name); assert_eq!(if0.identity.description, if0_params.identity.description); assert_eq!(if0.instance_id, instance.identity.id); - assert_eq!(if0.ip, std::net::IpAddr::V4("172.30.0.5".parse().unwrap())); + let ipv4_stack = if0.ip_stack.ipv4_stack().expect("Expected an IPv4 stack"); + assert_eq!(ipv4_stack.ip, "172.30.0.5".parse::().unwrap()); + assert!(ipv4_stack.transit_ips.is_empty()); let interfaces1 = NexusRequest::iter_collection_authn::( @@ -3001,20 +3127,30 @@ async fn test_instance_with_new_custom_network_interfaces( "Should be a single interface in the non-default subnet" ); let if1 = &interfaces1.all_items[0]; - - // TODO-coverage: Add this test once the `VpcSubnet` type can be - // deserialized. - // assert_eq!(if1.subnet_id, non_default_vpc_subnet.id); + assert_eq!(if1.subnet_id, non_default_vpc_subnet.identity.id); assert_eq!(if1.identity.name, if1_params.identity.name); assert_eq!(if1.identity.description, if1_params.identity.description); - assert_eq!(if1.ip, std::net::IpAddr::V4("172.31.0.5".parse().unwrap())); + assert!(if1.ip_stack.is_dual_stack()); + let ipv4_stack = if1.ip_stack.ipv4_stack().expect("Expected an IPv4 stack"); + assert_eq!(ipv4_stack.ip, "172.31.0.5".parse::().unwrap()); + assert!(ipv4_stack.transit_ips.is_empty()); + let ipv6_stack = if1.ip_stack.ipv6_stack().expect("An IPv6 stack"); + assert!( + non_default_vpc_subnet.ipv6_block.contains(ipv6_stack.ip), + "Auto-assigned IPv6 address {} isn't within the VPC Subnet's \ + IPv6 block {}", + ipv6_stack.ip, + non_default_vpc_subnet.ipv6_block, + ); + assert!(ipv6_stack.transit_ips.is_empty()); assert_eq!(if1.instance_id, instance.identity.id); assert_eq!(if0.vpc_id, if1.vpc_id); assert_ne!( if0.subnet_id, if1.subnet_id, "Two interfaces should be created in different subnets" ); + assert_eq!(if1.subnet_id, non_default_vpc_subnet.identity.id); } #[nexus_test] @@ -3103,11 +3239,13 @@ async fn test_instance_create_delete_network_interface( }, vpc_name: "default".parse().unwrap(), subnet_name: "default".parse().unwrap(), - ip: Some("172.30.0.10".parse().unwrap()), - transit_ips: vec![ - "10.0.0.0/24".parse().unwrap(), - "10.1.0.0/24".parse().unwrap(), - ], + ip_config: PrivateIpStackCreate::V4(PrivateIpv4StackCreate { + ip: IpAssignment::Explicit("172.30.0.10".parse().unwrap()), + transit_ips: vec![ + "10.0.0.0/24".parse().unwrap(), + "10.1.0.0/24".parse().unwrap(), + ], + }), }, params::InstanceNetworkInterfaceCreate { identity: IdentityMetadataCreateParams { @@ -3116,8 +3254,10 @@ async fn test_instance_create_delete_network_interface( }, vpc_name: "default".parse().unwrap(), subnet_name: secondary_subnet.identity.name.clone(), - ip: Some("172.31.0.11".parse().unwrap()), - transit_ips: vec!["192.168.1.0/24".parse().unwrap()], + ip_config: PrivateIpStackCreate::V4(PrivateIpv4StackCreate { + ip: IpAssignment::Explicit("172.31.0.11".parse().unwrap()), + transit_ips: vec!["192.168.1.0/24".parse().unwrap()], + }), }, ]; @@ -3165,13 +3305,26 @@ async fn test_instance_create_delete_network_interface( .expect("Failed to create network interface on stopped instance"); let iface = response.parsed_body::().unwrap(); assert_eq!(iface.identity.name, params.identity.name); - assert_eq!(iface.ip, params.ip.unwrap()); assert_eq!( iface.primary, i == 0, "Only the first interface should be primary" ); - assert_eq!(iface.transit_ips, params.transit_ips); + + let ipv4_stack = + iface.ip_stack.ipv4_stack().expect("Expected an IPv4 stack"); + let ipv4_addr = + params.ip_config.ipv4_addr().expect("An explicit IPv4 address"); + assert_eq!(ipv4_stack.ip, *ipv4_addr); + assert_eq!( + ipv4_stack + .transit_ips + .iter() + .copied() + .map(oxnet::IpNet::V4) + .collect::>(), + params.ip_config.transit_ips(), + ); interfaces.push(iface); } @@ -3190,9 +3343,14 @@ async fn test_instance_create_delete_network_interface( assert_eq!(iface0.identity.id, iface1.identity.id); assert_eq!(iface0.vpc_id, iface1.vpc_id); assert_eq!(iface0.subnet_id, iface1.subnet_id); - assert_eq!(iface0.ip, iface1.ip); assert_eq!(iface0.primary, iface1.primary); - assert_eq!(iface0.transit_ips, iface1.transit_ips); + + let ipv4_stack0 = + iface0.ip_stack.ipv4_stack().expect("Expected an IPv4 stack"); + let ipv4_stack1 = + iface1.ip_stack.ipv4_stack().expect("Expected an IPv4 stack"); + assert_eq!(ipv4_stack0.ip, ipv4_stack1.ip); + assert_eq!(ipv4_stack0.transit_ips, ipv4_stack1.transit_ips); } // Verify we cannot delete either interface while the instance is running @@ -3348,8 +3506,9 @@ async fn test_instance_update_network_interfaces( }, vpc_name: "default".parse().unwrap(), subnet_name: "default".parse().unwrap(), - ip: Some("172.30.0.10".parse().unwrap()), - transit_ips: vec![], + ip_config: PrivateIpStackCreate::from_ipv4( + "172.30.0.10".parse().unwrap(), + ), }, params::InstanceNetworkInterfaceCreate { identity: IdentityMetadataCreateParams { @@ -3358,8 +3517,9 @@ async fn test_instance_update_network_interfaces( }, vpc_name: "default".parse().unwrap(), subnet_name: secondary_subnet.identity.name.clone(), - ip: Some("172.31.0.11".parse().unwrap()), - transit_ips: vec![], + ip_config: PrivateIpStackCreate::from_ipv4( + "172.31.0.11".parse().unwrap(), + ), }, ]; @@ -3382,8 +3542,17 @@ async fn test_instance_update_network_interfaces( .parsed_body::() .unwrap(); assert_eq!(primary_iface.identity.name, if_params[0].identity.name); - assert_eq!(primary_iface.ip, if_params[0].ip.unwrap()); assert!(primary_iface.primary, "The first interface should be primary"); + let ipv4_stack = + primary_iface.ip_stack.ipv4_stack().expect("Expected an IPv4 stack"); + assert_eq!( + ipv4_stack.ip, + if_params[0] + .ip_config + .ipv4_addr() + .copied() + .expect("An explicit IPv4 address") + ); // Restart the instance, to ensure we can only modify things when it's // stopped. @@ -3462,7 +3631,15 @@ async fn test_instance_update_network_interfaces( original_iface.identity.time_modified < new_iface.identity.time_modified ); - assert_eq!(original_iface.ip, new_iface.ip); + let original_ipv4_stack = original_iface + .ip_stack + .ipv4_stack() + .expect("Expected an IPv4 stack"); + let new_ipv4_stack = new_iface + .ip_stack + .ipv4_stack() + .expect("Expected an IPv4 stack"); + assert_eq!(original_ipv4_stack, new_ipv4_stack); assert_eq!(original_iface.mac, new_iface.mac); assert_eq!(original_iface.subnet_id, new_iface.subnet_id); assert_eq!(original_iface.vpc_id, new_iface.vpc_id); @@ -3525,7 +3702,10 @@ async fn test_instance_update_network_interfaces( .parsed_body::() .unwrap(); assert_eq!(secondary_iface.identity.name, if_params[1].identity.name); - assert_eq!(secondary_iface.ip, if_params[1].ip.unwrap()); + assert_eq!( + secondary_iface.ip_stack.ipv4_addr().expect("Expected an IPv4 stack"), + if_params[1].ip_config.ipv4_addr().expect("An explicit IPv4 address"), + ); assert!( !secondary_iface.primary, "Only the first interface should be primary" @@ -3669,6 +3849,27 @@ async fn test_instance_update_network_interfaces( assert_eq!(iface.identity.name, if_params[0].identity.name); } +#[nexus_test] +async fn can_add_instance_network_interface_ip_stack( + _cptestctx: &ControlPlaneTestContext, +) { + todo!() +} + +#[nexus_test] +async fn can_remove_instance_network_interface_ip_stack( + _cptestctx: &ControlPlaneTestContext, +) { + todo!() +} + +#[nexus_test] +async fn cannot_remove_all_network_interface_private_ip_stacks( + _cptestctx: &ControlPlaneTestContext, +) { + todo!() +} + #[nexus_test] async fn test_instance_update_network_interface_transit_ips( cptestctx: &ControlPlaneTestContext, @@ -3684,7 +3885,7 @@ async fn test_instance_update_network_interface_transit_ips( &client, PROJECT_NAME, instance_name, - ¶ms::InstanceNetworkInterfaceAttachment::Default, + ¶ms::InstanceNetworkInterfaceAttachment::DefaultIpv4, vec![], vec![], false, @@ -3717,7 +3918,10 @@ async fn test_instance_update_network_interface_transit_ips( let updated_nic: InstanceNetworkInterface = object_put(client, &url_interface, &base_update).await; - assert_eq!(base_update.transit_ips, updated_nic.transit_ips); + assert_eq!( + base_update.transit_ips, + updated_nic.ip_stack.all_transit_ips().collect::>() + ); // Non-canonical form (e.g., host identifier is nonzero) subnets should // be rejected. @@ -3885,7 +4089,10 @@ async fn test_instance_update_network_interface_transit_ips( // ...and in the end, no changes have applied. let final_nic: InstanceNetworkInterface = object_get(client, &url_interface).await; - assert_eq!(updated_nic.transit_ips, final_nic.transit_ips); + assert_eq!( + updated_nic.ip_stack.all_transit_ips().collect::>(), + final_nic.ip_stack.all_transit_ips().collect::>() + ); // As a final sanity test, we can still effectively remove spoof checking // using the unspecified network address. @@ -3897,7 +4104,10 @@ async fn test_instance_update_network_interface_transit_ips( let updated_nic: InstanceNetworkInterface = object_put(client, &url_interface, &allow_all).await; - assert_eq!(allow_all.transit_ips, updated_nic.transit_ips); + assert_eq!( + allow_all.transit_ips, + updated_nic.ip_stack.all_transit_ips().collect::>(), + ); } /// This test specifically creates two NICs, the second of which will fail the @@ -3923,8 +4133,9 @@ async fn test_instance_with_multiple_nics_unwinds_completely( }, vpc_name: default_name.clone(), subnet_name: default_name.clone(), - ip: Some("172.30.0.6".parse().unwrap()), - transit_ips: vec![], + ip_config: PrivateIpStackCreate::from_ipv4( + "172.30.0.6".parse().unwrap(), + ), }; let if1_params = params::InstanceNetworkInterfaceCreate { identity: IdentityMetadataCreateParams { @@ -3933,8 +4144,9 @@ async fn test_instance_with_multiple_nics_unwinds_completely( }, vpc_name: default_name.clone(), subnet_name: default_name.clone(), - ip: Some("172.30.0.7".parse().unwrap()), - transit_ips: vec![], + ip_config: PrivateIpStackCreate::from_ipv4( + "172.30.0.7".parse().unwrap(), + ), }; let interface_params = params::InstanceNetworkInterfaceAttachment::Create(vec![ @@ -4026,7 +4238,8 @@ async fn test_attach_one_disk_to_instance(cptestctx: &ControlPlaneTestContext) { hostname: "nfs".parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], boot_disk: Some(params::InstanceDiskAttachment::Attach( params::InstanceDiskAttach { name: disk_name.clone() }, @@ -4089,7 +4302,8 @@ async fn test_instance_create_attach_disks( hostname: "nfs".parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], boot_disk: Some(params::InstanceDiskAttachment::Create( params::DiskCreate { @@ -4208,7 +4422,8 @@ async fn test_instance_create_attach_disks_undo( hostname: "nfs".parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], disks: vec![ params::InstanceDiskAttachment::Create(params::DiskCreate { @@ -4300,7 +4515,8 @@ async fn test_attach_eight_disks_to_instance( hostname: "nfs".parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], boot_disk: Some(params::InstanceDiskAttachment::Attach( params::InstanceDiskAttach { @@ -4390,7 +4606,8 @@ async fn test_cannot_attach_nine_disks_to_instance( hostname: "nfs".parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], boot_disk: Some(params::InstanceDiskAttachment::Attach( params::InstanceDiskAttach { @@ -4494,7 +4711,8 @@ async fn test_cannot_attach_faulted_disks(cptestctx: &ControlPlaneTestContext) { hostname: "nfs".parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], boot_disk: Some(params::InstanceDiskAttachment::Attach( params::InstanceDiskAttach { @@ -4587,7 +4805,8 @@ async fn test_disks_detached_when_instance_destroyed( hostname: "nfs".parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], boot_disk: Some(params::InstanceDiskAttachment::Attach( params::InstanceDiskAttach { @@ -4687,7 +4906,8 @@ async fn test_disks_detached_when_instance_destroyed( hostname: "nfsv2".parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], boot_disk: Some(params::InstanceDiskAttachment::Attach( params::InstanceDiskAttach { @@ -4775,7 +4995,8 @@ async fn test_duplicate_disk_attach_requests_ok( hostname: "nfs".parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], disks: vec![ params::InstanceDiskAttachment::Attach( @@ -4824,7 +5045,8 @@ async fn test_duplicate_disk_attach_requests_ok( hostname: "nfs2".parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], boot_disk: Some(params::InstanceDiskAttachment::Attach( params::InstanceDiskAttach { @@ -4887,7 +5109,8 @@ async fn test_cannot_detach_boot_disk(cptestctx: &ControlPlaneTestContext) { hostname: "nfs".parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], boot_disk: Some(params::InstanceDiskAttachment::Attach( params::InstanceDiskAttach { @@ -5020,7 +5243,8 @@ async fn test_updating_running_instance_boot_disk_is_conflict( hostname: "inst".parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], disks: vec![ params::InstanceDiskAttachment::Attach( @@ -5200,7 +5424,8 @@ async fn test_size_can_be_changed(cptestctx: &ControlPlaneTestContext) { hostname: instance_name.parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], boot_disk: None, cpu_platform: None, @@ -5418,7 +5643,8 @@ async fn test_auto_restart_policy_can_be_changed( hostname: instance_name.parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], boot_disk: None, cpu_platform: None, @@ -5493,7 +5719,8 @@ async fn test_cpu_platform_can_be_changed(cptestctx: &ControlPlaneTestContext) { hostname: instance_name.parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], boot_disk: None, // Start out with None @@ -5583,7 +5810,8 @@ async fn test_boot_disk_can_be_changed(cptestctx: &ControlPlaneTestContext) { hostname: "nfs".parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], boot_disk: Some(params::InstanceDiskAttachment::Attach( params::InstanceDiskAttach { @@ -5665,7 +5893,8 @@ async fn test_boot_disk_must_be_attached(cptestctx: &ControlPlaneTestContext) { hostname: "nfs".parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], disks: vec![], boot_disk: None, @@ -5763,7 +5992,8 @@ async fn test_instances_memory_rejected_less_than_min_memory_size( b"#cloud-config\nsystem_info:\n default_user:\n name: oxide" .to_vec(), ssh_public_keys: None, - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], disks: vec![], boot_disk: None, @@ -5818,7 +6048,8 @@ async fn test_instances_memory_not_divisible_by_min_memory_size( b"#cloud-config\nsystem_info:\n default_user:\n name: oxide" .to_vec(), ssh_public_keys: None, - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], disks: vec![], boot_disk: None, @@ -5873,7 +6104,8 @@ async fn test_instances_memory_greater_than_max_size( b"#cloud-config\nsystem_info:\n default_user:\n name: oxide" .to_vec(), ssh_public_keys: None, - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], disks: vec![], boot_disk: None, @@ -5983,7 +6215,8 @@ async fn test_instance_create_with_anti_affinity_groups( multicast_groups: Vec::new(), hostname: instance_name.parse().unwrap(), user_data: vec![], - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], disks: vec![], boot_disk: None, @@ -6054,7 +6287,8 @@ async fn test_instance_create_with_duplicate_anti_affinity_groups( multicast_groups: Vec::new(), hostname: instance_name.parse().unwrap(), user_data: vec![], - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], disks: vec![], boot_disk: None, @@ -6126,7 +6360,8 @@ async fn test_instance_create_with_anti_affinity_groups_that_do_not_exist( multicast_groups: Vec::new(), hostname: instance_name.parse().unwrap(), user_data: vec![], - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], disks: vec![], boot_disk: None, @@ -6211,7 +6446,8 @@ async fn test_instance_create_with_ssh_keys( multicast_groups: Vec::new(), hostname: instance_name.parse().unwrap(), user_data: vec![], - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], disks: vec![], boot_disk: None, @@ -6262,7 +6498,8 @@ async fn test_instance_create_with_ssh_keys( multicast_groups: Vec::new(), hostname: instance_name.parse().unwrap(), user_data: vec![], - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], disks: vec![], boot_disk: None, @@ -6312,7 +6549,8 @@ async fn test_instance_create_with_ssh_keys( multicast_groups: Vec::new(), hostname: instance_name.parse().unwrap(), user_data: vec![], - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], disks: vec![], boot_disk: None, @@ -6454,7 +6692,7 @@ async fn test_cannot_provision_instance_beyond_cpu_capacity( user_data: vec![], ssh_public_keys: None, network_interfaces: - params::InstanceNetworkInterfaceAttachment::Default, + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], disks: vec![], boot_disk: None, @@ -6515,7 +6753,8 @@ async fn test_cannot_provision_instance_beyond_cpu_limit( hostname: "test".parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], disks: vec![], boot_disk: None, @@ -6573,7 +6812,7 @@ async fn test_cannot_provision_instance_beyond_ram_capacity( user_data: vec![], ssh_public_keys: None, network_interfaces: - params::InstanceNetworkInterfaceAttachment::Default, + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], disks: vec![], boot_disk: None, @@ -6676,7 +6915,8 @@ async fn test_can_start_instance_with_cpu_platform( hostname: "test".parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], disks: vec![], boot_disk: None, @@ -6788,7 +7028,8 @@ async fn test_cannot_start_instance_with_unsatisfiable_cpu_platform( hostname: "test".parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], disks: vec![], boot_disk: None, @@ -7086,7 +7327,8 @@ async fn test_instance_ephemeral_ip_from_correct_pool( memory: ByteCount::from_gibibytes_u32(1), hostname: "the-host".parse().unwrap(), user_data: vec![], - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![params::ExternalIpCreate::Ephemeral { pool: Some("pool1".parse::().unwrap().into()), }], @@ -7158,7 +7400,8 @@ async fn test_instance_ephemeral_ip_from_orphan_pool( memory: ByteCount::from_gibibytes_u32(1), hostname: "the-host".parse().unwrap(), user_data: vec![], - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![params::ExternalIpCreate::Ephemeral { pool: Some("orphan-pool".parse::().unwrap().into()), }], @@ -7224,7 +7467,8 @@ async fn test_instance_ephemeral_ip_no_default_pool_error( memory: ByteCount::from_gibibytes_u32(1), hostname: "the-host".parse().unwrap(), user_data: vec![], - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![params::ExternalIpCreate::Ephemeral { pool: None, // <--- the only important thing here }], @@ -7297,7 +7541,7 @@ async fn test_instance_attach_several_external_ips( &client, PROJECT_NAME, instance_name, - ¶ms::InstanceNetworkInterfaceAttachment::Default, + ¶ms::InstanceNetworkInterfaceAttachment::DefaultIpv4, vec![], external_ip_create, true, @@ -7371,7 +7615,8 @@ async fn test_instance_allow_only_one_ephemeral_ip( b"#cloud-config\nsystem_info:\n default_user:\n name: oxide" .to_vec(), ssh_public_keys: None, - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![ephemeral_create.clone(), ephemeral_create], disks: vec![], boot_disk: None, @@ -7404,7 +7649,7 @@ async fn create_instance_with_pool( client, PROJECT_NAME, instance_name, - ¶ms::InstanceNetworkInterfaceAttachment::Default, + ¶ms::InstanceNetworkInterfaceAttachment::DefaultIpv4, vec![], vec![params::ExternalIpCreate::Ephemeral { pool: pool_name.map(|name| name.parse::().unwrap().into()), @@ -7508,7 +7753,8 @@ async fn test_instance_create_in_silo(cptestctx: &ControlPlaneTestContext) { hostname: "inst".parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![params::ExternalIpCreate::Ephemeral { pool: Some("default".parse::().unwrap().into()), }], @@ -7600,7 +7846,7 @@ async fn test_instance_create_with_cross_project_subnet( let client = &cptestctx.external_client; // Setup: Create IP pool and two projects - create_default_ip_pool(client).await; + create_default_ip_pools(client).await; let project_a_name = "project-a"; let project_b_name = "project-b"; create_project(&client, project_a_name).await; @@ -7676,8 +7922,7 @@ async fn test_instance_create_with_cross_project_subnet( }, vpc_name: vpc_b_name.parse().unwrap(), subnet_name: subnet_b_name.parse().unwrap(), - ip: None, - transit_ips: vec![], + ip_config: PrivateIpStackCreate::auto_ipv4(), }; let instance_params = params::InstanceCreate { @@ -7733,7 +7978,7 @@ async fn test_silo_limited_collaborator_cross_project_subnet( let client = &cptestctx.external_client; // Setup: Create IP pool and two projects - create_default_ip_pool(client).await; + create_default_ip_pools(client).await; let project_a_name = "project-a"; let project_b_name = "project-b"; create_project(&client, project_a_name).await; @@ -7807,8 +8052,7 @@ async fn test_silo_limited_collaborator_cross_project_subnet( }, vpc_name: vpc_a_name.parse().unwrap(), subnet_name: subnet_a_name.parse().unwrap(), - ip: None, - transit_ips: vec![], + ip_config: PrivateIpStackCreate::auto_ipv4(), }; let instance_same_project = params::InstanceCreate { @@ -7872,8 +8116,7 @@ async fn test_silo_limited_collaborator_cross_project_subnet( }, vpc_name: vpc_b_name.parse().unwrap(), subnet_name: subnet_b_name.parse().unwrap(), - ip: None, - transit_ips: vec![], + ip_config: PrivateIpStackCreate::auto_ipv4(), }; let instance_cross_project = params::InstanceCreate { @@ -8243,19 +8486,38 @@ async fn assert_sled_v2p_mappings( nic: &InstanceNetworkInterface, vni: Vni, ) { + let nic_ipv4 = nic.ip_stack.ipv4_addr().copied().map(IpAddr::from); + let nic_ipv6 = nic.ip_stack.ipv6_addr().copied().map(IpAddr::from); + let condition = || async { let v2p_mappings = sled_agent.v2p_mappings.lock().unwrap(); - let mapping = v2p_mappings.iter().find(|mapping| { - mapping.virtual_ip == nic.ip - && mapping.virtual_mac == nic.mac - && mapping.physical_host_ip == sled_agent.ip - && mapping.vni == vni - }); - if mapping.is_some() { - Ok(()) - } else { + // Check that we either don't need a mapping for a specific address, or + // that it exists if we do need one. + let missing_ipv4_mapping = match nic_ipv4 { + None => false, + Some(ipv4) => v2p_mappings.iter().any(|mapping| { + mapping.virtual_ip == ipv4 + && mapping.virtual_mac == nic.mac + && mapping.physical_host_ip == sled_agent.ip + && mapping.vni == vni + }), + }; + let missing_ipv6_mapping = match nic_ipv6 { + None => false, + Some(ipv6) => v2p_mappings.iter().any(|mapping| { + mapping.virtual_ip == ipv6 + && mapping.virtual_mac == nic.mac + && mapping.physical_host_ip == sled_agent.ip + && mapping.vni == vni + }), + }; + + // We're not ready if either mapping is expected and missing. + if missing_ipv4_mapping || missing_ipv6_mapping { Err(CondCheckError::NotYet::<()>) + } else { + Ok(()) } }; wait_for_condition( diff --git a/nexus/tests/integration_tests/internet_gateway.rs b/nexus/tests/integration_tests/internet_gateway.rs index 57e044ddb57..5acd4f8ca8f 100644 --- a/nexus/tests/integration_tests/internet_gateway.rs +++ b/nexus/tests/integration_tests/internet_gateway.rs @@ -20,7 +20,7 @@ use nexus_test_utils_macros::nexus_test; use nexus_types::external_api::{ params::{ self, ExternalIpCreate, InstanceNetworkInterfaceAttachment, - InstanceNetworkInterfaceCreate, + InstanceNetworkInterfaceCreate, PrivateIpStackCreate, }, shared::SiloRole, views::{InternetGateway, InternetGatewayIpAddress, InternetGatewayIpPool}, @@ -370,10 +370,9 @@ async fn test_setup(c: &ClientTestContext) { description: String::from("description"), name: "noname".parse().unwrap(), }, - ip: None, + ip_config: PrivateIpStackCreate::auto_ipv4(), subnet_name: "default".parse().unwrap(), vpc_name: VPC_NAME.parse().unwrap(), - transit_ips: vec![], }, ]); let _inst = create_instance_with( diff --git a/nexus/tests/integration_tests/local_storage.rs b/nexus/tests/integration_tests/local_storage.rs index b062fac11f7..35fcbc30329 100644 --- a/nexus/tests/integration_tests/local_storage.rs +++ b/nexus/tests/integration_tests/local_storage.rs @@ -10,7 +10,7 @@ use http::method::Method; use nexus_test_utils::http_testing::AuthnMode; use nexus_test_utils::http_testing::NexusRequest; use nexus_test_utils::http_testing::RequestBuilder; -use nexus_test_utils::resource_helpers::create_default_ip_pool; +use nexus_test_utils::resource_helpers::create_default_ip_pools; use nexus_test_utils::resource_helpers::create_project; use nexus_test_utils_macros::nexus_test; use nexus_types::external_api::{params, views}; @@ -38,7 +38,7 @@ fn get_disks_url() -> String { pub async fn create_project_and_pool( client: &ClientTestContext, ) -> views::Project { - create_default_ip_pool(client).await; + create_default_ip_pools(client).await; create_project(client, PROJECT_NAME).await } diff --git a/nexus/tests/integration_tests/metrics.rs b/nexus/tests/integration_tests/metrics.rs index 1664fd2eb54..639afc39b33 100644 --- a/nexus/tests/integration_tests/metrics.rs +++ b/nexus/tests/integration_tests/metrics.rs @@ -17,7 +17,7 @@ use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils::background::activate_background_task; use nexus_test_utils::http_testing::{AuthnMode, NexusRequest, RequestBuilder}; use nexus_test_utils::resource_helpers::{ - DiskTest, create_default_ip_pool, create_disk, create_instance, + DiskTest, create_default_ip_pools, create_disk, create_instance, create_project, grant_iam, object_create_error, }; use nexus_test_utils::wait_for_producer; @@ -122,7 +122,7 @@ async fn test_metrics( cptestctx: &ControlPlaneTestContext, ) { let client = &cptestctx.external_client; - create_default_ip_pool(&client).await; // needed for instance create to work + create_default_ip_pools(&client).await; // needed for instance create to work DiskTest::new(cptestctx).await; // needed for disk create to work // Wait until Nexus registers as a producer with Oximeter. @@ -467,7 +467,7 @@ async fn test_project_timeseries_query( ) { let client = &cptestctx.external_client; - create_default_ip_pool(&client).await; // needed for instance create to work + create_default_ip_pools(&client).await; // needed for instance create to work // Create two projects let p1 = create_project(&client, "project1").await; diff --git a/nexus/tests/integration_tests/multicast/api.rs b/nexus/tests/integration_tests/multicast/api.rs index bcda0eafe3a..a6afc438947 100644 --- a/nexus/tests/integration_tests/multicast/api.rs +++ b/nexus/tests/integration_tests/multicast/api.rs @@ -15,7 +15,7 @@ use http::{Method, StatusCode}; use nexus_test_utils::http_testing::{AuthnMode, NexusRequest, RequestBuilder}; use nexus_test_utils::resource_helpers::{ - create_default_ip_pool, create_project, object_create, + create_default_ip_pools, create_project, object_create, }; use nexus_test_utils_macros::nexus_test; use nexus_types::external_api::params::{ @@ -40,7 +40,7 @@ async fn test_multicast_api_behavior(cptestctx: &ControlPlaneTestContext) { // Setup in parallel let (_, _, mcast_pool) = ops::join3( create_project(client, project_name), - create_default_ip_pool(client), + create_default_ip_pools(client), create_multicast_ip_pool(client, "api-edge-pool"), ) .await; @@ -74,7 +74,7 @@ async fn test_multicast_api_behavior(cptestctx: &ControlPlaneTestContext) { hostname: "edge-case-1".parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: InstanceNetworkInterfaceAttachment::Default, + network_interfaces: InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], multicast_groups: vec![NameOrId::Name(group_name.parse().unwrap())], disks: vec![], @@ -100,7 +100,7 @@ async fn test_multicast_api_behavior(cptestctx: &ControlPlaneTestContext) { hostname: "edge-case-2".parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: InstanceNetworkInterfaceAttachment::Default, + network_interfaces: InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], multicast_groups: vec![], // No groups at creation disks: vec![], @@ -195,7 +195,7 @@ async fn test_multicast_api_behavior(cptestctx: &ControlPlaneTestContext) { hostname: "edge-case-3".parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: InstanceNetworkInterfaceAttachment::Default, + network_interfaces: InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], multicast_groups: vec![], disks: vec![], diff --git a/nexus/tests/integration_tests/multicast/authorization.rs b/nexus/tests/integration_tests/multicast/authorization.rs index 5247e4fe2a6..244e496078c 100644 --- a/nexus/tests/integration_tests/multicast/authorization.rs +++ b/nexus/tests/integration_tests/multicast/authorization.rs @@ -28,8 +28,8 @@ use http::StatusCode; use nexus_test_utils::http_testing::{AuthnMode, NexusRequest, RequestBuilder}; use nexus_test_utils::resource_helpers::test_params::UserPassword; use nexus_test_utils::resource_helpers::{ - create_default_ip_pool, create_instance, create_local_user, create_project, - grant_iam, link_ip_pool, object_get, + create_default_ip_pools, create_instance, create_local_user, + create_project, grant_iam, link_ip_pool, object_get, }; use nexus_test_utils_macros::nexus_test; use nexus_types::external_api::params::{ @@ -56,7 +56,7 @@ async fn test_silo_users_can_create_and_modify_multicast_groups( cptestctx: &ControlPlaneTestContext, ) { let client = &cptestctx.external_client; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; // Get current silo info let silo_url = format!("/v1/system/silos/{}", cptestctx.silo_name); @@ -180,7 +180,7 @@ async fn test_silo_users_can_attach_instances_to_multicast_groups( cptestctx: &ControlPlaneTestContext, ) { let client = &cptestctx.external_client; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; // Get current silo info let silo_url = format!("/v1/system/silos/{}", cptestctx.silo_name); @@ -264,7 +264,7 @@ async fn test_silo_users_can_attach_instances_to_multicast_groups( hostname: "user-instance".parse::().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: InstanceNetworkInterfaceAttachment::Default, + network_interfaces: InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], multicast_groups: vec![], disks: vec![], @@ -320,7 +320,7 @@ async fn test_authenticated_users_can_read_multicast_groups( cptestctx: &ControlPlaneTestContext, ) { let client = &cptestctx.external_client; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; // Get current silo info let silo_url = format!("/v1/system/silos/{}", cptestctx.silo_name); @@ -413,7 +413,7 @@ async fn test_cross_project_instance_attachment_allowed( // Create pools and projects let (_, _project1, _project2, mcast_pool) = ops::join4( - create_default_ip_pool(&client), + create_default_ip_pools(&client), create_project(client, "project1"), create_project(client, "project2"), create_multicast_ip_pool(&client, "mcast-pool"), @@ -488,7 +488,7 @@ async fn test_unauthenticated_cannot_list_multicast_groups( cptestctx: &ControlPlaneTestContext, ) { let client = &cptestctx.external_client; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; // Get current silo info let silo_url = format!("/v1/system/silos/{}", cptestctx.silo_name); @@ -537,7 +537,7 @@ async fn test_unauthenticated_cannot_access_member_operations( cptestctx: &ControlPlaneTestContext, ) { let client = &cptestctx.external_client; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; // Get current silo info let silo_url = format!("/v1/system/silos/{}", cptestctx.silo_name); @@ -628,7 +628,7 @@ async fn test_unprivileged_users_can_list_group_members( cptestctx: &ControlPlaneTestContext, ) { let client = &cptestctx.external_client; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; // Get current silo info let silo_url = format!("/v1/system/silos/{}", cptestctx.silo_name); @@ -722,7 +722,7 @@ async fn test_unprivileged_users_can_list_group_members( hostname: "privileged-instance".parse::().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: InstanceNetworkInterfaceAttachment::Default, + network_interfaces: InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], multicast_groups: vec![], disks: vec![], @@ -871,7 +871,7 @@ async fn test_project_only_users_can_access_multicast_groups( ) { let client = &cptestctx.external_client; // create_default_ip_pool already links "default" pool to the DEFAULT_SILO - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; // Create multicast pool (fleet-scoped, no per-silo linking needed) create_multicast_ip_pool(&client, "mcast-pool").await; @@ -1043,7 +1043,7 @@ async fn test_project_only_users_can_access_multicast_groups( hostname: instance_name.parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: InstanceNetworkInterfaceAttachment::Default, + network_interfaces: InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], disks: vec![], boot_disk: None, diff --git a/nexus/tests/integration_tests/multicast/cache_invalidation.rs b/nexus/tests/integration_tests/multicast/cache_invalidation.rs index 7e028239bc5..adfdbb0416a 100644 --- a/nexus/tests/integration_tests/multicast/cache_invalidation.rs +++ b/nexus/tests/integration_tests/multicast/cache_invalidation.rs @@ -10,7 +10,7 @@ use gateway_client::types::{PowerState, RotState, SpState}; use nexus_db_queries::context::OpContext; use nexus_test_utils::resource_helpers::object_create; use nexus_test_utils::resource_helpers::{ - create_default_ip_pool, create_project, + create_default_ip_pools, create_project, }; use nexus_test_utils_macros::nexus_test; use nexus_types::deployment::SledFilter; @@ -48,7 +48,7 @@ async fn test_sled_move_updates_multicast_port_mapping( let opctx = OpContext::for_tests(log.clone(), datastore.clone()); // Create project and multicast IP pool - create_default_ip_pool(client).await; + create_default_ip_pools(client).await; create_project(client, PROJECT_NAME).await; let pool = create_multicast_ip_pool(client, "sled-move-pool").await; @@ -319,7 +319,7 @@ async fn test_cache_ttl_driven_refresh() { let client = &cptestctx.external_client; // Create project and multicast IP pool - create_default_ip_pool(client).await; + create_default_ip_pools(client).await; create_project(client, PROJECT_NAME).await; let pool = create_multicast_ip_pool(client, "ttl-test-pool").await; @@ -530,7 +530,7 @@ async fn test_backplane_cache_ttl_expiry() { let client = &cptestctx.external_client; // Create project and multicast IP pool - create_default_ip_pool(client).await; + create_default_ip_pools(client).await; create_project(client, PROJECT_NAME).await; let pool = create_multicast_ip_pool(client, "backplane-ttl-pool").await; diff --git a/nexus/tests/integration_tests/multicast/enablement.rs b/nexus/tests/integration_tests/multicast/enablement.rs index b172a0b8753..6f2ea575d5a 100644 --- a/nexus/tests/integration_tests/multicast/enablement.rs +++ b/nexus/tests/integration_tests/multicast/enablement.rs @@ -9,7 +9,7 @@ use std::net::IpAddr; use nexus_test_utils::resource_helpers::{ - create_default_ip_pool, create_project, object_create, object_get, + create_default_ip_pools, create_project, object_create, object_get, }; use nexus_types::external_api::params::MulticastGroupCreate; use nexus_types::external_api::views::MulticastGroup; @@ -44,7 +44,7 @@ async fn test_multicast_enablement() { let client = &cptestctx.external_client; // Set up project and multicast infrastructure - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; create_project(client, PROJECT_NAME).await; let _pool = create_multicast_ip_pool(client, "test-pool").await; diff --git a/nexus/tests/integration_tests/multicast/failures.rs b/nexus/tests/integration_tests/multicast/failures.rs index a47b4b01991..98f7840cbf9 100644 --- a/nexus/tests/integration_tests/multicast/failures.rs +++ b/nexus/tests/integration_tests/multicast/failures.rs @@ -12,7 +12,7 @@ use std::net::{IpAddr, Ipv4Addr}; use nexus_test_utils::resource_helpers::{ - create_default_ip_pool, create_instance, create_project, object_create, + create_default_ip_pools, create_instance, create_project, object_create, object_delete, object_get, objects_list_page_authz, }; use nexus_test_utils_macros::nexus_test; @@ -38,7 +38,7 @@ async fn test_multicast_group_dpd_communication_failure_recovery( // Setup: project, pools, group with member - parallelize creation let (_, _, mcast_pool) = ops::join3( create_project(&client, project_name), - create_default_ip_pool(&client), + create_default_ip_pools(&client), create_multicast_ip_pool(&client, "mcast-pool"), ) .await; @@ -114,7 +114,7 @@ async fn test_multicast_reconciler_state_consistency_validation( // Create multiple groups to test reconciler batch processing with failures let (_, _, mcast_pool) = ops::join3( create_project(&client, project_name), - create_default_ip_pool(&client), + create_default_ip_pools(&client), create_multicast_ip_pool(&client, "mcast-pool"), ) .await; @@ -207,7 +207,7 @@ async fn test_dpd_failure_during_creating_state( // Setup: project, pools, group with member - parallelize creation let (_, _, mcast_pool) = ops::join3( create_project(&client, project_name), - create_default_ip_pool(&client), + create_default_ip_pools(&client), create_multicast_ip_pool(&client, "mcast-pool"), ) .await; @@ -294,7 +294,7 @@ async fn test_dpd_failure_during_active_state( // Setup: project, pools, group with member create_project(&client, project_name).await; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; let mcast_pool = create_multicast_ip_pool(&client, "mcast-pool").await; @@ -392,7 +392,7 @@ async fn test_dpd_failure_during_deleting_state( // Setup: project, pools, group with member create_project(&client, project_name).await; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; let mcast_pool = create_multicast_ip_pool(&client, "mcast-pool").await; @@ -512,7 +512,7 @@ async fn test_multicast_group_members_during_dpd_failure( // Setup: project, pools, group with member - parallelize creation let (_, _, mcast_pool) = ops::join3( create_project(&client, project_name), - create_default_ip_pool(&client), + create_default_ip_pools(&client), create_multicast_ip_pool(&client, "mcast-pool"), ) .await; diff --git a/nexus/tests/integration_tests/multicast/groups.rs b/nexus/tests/integration_tests/multicast/groups.rs index 8d795c6d26a..3ab0d5a2e31 100644 --- a/nexus/tests/integration_tests/multicast/groups.rs +++ b/nexus/tests/integration_tests/multicast/groups.rs @@ -21,7 +21,7 @@ use nexus_db_queries::db::fixed_data::silo::DEFAULT_SILO; use nexus_test_utils::dpd_client; use nexus_test_utils::http_testing::{AuthnMode, NexusRequest, RequestBuilder}; use nexus_test_utils::resource_helpers::{ - create_default_ip_pool, create_instance, create_project, link_ip_pool, + create_default_ip_pools, create_instance, create_project, link_ip_pool, object_create, object_create_error, object_delete, object_get, object_get_error, object_put, object_put_error, }; @@ -446,7 +446,7 @@ async fn test_multicast_group_with_source_ips( // Create a project and SSM multicast IP pool (232.0.0.0/8 range) create_project(&client, project_name).await; - create_default_ip_pool(&client).await; // Required for any instance operations + create_default_ip_pools(&client).await; // Required for any instance operations let mcast_pool = create_multicast_ip_pool_with_range( &client, "mcast-pool", @@ -727,7 +727,7 @@ async fn test_multicast_group_member_operations( // Create project and IP pools in parallel let (_, _, mcast_pool) = ops::join3( create_project(&client, project_name), - create_default_ip_pool(&client), // For instance networking + create_default_ip_pools(&client), // For instance networking create_multicast_ip_pool_with_range( &client, "mcast-pool", @@ -928,7 +928,7 @@ async fn test_instance_multicast_endpoints( // Create a project, default unicast pool, and multicast IP pool create_project(&client, project_name).await; - create_default_ip_pool(&client).await; // For instance networking + create_default_ip_pools(&client).await; // For instance networking let mcast_pool = create_multicast_ip_pool_with_range( &client, "mcast-pool", @@ -1327,7 +1327,7 @@ async fn test_instance_deletion_removes_multicast_memberships( // Setup: project, pools, group with unique IP range create_project(&client, project_name).await; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; let mcast_pool = create_multicast_ip_pool_with_range( &client, "mcast-pool", @@ -1432,7 +1432,7 @@ async fn test_member_operations_via_rpw_reconciler( // Setup: project, pools, group with unique IP range create_project(&client, project_name).await; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; let mcast_pool = create_multicast_ip_pool_with_range( &client, "mcast-pool", @@ -2655,7 +2655,7 @@ async fn test_multicast_group_mvlan_with_member_operations( let instance_name = "mvlan-test-instance"; // Setup - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; create_project(&client, project_name).await; let mcast_pool = create_multicast_ip_pool_with_range( &client, @@ -2766,7 +2766,7 @@ async fn test_multicast_group_mvlan_reconciler_update( let instance_name = "mvlan-reconciler-instance"; // Setup - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; create_project(&client, project_name).await; let mcast_pool = create_multicast_ip_pool_with_range( &client, diff --git a/nexus/tests/integration_tests/multicast/instances.rs b/nexus/tests/integration_tests/multicast/instances.rs index d2c53236eb6..e9516d75408 100644 --- a/nexus/tests/integration_tests/multicast/instances.rs +++ b/nexus/tests/integration_tests/multicast/instances.rs @@ -15,7 +15,7 @@ use http::{Method, StatusCode}; use nexus_db_queries::context::OpContext; use nexus_test_utils::http_testing::{AuthnMode, NexusRequest, RequestBuilder}; use nexus_test_utils::resource_helpers::{ - create_default_ip_pool, create_instance, create_project, object_create, + create_default_ip_pools, create_instance, create_project, object_create, object_delete, object_get, }; use nexus_test_utils_macros::nexus_test; @@ -48,7 +48,7 @@ async fn test_multicast_lifecycle(cptestctx: &ControlPlaneTestContext) { let client = &cptestctx.external_client; // Setup - create IP pool and project (shared across all operations) - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; create_project(client, PROJECT_NAME).await; let mcast_pool = create_multicast_ip_pool_with_range( &client, @@ -273,7 +273,7 @@ async fn test_multicast_group_attach_conflicts( ) { let client = &cptestctx.external_client; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; create_project(client, PROJECT_NAME).await; let mcast_pool = create_multicast_ip_pool_with_range( &client, @@ -362,7 +362,7 @@ async fn test_multicast_group_attach_limits( ) { let client = &cptestctx.external_client; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; create_project(client, PROJECT_NAME).await; let mcast_pool = create_multicast_ip_pool(&client, "mcast-pool").await; @@ -459,7 +459,7 @@ async fn test_multicast_group_instance_state_transitions( ) { let client = &cptestctx.external_client; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; create_project(client, PROJECT_NAME).await; let mcast_pool = create_multicast_ip_pool(&client, "mcast-pool").await; @@ -583,7 +583,7 @@ async fn test_multicast_group_persistence_through_stop_start( ) { let client = &cptestctx.external_client; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; create_project(client, PROJECT_NAME).await; let mcast_pool = create_multicast_ip_pool(&client, "mcast-pool").await; @@ -771,7 +771,7 @@ async fn test_multicast_concurrent_operations( ) { let client = &cptestctx.external_client; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; create_project(client, PROJECT_NAME).await; let mcast_pool = create_multicast_ip_pool_with_range( &client, @@ -939,7 +939,7 @@ async fn test_multicast_member_cleanup_instance_never_started( // Setup: project, pools, group create_project(client, project_name).await; - create_default_ip_pool(client).await; + create_default_ip_pools(client).await; let mcast_pool = create_multicast_ip_pool_with_range( client, "never-started-pool", @@ -976,7 +976,7 @@ async fn test_multicast_member_cleanup_instance_never_started( hostname: instance_name.parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: InstanceNetworkInterfaceAttachment::Default, + network_interfaces: InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], multicast_groups: vec![], disks: vec![], @@ -1128,7 +1128,7 @@ async fn test_multicast_group_membership_during_migration( // Setup: project, pools, and multicast group create_project(client, project_name).await; - create_default_ip_pool(client).await; + create_default_ip_pools(client).await; let mcast_pool = create_multicast_ip_pool_with_range( client, "migration-pool", @@ -1415,7 +1415,7 @@ async fn test_multicast_group_concurrent_member_migrations( // Setup: project, pools, and multicast group create_project(client, project_name).await; - create_default_ip_pool(client).await; + create_default_ip_pools(client).await; let mcast_pool = create_multicast_ip_pool_with_range( client, "concurrent-migration-pool", diff --git a/nexus/tests/integration_tests/multicast/mod.rs b/nexus/tests/integration_tests/multicast/mod.rs index fe818dd60e6..23babfdee73 100644 --- a/nexus/tests/integration_tests/multicast/mod.rs +++ b/nexus/tests/integration_tests/multicast/mod.rs @@ -1027,7 +1027,7 @@ pub(crate) async fn instance_for_multicast_groups( hostname: instance_name.parse::().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: InstanceNetworkInterfaceAttachment::Default, + network_interfaces: InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], multicast_groups, disks: vec![], @@ -1073,7 +1073,7 @@ pub(crate) async fn create_instances_with_multicast_groups( user_data: b"#cloud-config".to_vec(), ssh_public_keys: None, network_interfaces: - InstanceNetworkInterfaceAttachment::Default, + InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], disks: vec![], boot_disk: None, diff --git a/nexus/tests/integration_tests/multicast/networking_integration.rs b/nexus/tests/integration_tests/multicast/networking_integration.rs index 1ed2b1138d7..e0a942af3a6 100644 --- a/nexus/tests/integration_tests/multicast/networking_integration.rs +++ b/nexus/tests/integration_tests/multicast/networking_integration.rs @@ -14,7 +14,7 @@ use http::{Method, StatusCode}; use nexus_test_utils::http_testing::{AuthnMode, NexusRequest, RequestBuilder}; use nexus_test_utils::resource_helpers::create_floating_ip; use nexus_test_utils::resource_helpers::{ - create_default_ip_pool, create_project, object_create, object_delete, + create_default_ip_pools, create_project, object_create, object_delete, }; use nexus_test_utils_macros::nexus_test; use nexus_types::external_api::params::{ @@ -56,7 +56,7 @@ async fn test_multicast_with_external_ip_basic( // Setup: project and IP pools in parallel let (_, _, mcast_pool) = ops::join3( create_project(client, project_name), - create_default_ip_pool(client), // For external IPs + create_default_ip_pools(client), // For external IPs create_multicast_ip_pool_with_range( client, "external-ip-mcast-pool", @@ -94,7 +94,7 @@ async fn test_multicast_with_external_ip_basic( hostname: instance_name.parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: InstanceNetworkInterfaceAttachment::Default, + network_interfaces: InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], // Start without external IP multicast_groups: vec![], disks: vec![], @@ -248,7 +248,7 @@ async fn test_multicast_external_ip_lifecycle( // Setup in parallel let (_, _, mcast_pool) = ops::join3( create_project(client, project_name), - create_default_ip_pool(client), + create_default_ip_pools(client), create_multicast_ip_pool_with_range( client, "external-ip-lifecycle-pool", @@ -285,7 +285,7 @@ async fn test_multicast_external_ip_lifecycle( hostname: instance_name.parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: InstanceNetworkInterfaceAttachment::Default, + network_interfaces: InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], multicast_groups: vec![], disks: vec![], @@ -432,7 +432,7 @@ async fn test_multicast_with_external_ip_at_creation( // Setup - parallelize project and pool creation let (_, _, mcast_pool) = ops::join3( create_project(client, project_name), - create_default_ip_pool(client), + create_default_ip_pools(client), create_multicast_ip_pool_with_range( client, "creation-mixed-pool", @@ -472,7 +472,7 @@ async fn test_multicast_with_external_ip_at_creation( hostname: instance_name.parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: InstanceNetworkInterfaceAttachment::Default, + network_interfaces: InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![external_ip_param], // External IP at creation multicast_groups: vec![], // Will add to multicast group after creation disks: vec![], @@ -567,7 +567,7 @@ async fn test_multicast_with_floating_ip_basic( // Setup: project and IP pools - parallelize creation let (_, _, mcast_pool) = ops::join3( create_project(client, project_name), - create_default_ip_pool(client), // For floating IPs + create_default_ip_pools(client), // For floating IPs create_multicast_ip_pool_with_range( client, "floating-ip-mcast-pool", @@ -610,7 +610,7 @@ async fn test_multicast_with_floating_ip_basic( hostname: instance_name.parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: InstanceNetworkInterfaceAttachment::Default, + network_interfaces: InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], // Start without external IP multicast_groups: vec![], disks: vec![], diff --git a/nexus/tests/integration_tests/pantry.rs b/nexus/tests/integration_tests/pantry.rs index e5f070bc18b..451103857cf 100644 --- a/nexus/tests/integration_tests/pantry.rs +++ b/nexus/tests/integration_tests/pantry.rs @@ -14,7 +14,7 @@ use nexus_test_utils::http_testing::NexusRequest; use nexus_test_utils::http_testing::RequestBuilder; use nexus_test_utils::identity_eq; use nexus_test_utils::resource_helpers::DiskTest; -use nexus_test_utils::resource_helpers::create_default_ip_pool; +use nexus_test_utils::resource_helpers::create_default_ip_pools; use nexus_test_utils::resource_helpers::create_instance; use nexus_test_utils::resource_helpers::create_project; use nexus_test_utils::resource_helpers::object_create; @@ -58,7 +58,7 @@ fn get_disk_attach_url(instance_name: &str) -> String { } async fn create_project_and_pool(client: &ClientTestContext) -> Uuid { - create_default_ip_pool(client).await; + create_default_ip_pools(client).await; let project = create_project(client, PROJECT_NAME).await; project.identity.id } diff --git a/nexus/tests/integration_tests/probe.rs b/nexus/tests/integration_tests/probe.rs index 197e5410f0a..bc951aecaac 100644 --- a/nexus/tests/integration_tests/probe.rs +++ b/nexus/tests/integration_tests/probe.rs @@ -3,7 +3,7 @@ use http::{Method, StatusCode}; use nexus_test_utils::{ SLED_AGENT_UUID, http_testing::{AuthnMode, NexusRequest}, - resource_helpers::{create_default_ip_pool, create_project}, + resource_helpers::{create_default_ip_pools, create_project}, }; use nexus_test_utils_macros::nexus_test; use nexus_types::external_api::{params::ProbeCreate, shared::ProbeInfo}; @@ -16,7 +16,7 @@ type ControlPlaneTestContext = async fn test_probe_basic_crud(ctx: &ControlPlaneTestContext) { let client = &ctx.external_client; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; create_project(&client, "nebula").await; let probes = NexusRequest::iter_collection_authn::( diff --git a/nexus/tests/integration_tests/projects.rs b/nexus/tests/integration_tests/projects.rs index f221f8f8ffb..e6b17da22ef 100644 --- a/nexus/tests/integration_tests/projects.rs +++ b/nexus/tests/integration_tests/projects.rs @@ -12,7 +12,7 @@ use nexus_test_utils::http_testing::RequestBuilder; use nexus_test_utils::resource_helpers::DiskTest; use nexus_test_utils::resource_helpers::create_affinity_group; use nexus_test_utils::resource_helpers::create_anti_affinity_group; -use nexus_test_utils::resource_helpers::create_default_ip_pool; +use nexus_test_utils::resource_helpers::create_default_ip_pools; use nexus_test_utils::resource_helpers::create_disk; use nexus_test_utils::resource_helpers::create_floating_ip; use nexus_test_utils::resource_helpers::create_local_user; @@ -147,7 +147,7 @@ async fn test_project_deletion_with_instance( ) { let client = &cptestctx.external_client; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; // Create a project that we'll use for testing. let name = "springfield-squidport"; @@ -240,7 +240,7 @@ async fn test_project_deletion_with_floating_ip( let name = "springfield-squidport"; let url = format!("/v1/projects/{}", name); - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; create_project(&client, &name).await; delete_project_default_subnet(&name, &client).await; diff --git a/nexus/tests/integration_tests/quotas.rs b/nexus/tests/integration_tests/quotas.rs index b3805af056c..f05adce854c 100644 --- a/nexus/tests/integration_tests/quotas.rs +++ b/nexus/tests/integration_tests/quotas.rs @@ -106,7 +106,7 @@ impl ResourceAllocator { user_data: b"#cloud-config\nsystem_info:\n default_user:\n name: oxide" .to_vec(), ssh_public_keys: Some(Vec::new()), - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: Vec::::new(), disks: Vec::::new(), boot_disk: None, diff --git a/nexus/tests/integration_tests/schema.rs b/nexus/tests/integration_tests/schema.rs index 8a50c863d1f..0b16b3cfca3 100644 --- a/nexus/tests/integration_tests/schema.rs +++ b/nexus/tests/integration_tests/schema.rs @@ -1390,7 +1390,7 @@ fn at_current_101_0_0<'a>(ctx: &'a MigrationContext<'a>) -> BoxFuture<'a, ()> { user_data: vec![], ssh_public_keys: None, network_interfaces: - params::InstanceNetworkInterfaceAttachment::Default, + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], boot_disk: None, cpu_platform: None, diff --git a/nexus/tests/integration_tests/sleds.rs b/nexus/tests/integration_tests/sleds.rs index 74d69d621f4..53ae7d92394 100644 --- a/nexus/tests/integration_tests/sleds.rs +++ b/nexus/tests/integration_tests/sleds.rs @@ -11,7 +11,7 @@ use nexus_db_model::PhysicalDiskKind as DbPhysicalDiskKind; use nexus_db_queries::context::OpContext; use nexus_test_interface::NexusServer; use nexus_test_utils::SLED_AGENT_UUID; -use nexus_test_utils::resource_helpers::create_default_ip_pool; +use nexus_test_utils::resource_helpers::create_default_ip_pools; use nexus_test_utils::resource_helpers::create_instance; use nexus_test_utils::resource_helpers::create_project; use nexus_test_utils::resource_helpers::objects_list_page_authz; @@ -182,7 +182,7 @@ async fn test_sled_instance_list(cptestctx: &ControlPlaneTestContext) { } // Create an IP pool and project that we'll use for testing. - create_default_ip_pool(&external_client).await; + create_default_ip_pools(&external_client).await; let project = create_project(&external_client, "test-project").await; let instance = create_instance(&external_client, "test-project", "test-instance") diff --git a/nexus/tests/integration_tests/snapshots.rs b/nexus/tests/integration_tests/snapshots.rs index 0d0042c5f11..c6f3d397004 100644 --- a/nexus/tests/integration_tests/snapshots.rs +++ b/nexus/tests/integration_tests/snapshots.rs @@ -24,7 +24,7 @@ use nexus_test_utils::SLED_AGENT_UUID; use nexus_test_utils::http_testing::AuthnMode; use nexus_test_utils::http_testing::NexusRequest; use nexus_test_utils::http_testing::RequestBuilder; -use nexus_test_utils::resource_helpers::create_default_ip_pool; +use nexus_test_utils::resource_helpers::create_default_ip_pools; use nexus_test_utils::resource_helpers::create_disk; use nexus_test_utils::resource_helpers::create_project; use nexus_test_utils::resource_helpers::object_create; @@ -65,7 +65,7 @@ fn get_disk_url(name: &str) -> String { } async fn create_project_and_pool(client: &ClientTestContext) -> Uuid { - create_default_ip_pool(client).await; + create_default_ip_pools(client).await; let project = create_project(client, PROJECT_NAME).await; project.identity.id } diff --git a/nexus/tests/integration_tests/subnet_allocation.rs b/nexus/tests/integration_tests/subnet_allocation.rs index a31f99a8206..a255a3fd4e8 100644 --- a/nexus/tests/integration_tests/subnet_allocation.rs +++ b/nexus/tests/integration_tests/subnet_allocation.rs @@ -13,17 +13,19 @@ use nexus_config::NUM_INITIAL_RESERVED_IP_ADDRESSES; use nexus_test_utils::http_testing::AuthnMode; use nexus_test_utils::http_testing::NexusRequest; use nexus_test_utils::http_testing::RequestBuilder; -use nexus_test_utils::resource_helpers::create_default_ip_pool; +use nexus_test_utils::resource_helpers::create_default_ip_pools; use nexus_test_utils::resource_helpers::create_instance_with; use nexus_test_utils::resource_helpers::create_project; use nexus_test_utils::resource_helpers::objects_list_page_authz; use nexus_test_utils_macros::nexus_test; use nexus_types::external_api::params; +use nexus_types::external_api::params::PrivateIpStackCreate; use omicron_common::api::external::{ ByteCount, IdentityMetadataCreateParams, InstanceCpuCount, InstanceNetworkInterface, }; use oxnet::Ipv4Net; +use std::net::IpAddr; use std::net::Ipv4Addr; type ControlPlaneTestContext = @@ -46,8 +48,7 @@ async fn create_instance_expect_failure( }, vpc_name: "default".parse().unwrap(), subnet_name: subnet_name.parse().unwrap(), - ip: None, - transit_ips: vec![], + ip_config: PrivateIpStackCreate::auto_ipv4(), }, ]); let new_instance = params::InstanceCreate { @@ -91,7 +92,7 @@ async fn test_subnet_allocation(cptestctx: &ControlPlaneTestContext) { let project_name = "springfield-squidport"; // Create a project that we'll use for testing. - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; create_project(&client, project_name).await; let url_instances = format!("/v1/instances?project={}", project_name); @@ -136,8 +137,7 @@ async fn test_subnet_allocation(cptestctx: &ControlPlaneTestContext) { }, vpc_name: "default".parse().unwrap(), subnet_name: SUBNET_NAME.parse().unwrap(), - ip: None, - transit_ips: vec![], + ip_config: PrivateIpStackCreate::auto_ipv4(), }, ]); @@ -190,14 +190,22 @@ async fn test_subnet_allocation(cptestctx: &ControlPlaneTestContext) { .items; assert_eq!(network_interfaces.len(), subnet_size); - // Sort by IP address to simplify the checks - network_interfaces.sort_by(|a, b| a.ip.cmp(&b.ip)); + // Sort by IP address(es) to simplify the checks. + network_interfaces.sort_by(|a, b| { + a.ip_stack + .ipv4_addr() + .cmp(&b.ip_stack.ipv4_addr()) + .then(a.ip_stack.ipv6_addr().cmp(&b.ip_stack.ipv6_addr())) + }); for (iface, addr) in network_interfaces .iter() .zip(subnet.addr_iter().skip(NUM_INITIAL_RESERVED_IP_ADDRESSES)) { assert_eq!( - iface.ip, addr, + IpAddr::V4( + *iface.ip_stack.ipv4_addr().expect("Expected an IPv4 stack") + ), + addr, "Nexus should provide auto-assigned IP addresses in order within an IP subnet" ); } diff --git a/nexus/tests/integration_tests/utilization.rs b/nexus/tests/integration_tests/utilization.rs index 73d59d45817..e0969a597dc 100644 --- a/nexus/tests/integration_tests/utilization.rs +++ b/nexus/tests/integration_tests/utilization.rs @@ -5,7 +5,7 @@ use nexus_test_utils::http_testing::AuthnMode; use nexus_test_utils::http_testing::NexusRequest; use nexus_test_utils::http_testing::RequestBuilder; use nexus_test_utils::resource_helpers::DiskTest; -use nexus_test_utils::resource_helpers::create_default_ip_pool; +use nexus_test_utils::resource_helpers::create_default_ip_pools; use nexus_test_utils::resource_helpers::create_instance; use nexus_test_utils::resource_helpers::create_local_user; use nexus_test_utils::resource_helpers::create_project; @@ -38,7 +38,7 @@ type ControlPlaneTestContext = async fn test_utilization_list(cptestctx: &ControlPlaneTestContext) { let client = &cptestctx.external_client; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; // default-silo has quotas, but is explicitly filtered out by ID in the // DB query to avoid user confusion. test-suite-silo also exists, but is @@ -99,7 +99,7 @@ async fn test_utilization_list(cptestctx: &ControlPlaneTestContext) { async fn test_utilization_view(cptestctx: &ControlPlaneTestContext) { let client = &cptestctx.external_client; - create_default_ip_pool(&client).await; + create_default_ip_pools(&client).await; let _ = create_project(&client, &PROJECT_NAME).await; let _ = create_instance(client, &PROJECT_NAME, &INSTANCE_NAME).await; @@ -229,7 +229,8 @@ async fn create_resources_in_test_suite_silo(client: &ClientTestContext) { hostname: "test-inst".parse().unwrap(), user_data: vec![], ssh_public_keys: None, - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + network_interfaces: + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], disks: vec![], boot_disk: None, diff --git a/nexus/tests/integration_tests/volume_management.rs b/nexus/tests/integration_tests/volume_management.rs index e174f0abb16..924e9db5952 100644 --- a/nexus/tests/integration_tests/volume_management.rs +++ b/nexus/tests/integration_tests/volume_management.rs @@ -44,7 +44,7 @@ use nexus_db_queries::db::pagination::paginated; use nexus_test_utils::http_testing::AuthnMode; use nexus_test_utils::http_testing::NexusRequest; use nexus_test_utils::http_testing::RequestBuilder; -use nexus_test_utils::resource_helpers::create_default_ip_pool; +use nexus_test_utils::resource_helpers::create_default_ip_pools; use nexus_test_utils::resource_helpers::create_disk; use nexus_test_utils::resource_helpers::create_disk_from_snapshot; use nexus_test_utils::resource_helpers::create_project; @@ -105,7 +105,7 @@ fn get_snapshot_url(snapshot: &str) -> String { } async fn create_project_and_pool(client: &ClientTestContext) -> Uuid { - create_default_ip_pool(client).await; + create_default_ip_pools(client).await; let project = create_project(client, PROJECT_NAME).await; project.identity.id } diff --git a/nexus/tests/integration_tests/vpc_routers.rs b/nexus/tests/integration_tests/vpc_routers.rs index 21eb8511c84..9317d2e3e14 100644 --- a/nexus/tests/integration_tests/vpc_routers.rs +++ b/nexus/tests/integration_tests/vpc_routers.rs @@ -13,7 +13,7 @@ use nexus_test_utils::http_testing::AuthnMode; use nexus_test_utils::http_testing::NexusRequest; use nexus_test_utils::http_testing::RequestBuilder; use nexus_test_utils::identity_eq; -use nexus_test_utils::resource_helpers::create_default_ip_pool; +use nexus_test_utils::resource_helpers::create_default_ip_pools; use nexus_test_utils::resource_helpers::create_instance_with; use nexus_test_utils::resource_helpers::create_route; use nexus_test_utils::resource_helpers::create_router; @@ -26,6 +26,7 @@ use nexus_test_utils_macros::nexus_test; use nexus_types::external_api::params; use nexus_types::external_api::params::InstanceNetworkInterfaceAttachment; use nexus_types::external_api::params::InstanceNetworkInterfaceCreate; +use nexus_types::external_api::params::PrivateIpStackCreate; use nexus_types::external_api::params::VpcSubnetUpdate; use nexus_types::external_api::views::VpcRouter; use nexus_types::external_api::views::VpcRouterKind; @@ -475,7 +476,7 @@ async fn test_vpc_routers_custom_delivered_to_instance( OpContext::for_tests(cptestctx.logctx.log.new(o!()), datastore.clone()); // Create some instances, one per subnet, and a default pool etc. - create_default_ip_pool(client).await; + create_default_ip_pools(client).await; create_project(client, PROJECT_NAME).await; let vpc = create_vpc(&client, PROJECT_NAME, VPC_NAME).await; @@ -509,8 +510,9 @@ async fn test_vpc_routers_custom_delivered_to_instance( }, vpc_name: vpc.name().clone(), subnet_name: subnet_name.parse().unwrap(), - ip: Some(format!("192.168.{i}.10").parse().unwrap()), - transit_ips: vec![], + ip_config: PrivateIpStackCreate::from_ipv4( + format!("192.168.{i}.10").parse().unwrap(), + ), }, ]), vec![], diff --git a/nexus/tests/integration_tests/vpc_subnets.rs b/nexus/tests/integration_tests/vpc_subnets.rs index 9d9cbea3982..8469ed2fb34 100644 --- a/nexus/tests/integration_tests/vpc_subnets.rs +++ b/nexus/tests/integration_tests/vpc_subnets.rs @@ -15,7 +15,7 @@ use nexus_test_utils::http_testing::RequestBuilder; use nexus_test_utils::identity_eq; use nexus_test_utils::resource_helpers::objects_list_page_authz; use nexus_test_utils::resource_helpers::{ - create_default_ip_pool, create_instance, create_project, create_vpc, + create_default_ip_pools, create_instance, create_project, create_vpc, }; use nexus_test_utils_macros::nexus_test; use nexus_types::external_api::{params, views::VpcSubnet}; @@ -41,7 +41,7 @@ async fn test_delete_vpc_subnet_with_interfaces_fails( // Create a project that we'll use for testing. let project_name = "springfield-squidport"; let instance_name = "inst"; - create_default_ip_pool(client).await; + create_default_ip_pools(client).await; let _ = create_project(&client, project_name).await; let subnets_url = diff --git a/nexus/tests/integration_tests/vpcs.rs b/nexus/tests/integration_tests/vpcs.rs index 3d2dd7b83f2..c58b170032c 100644 --- a/nexus/tests/integration_tests/vpcs.rs +++ b/nexus/tests/integration_tests/vpcs.rs @@ -12,7 +12,7 @@ use nexus_test_utils::http_testing::NexusRequest; use nexus_test_utils::http_testing::RequestBuilder; use nexus_test_utils::identity_eq; use nexus_test_utils::resource_helpers::{ - create_default_ip_pool, create_local_user, create_project, create_vpc, + create_default_ip_pools, create_local_user, create_project, create_vpc, create_vpc_with_error, grant_iam, objects_list_page_authz, test_params, }; use nexus_test_utils_macros::nexus_test; @@ -394,7 +394,7 @@ async fn test_limited_collaborator_can_create_instance( let client = &cptestctx.external_client; // Create IP pool and project (with default VPC and subnet) - create_default_ip_pool(client).await; + create_default_ip_pools(client).await; let project_name = "test-project"; create_project(&client, &project_name).await; @@ -464,7 +464,7 @@ async fn test_limited_collaborator_can_create_instance( user_data: vec![], ssh_public_keys: None, network_interfaces: - params::InstanceNetworkInterfaceAttachment::Default, + params::InstanceNetworkInterfaceAttachment::DefaultIpv4, external_ips: vec![], multicast_groups: vec![], disks: vec![], @@ -492,7 +492,7 @@ async fn test_limited_collaborator_blocked_from_networking_resources( let client = &cptestctx.external_client; // Create IP pool and project - create_default_ip_pool(client).await; + create_default_ip_pools(client).await; let project_name = "test-project"; create_project(&client, &project_name).await; @@ -723,7 +723,7 @@ async fn test_limited_collaborator_can_manage_floating_ips_and_nics( let client = &cptestctx.external_client; // Create IP pool and project (with default VPC and subnet) - create_default_ip_pool(client).await; + create_default_ip_pools(client).await; let project_name = "test-project"; create_project(&client, &project_name).await; diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 6ec29550d2f..08ab2e3a1be 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -9,6 +9,7 @@ use crate::external_api::shared; use base64::Engine; use chrono::{DateTime, Utc}; use http::Uri; +use omicron_common::address::ConcreteIp; use omicron_common::api::external::{ AddressLotKind, AffinityPolicy, AllowedSourceIps, BfdMode, BgpPeer, ByteCount, FailureDomain, Hostname, IdentityMetadataCreateParams, @@ -948,12 +949,12 @@ pub struct InstanceNetworkInterfaceCreate { pub vpc_name: Name, /// The VPC Subnet in which to create the interface. pub subnet_name: Name, - /// The IP address for the interface. One will be auto-assigned if not provided. - pub ip: Option, - /// A set of additional networks that this interface may send and - /// receive traffic on. - #[serde(default)] - pub transit_ips: Vec, + /// The IP stack configuration for this interface. + /// + /// If not provided, a default configuration will be used, which creates a + /// dual-stack IPv4 / IPv6 interface. + #[serde(default = "PrivateIpStackCreate::auto_dual_stack")] + pub ip_config: PrivateIpStackCreate, } /// Parameters for updating an `InstanceNetworkInterface` @@ -987,6 +988,261 @@ pub struct InstanceNetworkInterfaceUpdate { pub transit_ips: Vec, } +/// How a VPC-private IP address is assigned to a network interface. +// +// NOTE: This type is used as a layer of indirection for generating the JSON +// Schema we want for `IpAssignment`. In particular, we use most of its +// contents, but let the real type set the _name_ of the schema based on the +// `ConcreteIp` type being used. +#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema, Serialize)] +#[serde(rename_all = "snake_case", tag = "type", content = "value")] +enum IpAssignmentShadow { + /// Automatically assign an IP address from the VPC Subnet. + #[default] + Auto, + /// Explicitly assign a specific address, if available. + Explicit(T), +} + +trait IpAssignmentSchema { + fn ip_assignment_schema_name() -> String; +} + +impl IpAssignmentSchema for Ipv4Addr { + fn ip_assignment_schema_name() -> String { + String::from("Ipv4Assignment") + } +} + +impl IpAssignmentSchema for Ipv6Addr { + fn ip_assignment_schema_name() -> String { + String::from("Ipv6Assignment") + } +} + +/// How a VPC-private IP address is assigned to a network interface. +#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)] +#[serde(rename_all = "snake_case", tag = "type", content = "value")] +pub enum IpAssignment { + /// Automatically assign an IP address from the VPC Subnet. + #[default] + Auto, + /// Explicitly assign a specific address, if available. + Explicit(T), +} + +impl JsonSchema for IpAssignment +where + T: ConcreteIp + IpAssignmentSchema, +{ + fn schema_name() -> String { + ::ip_assignment_schema_name() + } + + fn json_schema( + generator: &mut schemars::r#gen::SchemaGenerator, + ) -> schemars::schema::Schema { + IpAssignmentShadow::::json_schema(generator) + } +} + +impl From for IpAssignment { + fn from(ip: T) -> Self { + Self::Explicit(ip) + } +} + +/// How to assign an IPv4 address. +pub type Ipv4Assignment = IpAssignment; + +/// How to assign an IPv6 address. +pub type Ipv6Assignment = IpAssignment; + +/// Configuration for a network interface's IPv4 addressing. +#[derive(Clone, Debug, Default, Deserialize, JsonSchema, Serialize)] +pub struct PrivateIpv4StackCreate { + /// The VPC-private address to assign to the interface. + pub ip: Ipv4Assignment, + /// Additional IP networks the interface can send / receive on. + #[serde(default)] + pub transit_ips: Vec, +} + +/// Configuration for a network interface's IPv6 addressing. +#[derive(Clone, Debug, Default, Deserialize, JsonSchema, Serialize)] +pub struct PrivateIpv6StackCreate { + /// The VPC-private address to assign to the interface. + pub ip: Ipv6Assignment, + /// Additional IP networks the interface can send / receive on. + #[serde(default)] + pub transit_ips: Vec, +} + +/// Create parameters for a network interface's IP stack. +#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] +#[serde(rename_all = "snake_case", tag = "type", content = "value")] +pub enum PrivateIpStackCreate { + /// The interface has only an IPv4 stack. + V4(PrivateIpv4StackCreate), + /// The interface has only an IPv6 stack. + V6(PrivateIpv6StackCreate), + /// The interface has both an IPv4 and IPv6 stack. + DualStack { v4: PrivateIpv4StackCreate, v6: PrivateIpv6StackCreate }, +} + +impl PrivateIpStackCreate { + /// Construct an IPv4 configuration with no transit IPs. + pub fn from_ipv4(addr: std::net::Ipv4Addr) -> Self { + PrivateIpStackCreate::V4(PrivateIpv4StackCreate { + ip: Ipv4Assignment::Explicit(addr), + transit_ips: vec![], + }) + } + + /// Construct an IP configuration with only an automatic IPv4 address. + pub fn auto_ipv4() -> Self { + PrivateIpStackCreate::V4(PrivateIpv4StackCreate::default()) + } + + /// Return the IPv4 create parameters, if they exist. + pub fn as_ipv4_create(&self) -> Option<&PrivateIpv4StackCreate> { + match self { + PrivateIpStackCreate::V4(v4) + | PrivateIpStackCreate::DualStack { v4, .. } => Some(v4), + PrivateIpStackCreate::V6(_) => None, + } + } + + /// Return the IPv4 address assignment. + pub fn ipv4_assignment(&self) -> Option<&Ipv4Assignment> { + self.as_ipv4_create().map(|c| &c.ip) + } + + /// Return the IPv4 address explicitly requested, if one exists. + pub fn ipv4_addr(&self) -> Option<&std::net::Ipv4Addr> { + self.ipv4_assignment().and_then(|assignment| match assignment { + IpAssignment::Auto => None, + IpAssignment::Explicit(addr) => Some(addr), + }) + } + + /// Return the IPv4 transit IPs, if they exist. + pub fn ipv4_transit_ips(&self) -> Option<&[Ipv4Net]> { + self.as_ipv4_create().map(|c| c.transit_ips.as_slice()) + } + + /// Construct an IPv6 configuration with no transit IPs. + pub fn from_ipv6(addr: std::net::Ipv6Addr) -> Self { + PrivateIpStackCreate::V6(PrivateIpv6StackCreate { + ip: Ipv6Assignment::Explicit(addr), + transit_ips: vec![], + }) + } + + /// Construct an IP configuration with only an automatic IPv6 address. + pub fn auto_ipv6() -> Self { + PrivateIpStackCreate::V6(PrivateIpv6StackCreate::default()) + } + + /// Return the IPv6 create parameters, if they exist. + pub fn as_ipv6_create(&self) -> Option<&PrivateIpv6StackCreate> { + match self { + PrivateIpStackCreate::V6(v6) + | PrivateIpStackCreate::DualStack { v6, .. } => Some(v6), + PrivateIpStackCreate::V4(_) => None, + } + } + + /// Return the IPv6 address assignment. + pub fn ipv6_assignment(&self) -> Option<&Ipv6Assignment> { + self.as_ipv6_create().map(|c| &c.ip) + } + + /// Return the IPv6 address explicitly requested, if one exists. + pub fn ipv6_addr(&self) -> Option<&std::net::Ipv6Addr> { + self.ipv6_assignment().and_then(|assignment| match assignment { + IpAssignment::Auto => None, + IpAssignment::Explicit(addr) => Some(addr), + }) + } + + /// Return the IPv6 transit IPs, if they exist. + pub fn ipv6_transit_ips(&self) -> Option<&[Ipv6Net]> { + self.as_ipv6_create().map(|c| c.transit_ips.as_slice()) + } + + /// Return the transit IPs requested in this configuration. + pub fn transit_ips(&self) -> Vec { + self.ipv4_transit_ips() + .unwrap_or_default() + .iter() + .copied() + .map(Into::into) + .chain( + self.ipv6_transit_ips() + .unwrap_or_default() + .iter() + .copied() + .map(Into::into), + ) + .collect() + } + + /// Construct a dual-stack IP configuration with explicit IP addresses. + pub fn new_dual_stack( + ipv4: std::net::Ipv4Addr, + ipv6: std::net::Ipv6Addr, + ) -> Self { + PrivateIpStackCreate::DualStack { + v4: PrivateIpv4StackCreate { + ip: Ipv4Assignment::Explicit(ipv4), + transit_ips: Vec::new(), + }, + v6: PrivateIpv6StackCreate { + ip: Ipv6Assignment::Explicit(ipv6), + transit_ips: Vec::new(), + }, + } + } + + /// Construct an IP configuration with both IPv4 / IPv6 addresses and no + /// transit IPs. + pub fn auto_dual_stack() -> Self { + PrivateIpStackCreate::DualStack { + v4: PrivateIpv4StackCreate::default(), + v6: PrivateIpv6StackCreate::default(), + } + } + + /// Return true if this config has any transit IPs + pub fn has_transit_ips(&self) -> bool { + match self { + PrivateIpStackCreate::V4(PrivateIpv4StackCreate { + transit_ips, + .. + }) => !transit_ips.is_empty(), + PrivateIpStackCreate::V6(PrivateIpv6StackCreate { + transit_ips, + .. + }) => !transit_ips.is_empty(), + PrivateIpStackCreate::DualStack { + v4: PrivateIpv4StackCreate { transit_ips: ipv4_addrs, .. }, + v6: PrivateIpv6StackCreate { transit_ips: ipv6_addrs, .. }, + } => !ipv4_addrs.is_empty() || !ipv6_addrs.is_empty(), + } + } + + /// Return true if this IP configuration has an IPv4 stack. + pub fn has_ipv4_stack(&self) -> bool { + self.ipv4_assignment().is_some() + } + + /// Return true if this IP configuration has an IPv6 stack. + pub fn has_ipv6_stack(&self) -> bool { + self.ipv6_assignment().is_some() + } +} + // CERTIFICATES /// Create-time parameters for a `Certificate` @@ -1155,11 +1411,24 @@ pub enum InstanceNetworkInterfaceAttachment { /// designated the primary interface for the instance. Create(Vec), - /// The default networking configuration for an instance is to create a - /// single primary interface with an automatically-assigned IP address. The - /// IP will be pulled from the Project's default VPC / VPC Subnet. + /// Create a single primary interface with an automatically-assigned IPv6 + /// address. + /// + /// The IP will be pulled from the Project's default VPC / VPC Subnet. + DefaultIpv4, + + /// Create a single primary interface with an automatically-assigned IPv6 + /// address. + /// + /// The IP will be pulled from the Project's default VPC / VPC Subnet. + DefaultIpv6, + + /// Create a single primary interface with automatically-assigned IPv4 and + /// IPv6 addresses. + /// + /// The IPs will be pulled from the Project's default VPC / VPC Subnet. #[default] - Default, + DefaultDualStack, /// No network interfaces at all will be created for the instance. None, diff --git a/openapi/nexus/nexus-2025121800.0.0-c18c45.json b/openapi/nexus/nexus-2025121800.0.0-c18c45.json new file mode 100644 index 00000000000..6a680bebd00 --- /dev/null +++ b/openapi/nexus/nexus-2025121800.0.0-c18c45.json @@ -0,0 +1,29598 @@ +{ + "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": "2025121800.0.0" + }, + "paths": { + "/device/auth": { + "post": { + "tags": [ + "console-auth" + ], + "summary": "Start an OAuth 2.0 Device Authorization Grant", + "description": "This endpoint is designed to be accessed from an *unauthenticated* API client. It generates and records a `device_code` and `user_code` which must be verified and confirmed prior to a token being granted.", + "operationId": "device_auth_request", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DeviceAuthRequest" + } + } + }, + "required": true + }, + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + } + }, + "/device/confirm": { + "post": { + "tags": [ + "console-auth" + ], + "summary": "Confirm an OAuth 2.0 Device Authorization Grant", + "description": "This endpoint is designed to be accessed by the user agent (browser), not the client requesting the token. So we do not actually return the token here; it will be returned in response to the poll on `/device/token`.\n\nSome special logic applies when authenticating this request with an existing device token instead of a console session: the requested TTL must not produce an expiration time later than the authenticating token's expiration. If no TTL was specified in the initial grant request, the expiration will be the lesser of the silo max and the authenticating token's expiration time. To get the longest allowed lifetime, omit the TTL and authenticate with a web console session.", + "operationId": "device_auth_confirm", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeviceAuthVerify" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/device/token": { + "post": { + "tags": [ + "console-auth" + ], + "summary": "Request a device access token", + "description": "This endpoint should be polled by the client until the user code is verified and the grant is confirmed.", + "operationId": "device_access_token", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DeviceAccessTokenRequest" + } + } + }, + "required": true + }, + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + } + }, + "/experimental/v1/probes": { + "get": { + "tags": [ + "experimental" + ], + "summary": "List instrumentation probes", + "operationId": "probe_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProbeInfoResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "project" + ] + } + }, + "post": { + "tags": [ + "experimental" + ], + "summary": "Create instrumentation probe", + "operationId": "probe_create", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProbeCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Probe" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/experimental/v1/probes/{probe}": { + "get": { + "tags": [ + "experimental" + ], + "summary": "View instrumentation probe", + "operationId": "probe_view", + "parameters": [ + { + "in": "path", + "name": "probe", + "description": "Name or ID of the probe", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProbeInfo" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "experimental" + ], + "summary": "Delete instrumentation probe", + "operationId": "probe_delete", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "probe", + "description": "Name or ID of the probe", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/experimental/v1/system/support-bundles": { + "get": { + "tags": [ + "experimental" + ], + "summary": "List all support bundles", + "operationId": "support_bundle_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/TimeAndIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupportBundleInfoResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "experimental" + ], + "summary": "Create a new support bundle", + "operationId": "support_bundle_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupportBundleCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupportBundleInfo" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/experimental/v1/system/support-bundles/{bundle_id}": { + "get": { + "tags": [ + "experimental" + ], + "summary": "View a support bundle", + "operationId": "support_bundle_view", + "parameters": [ + { + "in": "path", + "name": "bundle_id", + "description": "ID of the support bundle", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupportBundleInfo" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "experimental" + ], + "summary": "Update a support bundle", + "operationId": "support_bundle_update", + "parameters": [ + { + "in": "path", + "name": "bundle_id", + "description": "ID of the support bundle", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupportBundleUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupportBundleInfo" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "experimental" + ], + "summary": "Delete an existing support bundle", + "description": "May also be used to cancel a support bundle which is currently being collected, or to remove metadata for a support bundle that has failed.", + "operationId": "support_bundle_delete", + "parameters": [ + { + "in": "path", + "name": "bundle_id", + "description": "ID of the support bundle", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/experimental/v1/system/support-bundles/{bundle_id}/download": { + "get": { + "tags": [ + "experimental" + ], + "summary": "Download the contents of a support bundle", + "operationId": "support_bundle_download", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "bundle_id", + "description": "ID of the support bundle", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + }, + "head": { + "tags": [ + "experimental" + ], + "summary": "Download the metadata of a support bundle", + "operationId": "support_bundle_head", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "bundle_id", + "description": "ID of the support bundle", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + } + }, + "/experimental/v1/system/support-bundles/{bundle_id}/download/{file}": { + "get": { + "tags": [ + "experimental" + ], + "summary": "Download a file within a support bundle", + "operationId": "support_bundle_download_file", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "bundle_id", + "description": "ID of the support bundle", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "path", + "name": "file", + "description": "The file within the bundle to download", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + }, + "head": { + "tags": [ + "experimental" + ], + "summary": "Download the metadata of a file within the support bundle", + "operationId": "support_bundle_head_file", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "bundle_id", + "description": "ID of the support bundle", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "path", + "name": "file", + "description": "The file within the bundle to download", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + } + }, + "/experimental/v1/system/support-bundles/{bundle_id}/index": { + "get": { + "tags": [ + "experimental" + ], + "summary": "Download the index of a support bundle", + "operationId": "support_bundle_index", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "bundle_id", + "description": "ID of the support bundle", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + } + }, + "/login/{silo_name}/saml/{provider_name}": { + "post": { + "tags": [ + "login" + ], + "summary": "Authenticate a user via SAML", + "operationId": "login_saml", + "parameters": [ + { + "in": "path", + "name": "provider_name", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + }, + { + "in": "path", + "name": "silo_name", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + }, + "required": true + }, + "responses": { + "303": { + "description": "redirect (see other)", + "headers": { + "location": { + "description": "HTTP \"Location\" header", + "style": "simple", + "required": true, + "schema": { + "type": "string" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/affinity-groups": { + "get": { + "tags": [ + "experimental" + ], + "summary": "List affinity groups", + "operationId": "affinity_group_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AffinityGroupResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "project" + ] + } + }, + "post": { + "tags": [ + "experimental" + ], + "summary": "Create affinity group", + "operationId": "affinity_group_create", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AffinityGroupCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AffinityGroup" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/affinity-groups/{affinity_group}": { + "get": { + "tags": [ + "experimental" + ], + "summary": "Fetch affinity group", + "operationId": "affinity_group_view", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "affinity_group", + "description": "Name or ID of the affinity group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AffinityGroup" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "experimental" + ], + "summary": "Update affinity group", + "operationId": "affinity_group_update", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "affinity_group", + "description": "Name or ID of the affinity group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AffinityGroupUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AffinityGroup" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "experimental" + ], + "summary": "Delete affinity group", + "operationId": "affinity_group_delete", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "affinity_group", + "description": "Name or ID of the affinity group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/affinity-groups/{affinity_group}/members": { + "get": { + "tags": [ + "experimental" + ], + "summary": "List affinity group members", + "operationId": "affinity_group_member_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "path", + "name": "affinity_group", + "description": "Name or ID of the affinity group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AffinityGroupMemberResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/affinity-groups/{affinity_group}/members/instance/{instance}": { + "get": { + "tags": [ + "experimental" + ], + "summary": "Fetch affinity group member", + "operationId": "affinity_group_member_instance_view", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "affinity_group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AffinityGroupMember" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "tags": [ + "experimental" + ], + "summary": "Add member to affinity group", + "operationId": "affinity_group_member_instance_add", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "affinity_group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AffinityGroupMember" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "experimental" + ], + "summary": "Remove member from affinity group", + "operationId": "affinity_group_member_instance_delete", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "affinity_group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/alert-classes": { + "get": { + "tags": [ + "system/alerts" + ], + "summary": "List alert classes", + "operationId": "alert_class_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "filter", + "description": "An optional glob pattern for filtering alert class names.\n\nIf provided, only alert classes which match this glob pattern will be included in the response.", + "schema": { + "$ref": "#/components/schemas/AlertSubscription" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AlertClassResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/alert-receivers": { + "get": { + "tags": [ + "system/alerts" + ], + "summary": "List alert receivers", + "operationId": "alert_receiver_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AlertReceiverResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/alert-receivers/{receiver}": { + "get": { + "tags": [ + "system/alerts" + ], + "summary": "Fetch alert receiver", + "operationId": "alert_receiver_view", + "parameters": [ + { + "in": "path", + "name": "receiver", + "description": "The name or ID of the webhook receiver.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AlertReceiver" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "system/alerts" + ], + "summary": "Delete alert receiver", + "operationId": "alert_receiver_delete", + "parameters": [ + { + "in": "path", + "name": "receiver", + "description": "The name or ID of the webhook receiver.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/alert-receivers/{receiver}/deliveries": { + "get": { + "tags": [ + "system/alerts" + ], + "summary": "List delivery attempts to alert receiver", + "description": "Optional query parameters to this endpoint may be used to filter deliveries by state. If none of the `failed`, `pending` or `delivered` query parameters are present, all deliveries are returned. If one or more of these parameters are provided, only those which are set to \"true\" are included in the response.", + "operationId": "alert_delivery_list", + "parameters": [ + { + "in": "path", + "name": "receiver", + "description": "The name or ID of the webhook receiver.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "delivered", + "description": "If true, include deliveries which have succeeded.\n\nIf any of the \"pending\", \"failed\", or \"delivered\" query parameters are set to true, only deliveries matching those state(s) will be included in the response. If NO state filter parameters are set, then all deliveries are included.", + "schema": { + "nullable": true, + "type": "boolean" + } + }, + { + "in": "query", + "name": "failed", + "description": "If true, include deliveries which have failed permanently.\n\nIf any of the \"pending\", \"failed\", or \"delivered\" query parameters are set to true, only deliveries matching those state(s) will be included in the response. If NO state filter parameters are set, then all deliveries are included.\n\nA delivery fails permanently when the retry limit of three total attempts is reached without a successful delivery.", + "schema": { + "nullable": true, + "type": "boolean" + } + }, + { + "in": "query", + "name": "pending", + "description": "If true, include deliveries which are currently in progress.\n\nIf any of the \"pending\", \"failed\", or \"delivered\" query parameters are set to true, only deliveries matching those state(s) will be included in the response. If NO state filter parameters are set, then all deliveries are included.\n\nA delivery is considered \"pending\" if it has not yet been sent at all, or if a delivery attempt has failed but the delivery has retries remaining.", + "schema": { + "nullable": true, + "type": "boolean" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/TimeAndIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AlertDeliveryResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/alert-receivers/{receiver}/probe": { + "post": { + "tags": [ + "system/alerts" + ], + "summary": "Send liveness probe to alert receiver", + "description": "This endpoint synchronously sends a liveness probe to the selected alert receiver. The response message describes the outcome of the probe: either the successful response (as appropriate), or indication of why the probe failed.\n\nThe result of the probe is represented as an `AlertDelivery` model. Details relating to the status of the probe depend on the alert delivery mechanism, and are included in the `AlertDeliveryAttempts` model. For example, webhook receiver liveness probes include the HTTP status code returned by the receiver endpoint.\n\nNote that the response status is `200 OK` as long as a probe request was able to be sent to the receiver endpoint. If an HTTP-based receiver, such as a webhook, responds to the another status code, including an error, this will be indicated by the response body, *not* the status of the response.\n\nThe `resend` query parameter can be used to request re-delivery of failed events if the liveness probe succeeds. If it is set to true and the liveness probe succeeds, any alerts for which delivery to this receiver has failed will be queued for re-delivery.", + "operationId": "alert_receiver_probe", + "parameters": [ + { + "in": "path", + "name": "receiver", + "description": "The name or ID of the webhook receiver.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "resend", + "description": "If true, resend all events that have not been delivered successfully if the probe request succeeds.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AlertProbeResult" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/alert-receivers/{receiver}/subscriptions": { + "post": { + "tags": [ + "system/alerts" + ], + "summary": "Add alert receiver subscription", + "operationId": "alert_receiver_subscription_add", + "parameters": [ + { + "in": "path", + "name": "receiver", + "description": "The name or ID of the webhook receiver.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AlertSubscriptionCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AlertSubscriptionCreated" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/alert-receivers/{receiver}/subscriptions/{subscription}": { + "delete": { + "tags": [ + "system/alerts" + ], + "summary": "Remove alert receiver subscription", + "operationId": "alert_receiver_subscription_remove", + "parameters": [ + { + "in": "path", + "name": "receiver", + "description": "The name or ID of the webhook receiver.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "subscription", + "description": "The event class subscription itself.", + "required": true, + "schema": { + "$ref": "#/components/schemas/AlertSubscription" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/alerts/{alert_id}/resend": { + "post": { + "tags": [ + "system/alerts" + ], + "summary": "Request re-delivery of alert", + "operationId": "alert_delivery_resend", + "parameters": [ + { + "in": "path", + "name": "alert_id", + "description": "UUID of the alert", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "receiver", + "description": "The name or ID of the webhook receiver.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AlertDeliveryId" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/anti-affinity-groups": { + "get": { + "tags": [ + "affinity" + ], + "summary": "List anti-affinity groups", + "operationId": "anti_affinity_group_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AntiAffinityGroupResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "project" + ] + } + }, + "post": { + "tags": [ + "affinity" + ], + "summary": "Create anti-affinity group", + "operationId": "anti_affinity_group_create", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AntiAffinityGroupCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AntiAffinityGroup" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/anti-affinity-groups/{anti_affinity_group}": { + "get": { + "tags": [ + "affinity" + ], + "summary": "Fetch anti-affinity group", + "operationId": "anti_affinity_group_view", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "anti_affinity_group", + "description": "Name or ID of the anti affinity group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AntiAffinityGroup" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "affinity" + ], + "summary": "Update anti-affinity group", + "operationId": "anti_affinity_group_update", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "anti_affinity_group", + "description": "Name or ID of the anti affinity group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AntiAffinityGroupUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AntiAffinityGroup" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "affinity" + ], + "summary": "Delete anti-affinity group", + "operationId": "anti_affinity_group_delete", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "anti_affinity_group", + "description": "Name or ID of the anti affinity group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/anti-affinity-groups/{anti_affinity_group}/members": { + "get": { + "tags": [ + "affinity" + ], + "summary": "List anti-affinity group members", + "operationId": "anti_affinity_group_member_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "path", + "name": "anti_affinity_group", + "description": "Name or ID of the anti affinity group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AntiAffinityGroupMemberResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/anti-affinity-groups/{anti_affinity_group}/members/instance/{instance}": { + "get": { + "tags": [ + "affinity" + ], + "summary": "Fetch anti-affinity group member", + "operationId": "anti_affinity_group_member_instance_view", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "anti_affinity_group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AntiAffinityGroupMember" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "tags": [ + "affinity" + ], + "summary": "Add member to anti-affinity group", + "operationId": "anti_affinity_group_member_instance_add", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "anti_affinity_group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AntiAffinityGroupMember" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "affinity" + ], + "summary": "Remove member from anti-affinity group", + "operationId": "anti_affinity_group_member_instance_delete", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "anti_affinity_group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/auth-settings": { + "get": { + "tags": [ + "silos" + ], + "summary": "Fetch current silo's auth settings", + "operationId": "auth_settings_view", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloAuthSettings" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "silos" + ], + "summary": "Update current silo's auth settings", + "operationId": "auth_settings_update", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloAuthSettingsUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloAuthSettings" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/certificates": { + "get": { + "tags": [ + "silos" + ], + "summary": "List certificates for external endpoints", + "description": "Returns a list of TLS certificates used for the external API (for the current Silo). These are sorted by creation date, with the most recent certificates appearing first.", + "operationId": "certificate_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CertificateResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "silos" + ], + "summary": "Create new system-wide x.509 certificate", + "description": "This certificate is automatically used by the Oxide Control plane to serve external connections.", + "operationId": "certificate_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CertificateCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Certificate" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/certificates/{certificate}": { + "get": { + "tags": [ + "silos" + ], + "summary": "Fetch certificate", + "description": "Returns the details of a specific certificate", + "operationId": "certificate_view", + "parameters": [ + { + "in": "path", + "name": "certificate", + "description": "Name or ID of the certificate", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Certificate" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "silos" + ], + "summary": "Delete certificate", + "description": "Permanently delete a certificate. This operation cannot be undone.", + "operationId": "certificate_delete", + "parameters": [ + { + "in": "path", + "name": "certificate", + "description": "Name or ID of the certificate", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/disks": { + "get": { + "tags": [ + "disks" + ], + "summary": "List disks", + "operationId": "disk_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DiskResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "project" + ] + } + }, + "post": { + "tags": [ + "disks" + ], + "summary": "Create a disk", + "operationId": "disk_create", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DiskCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Disk" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/disks/{disk}": { + "get": { + "tags": [ + "disks" + ], + "summary": "Fetch disk", + "operationId": "disk_view", + "parameters": [ + { + "in": "path", + "name": "disk", + "description": "Name or ID of the disk", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Disk" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "disks" + ], + "summary": "Delete disk", + "operationId": "disk_delete", + "parameters": [ + { + "in": "path", + "name": "disk", + "description": "Name or ID of the disk", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/disks/{disk}/bulk-write": { + "post": { + "tags": [ + "disks" + ], + "summary": "Import blocks into disk", + "operationId": "disk_bulk_write_import", + "parameters": [ + { + "in": "path", + "name": "disk", + "description": "Name or ID of the disk", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportBlocksBulkWrite" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/disks/{disk}/bulk-write-start": { + "post": { + "tags": [ + "disks" + ], + "summary": "Start importing blocks into disk", + "description": "Start the process of importing blocks into a disk", + "operationId": "disk_bulk_write_import_start", + "parameters": [ + { + "in": "path", + "name": "disk", + "description": "Name or ID of the disk", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/disks/{disk}/bulk-write-stop": { + "post": { + "tags": [ + "disks" + ], + "summary": "Stop importing blocks into disk", + "description": "Stop the process of importing blocks into a disk", + "operationId": "disk_bulk_write_import_stop", + "parameters": [ + { + "in": "path", + "name": "disk", + "description": "Name or ID of the disk", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/disks/{disk}/finalize": { + "post": { + "tags": [ + "disks" + ], + "summary": "Confirm disk block import completion", + "operationId": "disk_finalize_import", + "parameters": [ + { + "in": "path", + "name": "disk", + "description": "Name or ID of the disk", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FinalizeDisk" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/floating-ips": { + "get": { + "tags": [ + "floating-ips" + ], + "summary": "List floating IPs", + "operationId": "floating_ip_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FloatingIpResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "project" + ] + } + }, + "post": { + "tags": [ + "floating-ips" + ], + "summary": "Create floating IP", + "operationId": "floating_ip_create", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FloatingIpCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FloatingIp" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/floating-ips/{floating_ip}": { + "get": { + "tags": [ + "floating-ips" + ], + "summary": "Fetch floating IP", + "operationId": "floating_ip_view", + "parameters": [ + { + "in": "path", + "name": "floating_ip", + "description": "Name or ID of the floating IP", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FloatingIp" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "floating-ips" + ], + "summary": "Update floating IP", + "operationId": "floating_ip_update", + "parameters": [ + { + "in": "path", + "name": "floating_ip", + "description": "Name or ID of the floating IP", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FloatingIpUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FloatingIp" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "floating-ips" + ], + "summary": "Delete floating IP", + "operationId": "floating_ip_delete", + "parameters": [ + { + "in": "path", + "name": "floating_ip", + "description": "Name or ID of the floating IP", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/floating-ips/{floating_ip}/attach": { + "post": { + "tags": [ + "floating-ips" + ], + "summary": "Attach floating IP", + "description": "Attach floating IP to an instance or other resource.", + "operationId": "floating_ip_attach", + "parameters": [ + { + "in": "path", + "name": "floating_ip", + "description": "Name or ID of the floating IP", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FloatingIpAttach" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FloatingIp" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/floating-ips/{floating_ip}/detach": { + "post": { + "tags": [ + "floating-ips" + ], + "summary": "Detach floating IP", + "operationId": "floating_ip_detach", + "parameters": [ + { + "in": "path", + "name": "floating_ip", + "description": "Name or ID of the floating IP", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FloatingIp" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/groups": { + "get": { + "tags": [ + "silos" + ], + "summary": "List groups", + "operationId": "group_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/groups/{group_id}": { + "get": { + "tags": [ + "silos" + ], + "summary": "Fetch group", + "operationId": "group_view", + "parameters": [ + { + "in": "path", + "name": "group_id", + "description": "ID of the group", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Group" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/images": { + "get": { + "tags": [ + "images" + ], + "summary": "List images", + "description": "List images which are global or scoped to the specified project. The images are returned sorted by creation date, with the most recent images appearing first.", + "operationId": "image_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "images" + ], + "summary": "Create image", + "description": "Create a new image in a project.", + "operationId": "image_create", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Image" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/images/{image}": { + "get": { + "tags": [ + "images" + ], + "summary": "Fetch image", + "description": "Fetch the details for a specific image in a project.", + "operationId": "image_view", + "parameters": [ + { + "in": "path", + "name": "image", + "description": "Name or ID of the image", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Image" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "images" + ], + "summary": "Delete image", + "description": "Permanently delete an image from a project. This operation cannot be undone. Any instances in the project using the image will continue to run, however new instances can not be created with this image.", + "operationId": "image_delete", + "parameters": [ + { + "in": "path", + "name": "image", + "description": "Name or ID of the image", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/images/{image}/demote": { + "post": { + "tags": [ + "images" + ], + "summary": "Demote silo image", + "description": "Demote silo image to be visible only to a specified project", + "operationId": "image_demote", + "parameters": [ + { + "in": "path", + "name": "image", + "description": "Name or ID of the image", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Image" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/images/{image}/promote": { + "post": { + "tags": [ + "images" + ], + "summary": "Promote project image", + "description": "Promote project image to be visible to all projects in the silo", + "operationId": "image_promote", + "parameters": [ + { + "in": "path", + "name": "image", + "description": "Name or ID of the image", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Image" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/instances": { + "get": { + "tags": [ + "instances" + ], + "summary": "List instances", + "operationId": "instance_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "project" + ] + } + }, + "post": { + "tags": [ + "instances" + ], + "summary": "Create instance", + "operationId": "instance_create", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Instance" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/instances/{instance}": { + "get": { + "tags": [ + "instances" + ], + "summary": "Fetch instance", + "operationId": "instance_view", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Instance" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "instances" + ], + "summary": "Update instance", + "operationId": "instance_update", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Instance" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "instances" + ], + "summary": "Delete instance", + "operationId": "instance_delete", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/instances/{instance}/affinity-groups": { + "get": { + "tags": [ + "experimental" + ], + "summary": "List affinity groups containing instance", + "operationId": "instance_affinity_group_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AffinityGroupResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/instances/{instance}/anti-affinity-groups": { + "get": { + "tags": [ + "instances" + ], + "summary": "List anti-affinity groups containing instance", + "operationId": "instance_anti_affinity_group_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AntiAffinityGroupResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/instances/{instance}/disks": { + "get": { + "tags": [ + "instances" + ], + "summary": "List disks for instance", + "operationId": "instance_disk_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DiskResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/instances/{instance}/disks/attach": { + "post": { + "tags": [ + "instances" + ], + "summary": "Attach disk to instance", + "operationId": "instance_disk_attach", + "parameters": [ + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DiskPath" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Disk" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/instances/{instance}/disks/detach": { + "post": { + "tags": [ + "instances" + ], + "summary": "Detach disk from instance", + "operationId": "instance_disk_detach", + "parameters": [ + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DiskPath" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Disk" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/instances/{instance}/external-ips": { + "get": { + "tags": [ + "instances" + ], + "summary": "List external IP addresses", + "operationId": "instance_external_ip_list", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalIpResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/instances/{instance}/external-ips/ephemeral": { + "post": { + "tags": [ + "instances" + ], + "summary": "Allocate and attach ephemeral IP to instance", + "operationId": "instance_ephemeral_ip_attach", + "parameters": [ + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EphemeralIpCreate" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalIp" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "instances" + ], + "summary": "Detach and deallocate ephemeral IP from instance", + "operationId": "instance_ephemeral_ip_detach", + "parameters": [ + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/instances/{instance}/multicast-groups": { + "get": { + "tags": [ + "experimental" + ], + "summary": "List multicast groups for instance", + "operationId": "instance_multicast_group_list", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupMemberResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/instances/{instance}/multicast-groups/{multicast_group}": { + "put": { + "tags": [ + "experimental" + ], + "summary": "Join multicast group.", + "description": "This is functionally equivalent to adding the instance via the group's member management endpoint or updating the instance's `multicast_groups` field. All approaches modify the same membership and trigger reconciliation.", + "operationId": "instance_multicast_group_join", + "parameters": [ + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "multicast_group", + "description": "Name or ID of the multicast group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupMember" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "experimental" + ], + "summary": "Leave multicast group.", + "description": "This is functionally equivalent to removing the instance via the group's member management endpoint or updating the instance's `multicast_groups` field. All approaches modify the same membership and trigger reconciliation.", + "operationId": "instance_multicast_group_leave", + "parameters": [ + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "multicast_group", + "description": "Name or ID of the multicast group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/instances/{instance}/reboot": { + "post": { + "tags": [ + "instances" + ], + "summary": "Reboot an instance", + "operationId": "instance_reboot", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Instance" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/instances/{instance}/serial-console": { + "get": { + "tags": [ + "instances" + ], + "summary": "Fetch instance serial console", + "operationId": "instance_serial_console", + "parameters": [ + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "from_start", + "description": "Character index in the serial buffer from which to read, counting the bytes output since instance start. If this is not provided, `most_recent` must be provided, and if this *is* provided, `most_recent` must *not* be provided.", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + { + "in": "query", + "name": "max_bytes", + "description": "Maximum number of bytes of buffered serial console contents to return. If the requested range runs to the end of the available buffer, the data returned will be shorter than `max_bytes`.", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + { + "in": "query", + "name": "most_recent", + "description": "Character index in the serial buffer from which to read, counting *backward* from the most recently buffered data retrieved from the instance. (See note on `from_start` about mutual exclusivity)", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `instance` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceSerialConsoleData" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/instances/{instance}/serial-console/stream": { + "get": { + "tags": [ + "instances" + ], + "summary": "Stream instance serial console", + "operationId": "instance_serial_console_stream", + "parameters": [ + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "most_recent", + "description": "Character index in the serial buffer from which to read, counting *backward* from the most recently buffered data retrieved from the instance.", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `instance` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + }, + "x-dropshot-websocket": {} + } + }, + "/v1/instances/{instance}/ssh-public-keys": { + "get": { + "tags": [ + "instances" + ], + "summary": "List SSH public keys for instance", + "description": "List SSH public keys injected via cloud-init during instance creation. Note that this list is a snapshot in time and will not reflect updates made after the instance is created.", + "operationId": "instance_ssh_public_key_list", + "parameters": [ + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SshKeyResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/instances/{instance}/start": { + "post": { + "tags": [ + "instances" + ], + "summary": "Boot instance", + "operationId": "instance_start", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Instance" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/instances/{instance}/stop": { + "post": { + "tags": [ + "instances" + ], + "summary": "Stop instance", + "operationId": "instance_stop", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Instance" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/internet-gateway-ip-addresses": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "List IP addresses attached to internet gateway", + "operationId": "internet_gateway_ip_address_list", + "parameters": [ + { + "in": "query", + "name": "gateway", + "description": "Name or ID of the internet gateway", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC, only required if `gateway` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternetGatewayIpAddressResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "gateway" + ] + } + }, + "post": { + "tags": [ + "vpcs" + ], + "summary": "Attach IP address to internet gateway", + "operationId": "internet_gateway_ip_address_create", + "parameters": [ + { + "in": "query", + "name": "gateway", + "description": "Name or ID of the internet gateway", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC, only required if `gateway` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternetGatewayIpAddressCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternetGatewayIpAddress" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/internet-gateway-ip-addresses/{address}": { + "delete": { + "tags": [ + "vpcs" + ], + "summary": "Detach IP address from internet gateway", + "operationId": "internet_gateway_ip_address_delete", + "parameters": [ + { + "in": "path", + "name": "address", + "description": "Name or ID of the IP address", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "cascade", + "description": "Also delete routes targeting this gateway element.", + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "gateway", + "description": "Name or ID of the internet gateway", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC, only required if `gateway` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/internet-gateway-ip-pools": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "List IP pools attached to internet gateway", + "operationId": "internet_gateway_ip_pool_list", + "parameters": [ + { + "in": "query", + "name": "gateway", + "description": "Name or ID of the internet gateway", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC, only required if `gateway` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternetGatewayIpPoolResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "gateway" + ] + } + }, + "post": { + "tags": [ + "vpcs" + ], + "summary": "Attach IP pool to internet gateway", + "operationId": "internet_gateway_ip_pool_create", + "parameters": [ + { + "in": "query", + "name": "gateway", + "description": "Name or ID of the internet gateway", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC, only required if `gateway` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternetGatewayIpPoolCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternetGatewayIpPool" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/internet-gateway-ip-pools/{pool}": { + "delete": { + "tags": [ + "vpcs" + ], + "summary": "Detach IP pool from internet gateway", + "operationId": "internet_gateway_ip_pool_delete", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the IP pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "cascade", + "description": "Also delete routes targeting this gateway element.", + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "gateway", + "description": "Name or ID of the internet gateway", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC, only required if `gateway` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/internet-gateways": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "List internet gateways", + "operationId": "internet_gateway_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternetGatewayResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "vpc" + ] + } + }, + "post": { + "tags": [ + "vpcs" + ], + "summary": "Create VPC internet gateway", + "operationId": "internet_gateway_create", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternetGatewayCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternetGateway" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/internet-gateways/{gateway}": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "Fetch internet gateway", + "operationId": "internet_gateway_view", + "parameters": [ + { + "in": "path", + "name": "gateway", + "description": "Name or ID of the gateway", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternetGateway" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "vpcs" + ], + "summary": "Delete internet gateway", + "operationId": "internet_gateway_delete", + "parameters": [ + { + "in": "path", + "name": "gateway", + "description": "Name or ID of the gateway", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "cascade", + "description": "Also delete routes targeting this gateway.", + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/ip-pools": { + "get": { + "tags": [ + "projects" + ], + "summary": "List IP pools", + "operationId": "project_ip_pool_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloIpPoolResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/ip-pools/{pool}": { + "get": { + "tags": [ + "projects" + ], + "summary": "Fetch IP pool", + "operationId": "project_ip_pool_view", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the IP pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloIpPool" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/login/{silo_name}/local": { + "post": { + "tags": [ + "login" + ], + "summary": "Authenticate a user via username and password", + "operationId": "login_local", + "parameters": [ + { + "in": "path", + "name": "silo_name", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UsernamePasswordCredentials" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/logout": { + "post": { + "tags": [ + "console-auth" + ], + "summary": "Log user out of web console by deleting session on client and server", + "operationId": "logout", + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/me": { + "get": { + "tags": [ + "current-user" + ], + "summary": "Fetch user for current session", + "operationId": "current_user_view", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CurrentUser" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/me/access-tokens": { + "get": { + "tags": [ + "tokens" + ], + "summary": "List access tokens", + "description": "List device access tokens for the currently authenticated user.", + "operationId": "current_user_access_token_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeviceAccessTokenResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/me/access-tokens/{token_id}": { + "delete": { + "tags": [ + "tokens" + ], + "summary": "Delete access token", + "description": "Delete a device access token for the currently authenticated user.", + "operationId": "current_user_access_token_delete", + "parameters": [ + { + "in": "path", + "name": "token_id", + "description": "ID of the token", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/me/groups": { + "get": { + "tags": [ + "current-user" + ], + "summary": "Fetch current user's groups", + "operationId": "current_user_groups", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/me/ssh-keys": { + "get": { + "tags": [ + "current-user" + ], + "summary": "List SSH public keys", + "description": "Lists SSH public keys for the currently authenticated user.", + "operationId": "current_user_ssh_key_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SshKeyResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "current-user" + ], + "summary": "Create SSH public key", + "description": "Create an SSH public key for the currently authenticated user.", + "operationId": "current_user_ssh_key_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SshKeyCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SshKey" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/me/ssh-keys/{ssh_key}": { + "get": { + "tags": [ + "current-user" + ], + "summary": "Fetch SSH public key", + "description": "Fetch SSH public key associated with the currently authenticated user.", + "operationId": "current_user_ssh_key_view", + "parameters": [ + { + "in": "path", + "name": "ssh_key", + "description": "Name or ID of the SSH key", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SshKey" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "current-user" + ], + "summary": "Delete SSH public key", + "description": "Delete an SSH public key associated with the currently authenticated user.", + "operationId": "current_user_ssh_key_delete", + "parameters": [ + { + "in": "path", + "name": "ssh_key", + "description": "Name or ID of the SSH key", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/metrics/{metric_name}": { + "get": { + "tags": [ + "metrics" + ], + "summary": "View metrics", + "description": "View CPU, memory, or storage utilization metrics at the silo or project level.", + "operationId": "silo_metric", + "parameters": [ + { + "in": "path", + "name": "metric_name", + "required": true, + "schema": { + "$ref": "#/components/schemas/SystemMetricName" + } + }, + { + "in": "query", + "name": "end_time", + "description": "An exclusive end time of metrics.", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "order", + "description": "Query result order", + "schema": { + "$ref": "#/components/schemas/PaginationOrder" + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "start_time", + "description": "An inclusive start time of metrics.", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MeasurementResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "end_time", + "start_time" + ] + } + } + }, + "/v1/multicast-groups": { + "get": { + "tags": [ + "experimental" + ], + "summary": "List all multicast groups.", + "operationId": "multicast_group_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "experimental" + ], + "summary": "Create a multicast group.", + "description": "Multicast groups are fleet-scoped resources that can be joined by instances across projects and silos. A single multicast IP serves all group members regardless of project or silo boundaries.", + "operationId": "multicast_group_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroup" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/multicast-groups/{multicast_group}": { + "get": { + "tags": [ + "experimental" + ], + "summary": "Fetch a multicast group.", + "operationId": "multicast_group_view", + "parameters": [ + { + "in": "path", + "name": "multicast_group", + "description": "Name or ID of the multicast group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroup" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "experimental" + ], + "summary": "Update a multicast group.", + "operationId": "multicast_group_update", + "parameters": [ + { + "in": "path", + "name": "multicast_group", + "description": "Name or ID of the multicast group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroup" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "experimental" + ], + "summary": "Delete a multicast group.", + "operationId": "multicast_group_delete", + "parameters": [ + { + "in": "path", + "name": "multicast_group", + "description": "Name or ID of the multicast group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/multicast-groups/{multicast_group}/members": { + "get": { + "tags": [ + "experimental" + ], + "summary": "List members of a multicast group.", + "operationId": "multicast_group_member_list", + "parameters": [ + { + "in": "path", + "name": "multicast_group", + "description": "Name or ID of the multicast group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupMemberResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "experimental" + ], + "summary": "Add instance to a multicast group.", + "description": "Functionally equivalent to updating the instance's `multicast_groups` field. Both approaches modify the same underlying membership and trigger the same reconciliation logic.\n\nSpecify instance by name (requires `?project=`) or UUID.", + "operationId": "multicast_group_member_add", + "parameters": [ + { + "in": "path", + "name": "multicast_group", + "description": "Name or ID of the multicast group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupMemberAdd" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupMember" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/multicast-groups/{multicast_group}/members/{instance}": { + "delete": { + "tags": [ + "experimental" + ], + "summary": "Remove instance from a multicast group.", + "description": "Functionally equivalent to removing the group from the instance's `multicast_groups` field. Both approaches modify the same underlying membership and trigger reconciliation.\n\nSpecify instance by name (requires `?project=`) or UUID.", + "operationId": "multicast_group_member_remove", + "parameters": [ + { + "in": "path", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "multicast_group", + "description": "Name or ID of the multicast group", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/network-interfaces": { + "get": { + "tags": [ + "instances" + ], + "summary": "List network interfaces", + "operationId": "instance_network_interface_list", + "parameters": [ + { + "in": "query", + "name": "instance", + "description": "Name or ID of the instance", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `instance` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceNetworkInterfaceResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "instance" + ] + } + }, + "post": { + "tags": [ + "instances" + ], + "summary": "Create network interface", + "operationId": "instance_network_interface_create", + "parameters": [ + { + "in": "query", + "name": "instance", + "description": "Name or ID of the instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `instance` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceNetworkInterfaceCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceNetworkInterface" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/network-interfaces/{interface}": { + "get": { + "tags": [ + "instances" + ], + "summary": "Fetch network interface", + "operationId": "instance_network_interface_view", + "parameters": [ + { + "in": "path", + "name": "interface", + "description": "Name or ID of the network interface", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "instance", + "description": "Name or ID of the instance", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `instance` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceNetworkInterface" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "instances" + ], + "summary": "Update network interface", + "operationId": "instance_network_interface_update", + "parameters": [ + { + "in": "path", + "name": "interface", + "description": "Name or ID of the network interface", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "instance", + "description": "Name or ID of the instance", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `instance` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceNetworkInterfaceUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceNetworkInterface" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "instances" + ], + "summary": "Delete network interface", + "description": "Note that the primary interface for an instance cannot be deleted if there are any secondary interfaces. A new primary interface must be designated first. The primary interface can be deleted if there are no secondary interfaces.", + "operationId": "instance_network_interface_delete", + "parameters": [ + { + "in": "path", + "name": "interface", + "description": "Name or ID of the network interface", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "instance", + "description": "Name or ID of the instance", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `instance` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/ping": { + "get": { + "tags": [ + "system/status" + ], + "summary": "Ping API", + "description": "Always responds with Ok if it responds at all.", + "operationId": "ping", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ping" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/policy": { + "get": { + "tags": [ + "silos" + ], + "summary": "Fetch current silo's IAM policy", + "operationId": "policy_view", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "silos" + ], + "summary": "Update current silo's IAM policy", + "operationId": "policy_update", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloRolePolicy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/projects": { + "get": { + "tags": [ + "projects" + ], + "summary": "List projects", + "operationId": "project_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "projects" + ], + "summary": "Create project", + "operationId": "project_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/projects/{project}": { + "get": { + "tags": [ + "projects" + ], + "summary": "Fetch project", + "operationId": "project_view", + "parameters": [ + { + "in": "path", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "projects" + ], + "summary": "Update a project", + "operationId": "project_update", + "parameters": [ + { + "in": "path", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "projects" + ], + "summary": "Delete project", + "operationId": "project_delete", + "parameters": [ + { + "in": "path", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/projects/{project}/policy": { + "get": { + "tags": [ + "projects" + ], + "summary": "Fetch project's IAM policy", + "operationId": "project_policy_view", + "parameters": [ + { + "in": "path", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "projects" + ], + "summary": "Update project's IAM policy", + "operationId": "project_policy_update", + "parameters": [ + { + "in": "path", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectRolePolicy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/snapshots": { + "get": { + "tags": [ + "snapshots" + ], + "summary": "List snapshots", + "operationId": "snapshot_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SnapshotResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "project" + ] + } + }, + "post": { + "tags": [ + "snapshots" + ], + "summary": "Create snapshot", + "description": "Creates a point-in-time snapshot from a disk.", + "operationId": "snapshot_create", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SnapshotCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Snapshot" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/snapshots/{snapshot}": { + "get": { + "tags": [ + "snapshots" + ], + "summary": "Fetch snapshot", + "operationId": "snapshot_view", + "parameters": [ + { + "in": "path", + "name": "snapshot", + "description": "Name or ID of the snapshot", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Snapshot" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "snapshots" + ], + "summary": "Delete snapshot", + "operationId": "snapshot_delete", + "parameters": [ + { + "in": "path", + "name": "snapshot", + "description": "Name or ID of the snapshot", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/audit-log": { + "get": { + "tags": [ + "system/audit-log" + ], + "summary": "View audit log", + "description": "A single item in the audit log represents both the beginning and end of the logged operation (represented by `time_started` and `time_completed`) so that clients do not have to find multiple entries and match them up by request ID to get the full picture of an operation. Because timestamps may not be unique, entries have also have a unique `id` that can be used to deduplicate items fetched from overlapping time intervals.\n\nAudit log entries are designed to be immutable: once you see an entry, fetching it again will never get you a different result. The list is ordered by `time_completed`, not `time_started`. If you fetch the audit log for a time range that is fully in the past, the resulting list is guaranteed to be complete, i.e., fetching the same timespan again later will always produce the same set of entries.", + "operationId": "audit_log_list", + "parameters": [ + { + "in": "query", + "name": "end_time", + "description": "Exclusive", + "schema": { + "nullable": true, + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/TimeAndIdSortMode" + } + }, + { + "in": "query", + "name": "start_time", + "description": "Required, inclusive", + "schema": { + "type": "string", + "format": "date-time" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuditLogEntryResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "start_time" + ] + } + } + }, + "/v1/system/hardware/disks": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "List physical disks", + "operationId": "physical_disk_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PhysicalDiskResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/hardware/disks/{disk_id}": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "Get a physical disk", + "operationId": "physical_disk_view", + "parameters": [ + { + "in": "path", + "name": "disk_id", + "description": "ID of the physical disk", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PhysicalDisk" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/hardware/rack-switch-port/{rack_id}/{switch_location}/{port}/lldp/neighbors": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Fetch the LLDP neighbors seen on a switch port", + "operationId": "networking_switch_port_lldp_neighbors", + "parameters": [ + { + "in": "path", + "name": "port", + "description": "A name to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + }, + { + "in": "path", + "name": "rack_id", + "description": "A rack id to use when selecting switch ports.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "path", + "name": "switch_location", + "description": "A switch location to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LldpNeighborResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/hardware/racks": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "List racks", + "operationId": "rack_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RackResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/hardware/racks/{rack_id}": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "Fetch rack", + "operationId": "rack_view", + "parameters": [ + { + "in": "path", + "name": "rack_id", + "description": "ID of the rack", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Rack" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/hardware/sleds": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "List sleds", + "operationId": "sled_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "system/hardware" + ], + "summary": "Add sled to initialized rack", + "operationId": "sled_add", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UninitializedSledId" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledId" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/hardware/sleds/{sled_id}": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "Fetch sled", + "operationId": "sled_view", + "parameters": [ + { + "in": "path", + "name": "sled_id", + "description": "ID of the sled", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Sled" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/hardware/sleds/{sled_id}/disks": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "List physical disks attached to sleds", + "operationId": "sled_physical_disk_list", + "parameters": [ + { + "in": "path", + "name": "sled_id", + "description": "ID of the sled", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PhysicalDiskResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/hardware/sleds/{sled_id}/instances": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "List instances running on given sled", + "operationId": "sled_instance_list", + "parameters": [ + { + "in": "path", + "name": "sled_id", + "description": "ID of the sled", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledInstanceResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/hardware/sleds/{sled_id}/provision-policy": { + "put": { + "tags": [ + "system/hardware" + ], + "summary": "Set sled provision policy", + "operationId": "sled_set_provision_policy", + "parameters": [ + { + "in": "path", + "name": "sled_id", + "description": "ID of the sled", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledProvisionPolicyParams" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledProvisionPolicyResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/hardware/sleds-uninitialized": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "List uninitialized sleds", + "operationId": "sled_list_uninitialized", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UninitializedSledResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/hardware/switch-port": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "List switch ports", + "operationId": "networking_switch_port_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + }, + { + "in": "query", + "name": "switch_port_id", + "description": "An optional switch port id to use when listing switch ports.", + "schema": { + "nullable": true, + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchPortResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/hardware/switch-port/{port}/lldp/config": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Fetch the LLDP configuration for a switch port", + "operationId": "networking_switch_port_lldp_config_view", + "parameters": [ + { + "in": "path", + "name": "port", + "description": "A name to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + }, + { + "in": "query", + "name": "rack_id", + "description": "A rack id to use when selecting switch ports.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "switch_location", + "description": "A switch location to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LldpLinkConfig" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "tags": [ + "system/networking" + ], + "summary": "Update the LLDP configuration for a switch port", + "operationId": "networking_switch_port_lldp_config_update", + "parameters": [ + { + "in": "path", + "name": "port", + "description": "A name to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + }, + { + "in": "query", + "name": "rack_id", + "description": "A rack id to use when selecting switch ports.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "switch_location", + "description": "A switch location to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LldpLinkConfig" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/hardware/switch-port/{port}/settings": { + "post": { + "tags": [ + "system/hardware" + ], + "summary": "Apply switch port settings", + "operationId": "networking_switch_port_apply_settings", + "parameters": [ + { + "in": "path", + "name": "port", + "description": "A name to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + }, + { + "in": "query", + "name": "rack_id", + "description": "A rack id to use when selecting switch ports.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "switch_location", + "description": "A switch location to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchPortApplySettings" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "system/hardware" + ], + "summary": "Clear switch port settings", + "operationId": "networking_switch_port_clear_settings", + "parameters": [ + { + "in": "path", + "name": "port", + "description": "A name to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + }, + { + "in": "query", + "name": "rack_id", + "description": "A rack id to use when selecting switch ports.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "switch_location", + "description": "A switch location to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/hardware/switch-port/{port}/status": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "Get switch port status", + "operationId": "networking_switch_port_status", + "parameters": [ + { + "in": "path", + "name": "port", + "description": "A name to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + }, + { + "in": "query", + "name": "rack_id", + "description": "A rack id to use when selecting switch ports.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "switch_location", + "description": "A switch location to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchLinkState" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/hardware/switches": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "List switches", + "operationId": "switch_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/hardware/switches/{switch_id}": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "Fetch switch", + "operationId": "switch_view", + "parameters": [ + { + "in": "path", + "name": "switch_id", + "description": "ID of the switch", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Switch" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/identity-providers": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "List identity providers for silo", + "description": "List identity providers for silo by silo name or ID.", + "operationId": "silo_identity_provider_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "silo", + "description": "Name or ID of the silo", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IdentityProviderResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "silo" + ] + } + } + }, + "/v1/system/identity-providers/local/users": { + "post": { + "tags": [ + "system/silos" + ], + "summary": "Create user", + "description": "Users can only be created in Silos with `provision_type` == `Fixed`. Otherwise, Silo users are just-in-time (JIT) provisioned when a user first logs in using an external Identity Provider.", + "operationId": "local_idp_user_create", + "parameters": [ + { + "in": "query", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/identity-providers/local/users/{user_id}": { + "delete": { + "tags": [ + "system/silos" + ], + "summary": "Delete user", + "operationId": "local_idp_user_delete", + "parameters": [ + { + "in": "path", + "name": "user_id", + "description": "The user's internal ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/identity-providers/local/users/{user_id}/set-password": { + "post": { + "tags": [ + "system/silos" + ], + "summary": "Set or invalidate user's password", + "description": "Passwords can only be updated for users in Silos with identity mode `LocalOnly`.", + "operationId": "local_idp_user_set_password", + "parameters": [ + { + "in": "path", + "name": "user_id", + "description": "The user's internal ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPassword" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/identity-providers/saml": { + "post": { + "tags": [ + "system/silos" + ], + "summary": "Create SAML identity provider", + "operationId": "saml_identity_provider_create", + "parameters": [ + { + "in": "query", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SamlIdentityProviderCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SamlIdentityProvider" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/identity-providers/saml/{provider}": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "Fetch SAML identity provider", + "operationId": "saml_identity_provider_view", + "parameters": [ + { + "in": "path", + "name": "provider", + "description": "Name or ID of the SAML identity provider", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "silo", + "description": "Name or ID of the silo", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SamlIdentityProvider" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/ip-pools": { + "get": { + "tags": [ + "system/ip-pools" + ], + "summary": "List IP pools", + "operationId": "ip_pool_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "system/ip-pools" + ], + "summary": "Create IP pool", + "description": "IPv6 is not yet supported for unicast pools.", + "operationId": "ip_pool_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPool" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/ip-pools/{pool}": { + "get": { + "tags": [ + "system/ip-pools" + ], + "summary": "Fetch IP pool", + "operationId": "ip_pool_view", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the IP pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPool" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "system/ip-pools" + ], + "summary": "Update IP pool", + "operationId": "ip_pool_update", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the IP pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPool" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "system/ip-pools" + ], + "summary": "Delete IP pool", + "operationId": "ip_pool_delete", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the IP pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/ip-pools/{pool}/ranges": { + "get": { + "tags": [ + "system/ip-pools" + ], + "summary": "List ranges for IP pool", + "description": "Ranges are ordered by their first address.", + "operationId": "ip_pool_range_list", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the IP pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolRangeResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/ip-pools/{pool}/ranges/add": { + "post": { + "tags": [ + "system/ip-pools" + ], + "summary": "Add range to IP pool.", + "description": "IPv6 ranges are not allowed yet for unicast pools.\n\nFor multicast pools, all ranges must be either Any-Source Multicast (ASM) or Source-Specific Multicast (SSM), but not both. Mixing ASM and SSM ranges in the same pool is not allowed.\n\nASM: IPv4 addresses outside 232.0.0.0/8, IPv6 addresses with flag field != 3 SSM: IPv4 addresses in 232.0.0.0/8, IPv6 addresses with flag field = 3", + "operationId": "ip_pool_range_add", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the IP pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpRange" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolRange" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/ip-pools/{pool}/ranges/remove": { + "post": { + "tags": [ + "system/ip-pools" + ], + "summary": "Remove range from IP pool", + "operationId": "ip_pool_range_remove", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the IP pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpRange" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/ip-pools/{pool}/silos": { + "get": { + "tags": [ + "system/ip-pools" + ], + "summary": "List IP pool's linked silos", + "operationId": "ip_pool_silo_list", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the IP pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolSiloLinkResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "system/ip-pools" + ], + "summary": "Link IP pool to silo", + "description": "Users in linked silos can allocate external IPs from this pool for their instances. A silo can have at most one default pool. IPs are allocated from the default pool when users ask for one without specifying a pool.", + "operationId": "ip_pool_silo_link", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the IP pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolLinkSilo" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolSiloLink" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/ip-pools/{pool}/silos/{silo}": { + "put": { + "tags": [ + "system/ip-pools" + ], + "summary": "Make IP pool default for silo", + "description": "When a user asks for an IP (e.g., at instance create time) without specifying a pool, the IP comes from the default pool if a default is configured. When a pool is made the default for a silo, any existing default will remain linked to the silo, but will no longer be the default.", + "operationId": "ip_pool_silo_update", + "parameters": [ + { + "in": "path", + "name": "pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolSiloUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolSiloLink" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "system/ip-pools" + ], + "summary": "Unlink IP pool from silo", + "description": "Will fail if there are any outstanding IPs allocated in the silo.", + "operationId": "ip_pool_silo_unlink", + "parameters": [ + { + "in": "path", + "name": "pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/ip-pools/{pool}/utilization": { + "get": { + "tags": [ + "system/ip-pools" + ], + "summary": "Fetch IP pool utilization", + "operationId": "ip_pool_utilization_view", + "parameters": [ + { + "in": "path", + "name": "pool", + "description": "Name or ID of the IP pool", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolUtilization" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/ip-pools-service": { + "get": { + "tags": [ + "system/ip-pools" + ], + "summary": "Fetch Oxide service IP pool", + "operationId": "ip_pool_service_view", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPool" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/ip-pools-service/ranges": { + "get": { + "tags": [ + "system/ip-pools" + ], + "summary": "List IP ranges for the Oxide service pool", + "description": "Ranges are ordered by their first address.", + "operationId": "ip_pool_service_range_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolRangeResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/ip-pools-service/ranges/add": { + "post": { + "tags": [ + "system/ip-pools" + ], + "summary": "Add IP range to Oxide service pool", + "description": "IPv6 ranges are not allowed yet.", + "operationId": "ip_pool_service_range_add", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpRange" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolRange" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/ip-pools-service/ranges/remove": { + "post": { + "tags": [ + "system/ip-pools" + ], + "summary": "Remove IP range from Oxide service pool", + "operationId": "ip_pool_service_range_remove", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpRange" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/metrics/{metric_name}": { + "get": { + "tags": [ + "system/metrics" + ], + "summary": "View metrics", + "description": "View CPU, memory, or storage utilization metrics at the fleet or silo level.", + "operationId": "system_metric", + "parameters": [ + { + "in": "path", + "name": "metric_name", + "required": true, + "schema": { + "$ref": "#/components/schemas/SystemMetricName" + } + }, + { + "in": "query", + "name": "end_time", + "description": "An exclusive end time of metrics.", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "order", + "description": "Query result order", + "schema": { + "$ref": "#/components/schemas/PaginationOrder" + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "start_time", + "description": "An inclusive start time of metrics.", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "silo", + "description": "Name or ID of the silo", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MeasurementResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "end_time", + "start_time" + ] + } + } + }, + "/v1/system/multicast-groups/by-ip/{address}": { + "get": { + "tags": [ + "experimental" + ], + "summary": "Look up multicast group by IP address.", + "operationId": "lookup_multicast_group_by_ip", + "parameters": [ + { + "in": "path", + "name": "address", + "description": "IP address of the multicast group", + "required": true, + "schema": { + "type": "string", + "format": "ip" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroup" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/address-lot": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "List address lots", + "operationId": "networking_address_lot_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddressLotResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "system/networking" + ], + "summary": "Create address lot", + "operationId": "networking_address_lot_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddressLotCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddressLotCreateResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/address-lot/{address_lot}": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Fetch address lot", + "operationId": "networking_address_lot_view", + "parameters": [ + { + "in": "path", + "name": "address_lot", + "description": "Name or ID of the address lot", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddressLotViewResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "system/networking" + ], + "summary": "Delete address lot", + "operationId": "networking_address_lot_delete", + "parameters": [ + { + "in": "path", + "name": "address_lot", + "description": "Name or ID of the address lot", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/address-lot/{address_lot}/blocks": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "List blocks in address lot", + "operationId": "networking_address_lot_block_list", + "parameters": [ + { + "in": "path", + "name": "address_lot", + "description": "Name or ID of the address lot", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddressLotBlockResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/networking/allow-list": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Get user-facing services IP allowlist", + "operationId": "networking_allow_list_view", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllowList" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "system/networking" + ], + "summary": "Update user-facing services IP allowlist", + "operationId": "networking_allow_list_update", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllowListUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllowList" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/bfd-disable": { + "post": { + "tags": [ + "system/networking" + ], + "summary": "Disable a BFD session", + "operationId": "networking_bfd_disable", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BfdSessionDisable" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/bfd-enable": { + "post": { + "tags": [ + "system/networking" + ], + "summary": "Enable a BFD session", + "operationId": "networking_bfd_enable", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BfdSessionEnable" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/bfd-status": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Get BFD status", + "operationId": "networking_bfd_status", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_BfdStatus", + "type": "array", + "items": { + "$ref": "#/components/schemas/BfdStatus" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/bgp": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "List BGP configurations", + "operationId": "networking_bgp_config_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BgpConfigResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "system/networking" + ], + "summary": "Create new BGP configuration", + "operationId": "networking_bgp_config_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BgpConfigCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BgpConfig" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "system/networking" + ], + "summary": "Delete BGP configuration", + "operationId": "networking_bgp_config_delete", + "parameters": [ + { + "in": "query", + "name": "name_or_id", + "description": "A name or id to use when selecting BGP config.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/bgp-announce-set": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "List BGP announce sets", + "operationId": "networking_bgp_announce_set_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_BgpAnnounceSet", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpAnnounceSet" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "put": { + "tags": [ + "system/networking" + ], + "summary": "Update BGP announce set", + "description": "If the announce set exists, this endpoint replaces the existing announce set with the one specified.", + "operationId": "networking_bgp_announce_set_update", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BgpAnnounceSetCreate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BgpAnnounceSet" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/bgp-announce-set/{announce_set}": { + "delete": { + "tags": [ + "system/networking" + ], + "summary": "Delete BGP announce set", + "operationId": "networking_bgp_announce_set_delete", + "parameters": [ + { + "in": "path", + "name": "announce_set", + "description": "Name or ID of the announce set", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/bgp-announce-set/{announce_set}/announcement": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Get originated routes for a specified BGP announce set", + "operationId": "networking_bgp_announcement_list", + "parameters": [ + { + "in": "path", + "name": "announce_set", + "description": "Name or ID of the announce set", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_BgpAnnouncement", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpAnnouncement" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/bgp-exported": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Get BGP exported routes", + "operationId": "networking_bgp_exported", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BgpExported" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/bgp-message-history": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Get BGP router message history", + "operationId": "networking_bgp_message_history", + "parameters": [ + { + "in": "query", + "name": "asn", + "description": "The ASN to filter on. Required.", + "required": true, + "schema": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AggregateBgpMessageHistory" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/bgp-routes-ipv4": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Get imported IPv4 BGP routes", + "operationId": "networking_bgp_imported_routes_ipv4", + "parameters": [ + { + "in": "query", + "name": "asn", + "description": "The ASN to filter on. Required.", + "required": true, + "schema": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_BgpImportedRouteIpv4", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpImportedRouteIpv4" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/bgp-status": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Get BGP peer status", + "operationId": "networking_bgp_status", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_BgpPeerStatus", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpPeerStatus" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/inbound-icmp": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Return whether API services can receive limited ICMP traffic", + "operationId": "networking_inbound_icmp_view", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceIcmpConfig" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "system/networking" + ], + "summary": "Set whether API services can receive limited ICMP traffic", + "operationId": "networking_inbound_icmp_update", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceIcmpConfig" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/loopback-address": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "List loopback addresses", + "operationId": "networking_loopback_address_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoopbackAddressResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "system/networking" + ], + "summary": "Create loopback address", + "operationId": "networking_loopback_address_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoopbackAddressCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoopbackAddress" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/loopback-address/{rack_id}/{switch_location}/{address}/{subnet_mask}": { + "delete": { + "tags": [ + "system/networking" + ], + "summary": "Delete loopback address", + "operationId": "networking_loopback_address_delete", + "parameters": [ + { + "in": "path", + "name": "address", + "description": "The IP address and subnet mask to use when selecting the loopback address.", + "required": true, + "schema": { + "type": "string", + "format": "ip" + } + }, + { + "in": "path", + "name": "rack_id", + "description": "The rack to use when selecting the loopback address.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "path", + "name": "subnet_mask", + "description": "The IP address and subnet mask to use when selecting the loopback address.", + "required": true, + "schema": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + { + "in": "path", + "name": "switch_location", + "description": "The switch location to use when selecting the loopback address.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/switch-port-settings": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "List switch port settings", + "operationId": "networking_switch_port_settings_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "port_settings", + "description": "An optional name or id to use when selecting port settings.", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchPortSettingsIdentityResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "system/networking" + ], + "summary": "Create switch port settings", + "operationId": "networking_switch_port_settings_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchPortSettingsCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchPortSettings" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "system/networking" + ], + "summary": "Delete switch port settings", + "operationId": "networking_switch_port_settings_delete", + "parameters": [ + { + "in": "query", + "name": "port_settings", + "description": "An optional name or id to use when selecting port settings.", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/switch-port-settings/{port}": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Get information about switch port", + "operationId": "networking_switch_port_settings_view", + "parameters": [ + { + "in": "path", + "name": "port", + "description": "A name or id to use when selecting switch port settings info objects.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchPortSettings" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/policy": { + "get": { + "tags": [ + "policy" + ], + "summary": "Fetch top-level IAM policy", + "operationId": "system_policy_view", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FleetRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "policy" + ], + "summary": "Update top-level IAM policy", + "operationId": "system_policy_update", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FleetRolePolicy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FleetRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/scim/tokens": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "List SCIM tokens", + "description": "Specify the silo by name or ID using the `silo` query parameter.", + "operationId": "scim_token_list", + "parameters": [ + { + "in": "query", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_ScimClientBearerToken", + "type": "array", + "items": { + "$ref": "#/components/schemas/ScimClientBearerToken" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "tags": [ + "system/silos" + ], + "summary": "Create SCIM token", + "description": "Specify the silo by name or ID using the `silo` query parameter. Be sure to save the bearer token in the response. It will not be retrievable later through the token view and list endpoints.", + "operationId": "scim_token_create", + "parameters": [ + { + "in": "query", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScimClientBearerTokenValue" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/scim/tokens/{token_id}": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "Fetch SCIM token", + "description": "Specify the silo by name or ID using the `silo` query parameter.", + "operationId": "scim_token_view", + "parameters": [ + { + "in": "path", + "name": "token_id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScimClientBearerToken" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "system/silos" + ], + "summary": "Delete SCIM token", + "description": "Specify the silo by name or ID using the `silo` query parameter.", + "operationId": "scim_token_delete", + "parameters": [ + { + "in": "path", + "name": "token_id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/silo-quotas": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "Lists resource quotas for all silos", + "operationId": "system_quotas_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloQuotasResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/silos": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "List silos", + "description": "Lists silos that are discoverable based on the current permissions.", + "operationId": "silo_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "system/silos" + ], + "summary": "Create a silo", + "operationId": "silo_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Silo" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/silos/{silo}": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "Fetch silo", + "description": "Fetch silo by name or ID.", + "operationId": "silo_view", + "parameters": [ + { + "in": "path", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Silo" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "system/silos" + ], + "summary": "Delete a silo", + "description": "Delete a silo by name or ID.", + "operationId": "silo_delete", + "parameters": [ + { + "in": "path", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/silos/{silo}/ip-pools": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "List IP pools linked to silo", + "description": "Linked IP pools are available to users in the specified silo. A silo can have at most one default pool. IPs are allocated from the default pool when users ask for one without specifying a pool.", + "operationId": "silo_ip_pool_list", + "parameters": [ + { + "in": "path", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloIpPoolResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/silos/{silo}/policy": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "Fetch silo IAM policy", + "operationId": "silo_policy_view", + "parameters": [ + { + "in": "path", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "system/silos" + ], + "summary": "Update silo IAM policy", + "operationId": "silo_policy_update", + "parameters": [ + { + "in": "path", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloRolePolicy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/silos/{silo}/quotas": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "Fetch resource quotas for silo", + "operationId": "silo_quotas_view", + "parameters": [ + { + "in": "path", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloQuotas" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "system/silos" + ], + "summary": "Update resource quotas for silo", + "description": "If a quota value is not specified, it will remain unchanged.", + "operationId": "silo_quotas_update", + "parameters": [ + { + "in": "path", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloQuotasUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloQuotas" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/timeseries/query": { + "post": { + "tags": [ + "system/metrics" + ], + "summary": "Run timeseries query", + "description": "Queries are written in OxQL.", + "operationId": "system_timeseries_query", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeseriesQuery" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OxqlQueryResult" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/timeseries/schemas": { + "get": { + "tags": [ + "system/metrics" + ], + "summary": "List timeseries schemas", + "operationId": "system_timeseries_schema_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeseriesSchemaResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/update/repositories": { + "get": { + "tags": [ + "system/update" + ], + "summary": "List all TUF repositories", + "description": "Returns a paginated list of all TUF repositories ordered by system version (newest first by default).", + "operationId": "system_update_repository_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/VersionSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TufRepoResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "put": { + "tags": [ + "system/update" + ], + "summary": "Upload system release repository", + "description": "System release repositories are verified by the updates trust store.", + "operationId": "system_update_repository_upload", + "parameters": [ + { + "in": "query", + "name": "file_name", + "description": "The name of the uploaded file.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TufRepoUpload" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/update/repositories/{system_version}": { + "get": { + "tags": [ + "system/update" + ], + "summary": "Fetch system release repository by version", + "operationId": "system_update_repository_view", + "parameters": [ + { + "in": "path", + "name": "system_version", + "description": "The version to get.", + "required": true, + "schema": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TufRepo" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/update/status": { + "get": { + "tags": [ + "system/update" + ], + "summary": "Fetch system update status", + "description": "Returns information about the current target release and the progress of system software updates.", + "operationId": "system_update_status", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateStatus" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/update/target-release": { + "put": { + "tags": [ + "system/update" + ], + "summary": "Set target release", + "description": "Set the current target release of the rack's system software. The rack reconfigurator will treat the software specified here as a goal state for the rack's software, and attempt to asynchronously update to that release. Use the update status endpoint to view the current target release.", + "operationId": "target_release_update", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetTargetReleaseParams" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/update/trust-roots": { + "get": { + "tags": [ + "system/update" + ], + "summary": "List root roles in the updates trust store", + "description": "A root role is a JSON document describing the cryptographic keys that are trusted to sign system release repositories, as described by The Update Framework. Uploading a repository requires its metadata to be signed by keys trusted by the trust store.", + "operationId": "system_update_trust_root_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdatesTrustRootResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "system/update" + ], + "summary": "Add trusted root role to updates trust store", + "operationId": "system_update_trust_root_create", + "requestBody": { + "content": { + "application/json": { + "schema": {} + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdatesTrustRoot" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/update/trust-roots/{trust_root_id}": { + "get": { + "tags": [ + "system/update" + ], + "summary": "Fetch trusted root role", + "operationId": "system_update_trust_root_view", + "parameters": [ + { + "in": "path", + "name": "trust_root_id", + "description": "ID of the trust root", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdatesTrustRoot" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "system/update" + ], + "summary": "Delete trusted root role", + "description": "Note that this method does not currently check for any uploaded system release repositories that would become untrusted after deleting the root role.", + "operationId": "system_update_trust_root_delete", + "parameters": [ + { + "in": "path", + "name": "trust_root_id", + "description": "ID of the trust root", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/users": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "List built-in (system) users in silo", + "operationId": "silo_user_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "silo", + "description": "Name or ID of the silo", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "silo" + ] + } + } + }, + "/v1/system/users/{user_id}": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "Fetch built-in (system) user", + "operationId": "silo_user_view", + "parameters": [ + { + "in": "path", + "name": "user_id", + "description": "The user's internal ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/users-builtin": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "List built-in users", + "operationId": "user_builtin_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserBuiltinResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/users-builtin/{user}": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "Fetch built-in user", + "operationId": "user_builtin_view", + "parameters": [ + { + "in": "path", + "name": "user", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserBuiltin" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/utilization/silos": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "List current utilization state for all silos", + "operationId": "silo_utilization_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloUtilizationResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/utilization/silos/{silo}": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "Fetch current utilization for given silo", + "operationId": "silo_utilization_view", + "parameters": [ + { + "in": "path", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloUtilization" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/timeseries/query": { + "post": { + "tags": [ + "experimental" + ], + "summary": "Run project-scoped timeseries query", + "description": "Queries are written in OxQL. Project must be specified by name or ID in URL query parameter. The OxQL query will only return timeseries data from the specified project.", + "operationId": "timeseries_query", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeseriesQuery" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OxqlQueryResult" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/users": { + "get": { + "tags": [ + "silos" + ], + "summary": "List users", + "operationId": "user_list", + "parameters": [ + { + "in": "query", + "name": "group", + "schema": { + "nullable": true, + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/users/{user_id}": { + "get": { + "tags": [ + "silos" + ], + "summary": "Fetch user", + "operationId": "user_view", + "parameters": [ + { + "in": "path", + "name": "user_id", + "description": "ID of the user", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/users/{user_id}/access-tokens": { + "get": { + "tags": [ + "silos" + ], + "summary": "List user's access tokens", + "operationId": "user_token_list", + "parameters": [ + { + "in": "path", + "name": "user_id", + "description": "ID of the user", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeviceAccessTokenResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/users/{user_id}/logout": { + "post": { + "tags": [ + "silos" + ], + "summary": "Log user out", + "description": "Silo admins can use this endpoint to log the specified user out by deleting all of their tokens AND sessions. This cannot be undone.", + "operationId": "user_logout", + "parameters": [ + { + "in": "path", + "name": "user_id", + "description": "ID of the user", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/users/{user_id}/sessions": { + "get": { + "tags": [ + "silos" + ], + "summary": "List user's console sessions", + "operationId": "user_session_list", + "parameters": [ + { + "in": "path", + "name": "user_id", + "description": "ID of the user", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConsoleSessionResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/utilization": { + "get": { + "tags": [ + "silos" + ], + "summary": "Fetch resource utilization for user's current silo", + "operationId": "utilization_view", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Utilization" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpc-firewall-rules": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "List firewall rules", + "operationId": "vpc_firewall_rules_view", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcFirewallRules" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "vpcs" + ], + "summary": "Replace firewall rules", + "description": "The maximum number of rules per VPC is 1024.\n\nTargets are used to specify the set of instances to which a firewall rule applies. You can target instances directly by name, or specify a VPC, VPC subnet, IP, or IP subnet, which will apply the rule to traffic going to all matching instances. Targets are additive: the rule applies to instances matching ANY target. The maximum number of targets is 256.\n\nFilters reduce the scope of a firewall rule. Without filters, the rule applies to all packets to the targets (or from the targets, if it's an outbound rule). With multiple filters, the rule applies only to packets matching ALL filters. The maximum number of each type of filter is 256.", + "operationId": "vpc_firewall_rules_update", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcFirewallRuleUpdateParams" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcFirewallRules" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpc-router-routes": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "List routes", + "description": "List the routes associated with a router in a particular VPC.", + "operationId": "vpc_router_route_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "router", + "description": "Name or ID of the router", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC, only required if `router` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouterRouteResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "router" + ] + } + }, + "post": { + "tags": [ + "vpcs" + ], + "summary": "Create route", + "operationId": "vpc_router_route_create", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "router", + "description": "Name or ID of the router", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC, only required if `router` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouterRouteCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouterRoute" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpc-router-routes/{route}": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "Fetch route", + "operationId": "vpc_router_route_view", + "parameters": [ + { + "in": "path", + "name": "route", + "description": "Name or ID of the route", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "router", + "description": "Name or ID of the router", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC, only required if `router` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouterRoute" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "vpcs" + ], + "summary": "Update route", + "operationId": "vpc_router_route_update", + "parameters": [ + { + "in": "path", + "name": "route", + "description": "Name or ID of the route", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "router", + "description": "Name or ID of the router", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC, only required if `router` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouterRouteUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouterRoute" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "vpcs" + ], + "summary": "Delete route", + "operationId": "vpc_router_route_delete", + "parameters": [ + { + "in": "path", + "name": "route", + "description": "Name or ID of the route", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "router", + "description": "Name or ID of the router", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC, only required if `router` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpc-routers": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "List routers", + "operationId": "vpc_router_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcRouterResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "vpc" + ] + } + }, + "post": { + "tags": [ + "vpcs" + ], + "summary": "Create VPC router", + "operationId": "vpc_router_create", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcRouterCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcRouter" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpc-routers/{router}": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "Fetch router", + "operationId": "vpc_router_view", + "parameters": [ + { + "in": "path", + "name": "router", + "description": "Name or ID of the router", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcRouter" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "vpcs" + ], + "summary": "Update router", + "operationId": "vpc_router_update", + "parameters": [ + { + "in": "path", + "name": "router", + "description": "Name or ID of the router", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcRouterUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcRouter" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "vpcs" + ], + "summary": "Delete router", + "operationId": "vpc_router_delete", + "parameters": [ + { + "in": "path", + "name": "router", + "description": "Name or ID of the router", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpc-subnets": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "List subnets", + "operationId": "vpc_subnet_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcSubnetResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "vpc" + ] + } + }, + "post": { + "tags": [ + "vpcs" + ], + "summary": "Create subnet", + "operationId": "vpc_subnet_create", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcSubnetCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcSubnet" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpc-subnets/{subnet}": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "Fetch subnet", + "operationId": "vpc_subnet_view", + "parameters": [ + { + "in": "path", + "name": "subnet", + "description": "Name or ID of the subnet", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcSubnet" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "vpcs" + ], + "summary": "Update subnet", + "operationId": "vpc_subnet_update", + "parameters": [ + { + "in": "path", + "name": "subnet", + "description": "Name or ID of the subnet", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcSubnetUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcSubnet" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "vpcs" + ], + "summary": "Delete subnet", + "operationId": "vpc_subnet_delete", + "parameters": [ + { + "in": "path", + "name": "subnet", + "description": "Name or ID of the subnet", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpc-subnets/{subnet}/network-interfaces": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "List network interfaces", + "operationId": "vpc_subnet_list_network_interfaces", + "parameters": [ + { + "in": "path", + "name": "subnet", + "description": "Name or ID of the subnet", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project, only required if `vpc` is provided as a `Name`", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "query", + "name": "vpc", + "description": "Name or ID of the VPC", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceNetworkInterfaceResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/vpcs": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "List VPCs", + "operationId": "vpc_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "project" + ] + } + }, + "post": { + "tags": [ + "vpcs" + ], + "summary": "Create VPC", + "operationId": "vpc_create", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Vpc" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpcs/{vpc}": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "Fetch VPC", + "operationId": "vpc_view", + "parameters": [ + { + "in": "path", + "name": "vpc", + "description": "Name or ID of the VPC", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Vpc" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "vpcs" + ], + "summary": "Update a VPC", + "operationId": "vpc_update", + "parameters": [ + { + "in": "path", + "name": "vpc", + "description": "Name or ID of the VPC", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Vpc" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "vpcs" + ], + "summary": "Delete VPC", + "operationId": "vpc_delete", + "parameters": [ + { + "in": "path", + "name": "vpc", + "description": "Name or ID of the VPC", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/webhook-receivers": { + "post": { + "tags": [ + "system/alerts" + ], + "summary": "Create webhook receiver", + "operationId": "webhook_receiver_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookReceiver" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/webhook-receivers/{receiver}": { + "put": { + "tags": [ + "system/alerts" + ], + "summary": "Update webhook receiver", + "description": "Note that receiver secrets are NOT added or removed using this endpoint. Instead, use the `/v1/webhooks/{secrets}/?receiver={receiver}` endpoint to add and remove secrets.", + "operationId": "webhook_receiver_update", + "parameters": [ + { + "in": "path", + "name": "receiver", + "description": "The name or ID of the webhook receiver.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookReceiverUpdate" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/webhook-secrets": { + "get": { + "tags": [ + "system/alerts" + ], + "summary": "List webhook receiver secret IDs", + "operationId": "webhook_secrets_list", + "parameters": [ + { + "in": "query", + "name": "receiver", + "description": "The name or ID of the webhook receiver.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookSecrets" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "tags": [ + "system/alerts" + ], + "summary": "Add secret to webhook receiver", + "operationId": "webhook_secrets_add", + "parameters": [ + { + "in": "query", + "name": "receiver", + "description": "The name or ID of the webhook receiver.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookSecretCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookSecret" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/webhook-secrets/{secret_id}": { + "delete": { + "tags": [ + "system/alerts" + ], + "summary": "Remove secret from webhook receiver", + "operationId": "webhook_secrets_delete", + "parameters": [ + { + "in": "path", + "name": "secret_id", + "description": "ID of the secret.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + } + }, + "components": { + "schemas": { + "Address": { + "description": "An address tied to an address lot.", + "type": "object", + "properties": { + "address": { + "description": "The address and prefix length of this address.", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + }, + "address_lot": { + "description": "The address lot this address is drawn from.", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + }, + "vlan_id": { + "nullable": true, + "description": "Optional VLAN ID for this address", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "address", + "address_lot" + ] + }, + "AddressConfig": { + "description": "A set of addresses associated with a port configuration.", + "type": "object", + "properties": { + "addresses": { + "description": "The set of addresses assigned to the port configuration.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Address" + } + }, + "link_name": { + "description": "Link to assign the addresses to. On ports that are not broken out, this is always phy0. On a 2x breakout the options are phy0 and phy1, on 4x phy0-phy3, etc.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + }, + "required": [ + "addresses", + "link_name" + ] + }, + "AddressLot": { + "description": "Represents an address lot object, containing the id of the lot that can be used in other API calls.", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "kind": { + "description": "Desired use of `AddressLot`", + "allOf": [ + { + "$ref": "#/components/schemas/AddressLotKind" + } + ] + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "kind", + "name", + "time_created", + "time_modified" + ] + }, + "AddressLotBlock": { + "description": "An address lot block is a part of an address lot and contains a range of addresses. The range is inclusive.", + "type": "object", + "properties": { + "first_address": { + "description": "The first address of the block (inclusive).", + "type": "string", + "format": "ip" + }, + "id": { + "description": "The id of the address lot block.", + "type": "string", + "format": "uuid" + }, + "last_address": { + "description": "The last address of the block (inclusive).", + "type": "string", + "format": "ip" + } + }, + "required": [ + "first_address", + "id", + "last_address" + ] + }, + "AddressLotBlockCreate": { + "description": "Parameters for creating an address lot block. Fist and last addresses are inclusive.", + "type": "object", + "properties": { + "first_address": { + "description": "The first address in the lot (inclusive).", + "type": "string", + "format": "ip" + }, + "last_address": { + "description": "The last address in the lot (inclusive).", + "type": "string", + "format": "ip" + } + }, + "required": [ + "first_address", + "last_address" + ] + }, + "AddressLotBlockResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/AddressLotBlock" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "AddressLotCreate": { + "description": "Parameters for creating an address lot.", + "type": "object", + "properties": { + "blocks": { + "description": "The blocks to add along with the new address lot.", + "type": "array", + "items": { + "$ref": "#/components/schemas/AddressLotBlockCreate" + } + }, + "description": { + "type": "string" + }, + "kind": { + "description": "The kind of address lot to create.", + "allOf": [ + { + "$ref": "#/components/schemas/AddressLotKind" + } + ] + }, + "name": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "blocks", + "description", + "kind", + "name" + ] + }, + "AddressLotCreateResponse": { + "description": "An address lot and associated blocks resulting from creating an address lot.", + "type": "object", + "properties": { + "blocks": { + "description": "The address lot blocks that were created.", + "type": "array", + "items": { + "$ref": "#/components/schemas/AddressLotBlock" + } + }, + "lot": { + "description": "The address lot that was created.", + "allOf": [ + { + "$ref": "#/components/schemas/AddressLot" + } + ] + } + }, + "required": [ + "blocks", + "lot" + ] + }, + "AddressLotKind": { + "description": "The kind associated with an address lot.", + "oneOf": [ + { + "description": "Infrastructure address lots are used for network infrastructure like addresses assigned to rack switches.", + "type": "string", + "enum": [ + "infra" + ] + }, + { + "description": "Pool address lots are used by IP pools.", + "type": "string", + "enum": [ + "pool" + ] + } + ] + }, + "AddressLotResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/AddressLot" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "AddressLotViewResponse": { + "description": "An address lot and associated blocks resulting from viewing an address lot.", + "type": "object", + "properties": { + "blocks": { + "description": "The address lot blocks.", + "type": "array", + "items": { + "$ref": "#/components/schemas/AddressLotBlock" + } + }, + "lot": { + "description": "The address lot.", + "allOf": [ + { + "$ref": "#/components/schemas/AddressLot" + } + ] + } + }, + "required": [ + "blocks", + "lot" + ] + }, + "AffinityGroup": { + "description": "View of an Affinity Group", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "failure_domain": { + "$ref": "#/components/schemas/FailureDomain" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "policy": { + "$ref": "#/components/schemas/AffinityPolicy" + }, + "project_id": { + "type": "string", + "format": "uuid" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "failure_domain", + "id", + "name", + "policy", + "project_id", + "time_created", + "time_modified" + ] + }, + "AffinityGroupCreate": { + "description": "Create-time parameters for an `AffinityGroup`", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "failure_domain": { + "$ref": "#/components/schemas/FailureDomain" + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "policy": { + "$ref": "#/components/schemas/AffinityPolicy" + } + }, + "required": [ + "description", + "failure_domain", + "name", + "policy" + ] + }, + "AffinityGroupMember": { + "description": "A member of an Affinity Group\n\nMembership in a group is not exclusive - members may belong to multiple affinity / anti-affinity groups.\n\nAffinity Groups can contain up to 32 members.", + "oneOf": [ + { + "description": "An instance belonging to this group\n\nInstances can belong to up to 16 affinity groups.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "instance" + ] + }, + "value": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "run_state": { + "$ref": "#/components/schemas/InstanceState" + } + }, + "required": [ + "id", + "name", + "run_state" + ] + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "AffinityGroupMemberResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/AffinityGroupMember" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "AffinityGroupResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/AffinityGroup" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "AffinityGroupUpdate": { + "description": "Updateable properties of an `AffinityGroup`", + "type": "object", + "properties": { + "description": { + "nullable": true, + "type": "string" + }, + "name": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + } + }, + "AffinityPolicy": { + "description": "Affinity policy used to describe \"what to do when a request cannot be satisfied\"\n\nUsed for both Affinity and Anti-Affinity Groups", + "oneOf": [ + { + "description": "If the affinity request cannot be satisfied, allow it anyway.\n\nThis enables a \"best-effort\" attempt to satisfy the affinity policy.", + "type": "string", + "enum": [ + "allow" + ] + }, + { + "description": "If the affinity request cannot be satisfied, fail explicitly.", + "type": "string", + "enum": [ + "fail" + ] + } + ] + }, + "AggregateBgpMessageHistory": { + "description": "BGP message history for rack switches.", + "type": "object", + "properties": { + "switch_histories": { + "description": "BGP history organized by switch.", + "type": "array", + "items": { + "$ref": "#/components/schemas/SwitchBgpHistory" + } + } + }, + "required": [ + "switch_histories" + ] + }, + "AlertClass": { + "description": "An alert class.", + "type": "object", + "properties": { + "description": { + "description": "A description of what this alert class represents.", + "type": "string" + }, + "name": { + "description": "The name of the alert class.", + "type": "string" + } + }, + "required": [ + "description", + "name" + ] + }, + "AlertClassResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/AlertClass" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "AlertDelivery": { + "description": "A delivery of a webhook event.", + "type": "object", + "properties": { + "alert_class": { + "description": "The event class.", + "type": "string" + }, + "alert_id": { + "description": "The UUID of the event.", + "type": "string", + "format": "uuid" + }, + "attempts": { + "description": "Individual attempts to deliver this webhook event, and their outcomes.", + "allOf": [ + { + "$ref": "#/components/schemas/AlertDeliveryAttempts" + } + ] + }, + "id": { + "description": "The UUID of this delivery attempt.", + "type": "string", + "format": "uuid" + }, + "receiver_id": { + "description": "The UUID of the alert receiver that this event was delivered to.", + "type": "string", + "format": "uuid" + }, + "state": { + "description": "The state of this delivery.", + "allOf": [ + { + "$ref": "#/components/schemas/AlertDeliveryState" + } + ] + }, + "time_started": { + "description": "The time at which this delivery began (i.e. the event was dispatched to the receiver).", + "type": "string", + "format": "date-time" + }, + "trigger": { + "description": "Why this delivery was performed.", + "allOf": [ + { + "$ref": "#/components/schemas/AlertDeliveryTrigger" + } + ] + } + }, + "required": [ + "alert_class", + "alert_id", + "attempts", + "id", + "receiver_id", + "state", + "time_started", + "trigger" + ] + }, + "AlertDeliveryAttempts": { + "description": "A list of attempts to deliver an alert to a receiver.\n\nThe type of the delivery attempt model depends on the receiver type, as it may contain information specific to that delivery mechanism. For example, webhook delivery attempts contain the HTTP status code of the webhook request.", + "oneOf": [ + { + "description": "A list of attempts to deliver an alert to a webhook receiver.", + "type": "object", + "properties": { + "webhook": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebhookDeliveryAttempt" + } + } + }, + "required": [ + "webhook" + ], + "additionalProperties": false + } + ] + }, + "AlertDeliveryId": { + "type": "object", + "properties": { + "delivery_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "delivery_id" + ] + }, + "AlertDeliveryResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/AlertDelivery" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "AlertDeliveryState": { + "description": "The state of a webhook delivery attempt.", + "oneOf": [ + { + "description": "The webhook event has not yet been delivered successfully.\n\nEither no delivery attempts have yet been performed, or the delivery has failed at least once but has retries remaining.", + "type": "string", + "enum": [ + "pending" + ] + }, + { + "description": "The webhook event has been delivered successfully.", + "type": "string", + "enum": [ + "delivered" + ] + }, + { + "description": "The webhook delivery attempt has failed permanently and will not be retried again.", + "type": "string", + "enum": [ + "failed" + ] + } + ] + }, + "AlertDeliveryTrigger": { + "description": "The reason an alert was delivered", + "oneOf": [ + { + "description": "Delivery was triggered by the alert itself.", + "type": "string", + "enum": [ + "alert" + ] + }, + { + "description": "Delivery was triggered by a request to resend the alert.", + "type": "string", + "enum": [ + "resend" + ] + }, + { + "description": "This delivery is a liveness probe.", + "type": "string", + "enum": [ + "probe" + ] + } + ] + }, + "AlertProbeResult": { + "description": "Data describing the result of an alert receiver liveness probe attempt.", + "type": "object", + "properties": { + "probe": { + "description": "The outcome of the probe delivery.", + "allOf": [ + { + "$ref": "#/components/schemas/AlertDelivery" + } + ] + }, + "resends_started": { + "nullable": true, + "description": "If the probe request succeeded, and resending failed deliveries on success was requested, the number of new delivery attempts started. Otherwise, if the probe did not succeed, or resending failed deliveries was not requested, this is null.\n\nNote that this may be 0, if there were no events found which had not been delivered successfully to this receiver.", + "type": "integer", + "format": "uint", + "minimum": 0 + } + }, + "required": [ + "probe" + ] + }, + "AlertReceiver": { + "description": "The configuration for an alert receiver.", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "kind": { + "description": "Configuration specific to the kind of alert receiver that this is.", + "allOf": [ + { + "$ref": "#/components/schemas/AlertReceiverKind" + } + ] + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "subscriptions": { + "description": "The list of alert classes to which this receiver is subscribed.", + "type": "array", + "items": { + "$ref": "#/components/schemas/AlertSubscription" + } + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "kind", + "name", + "subscriptions", + "time_created", + "time_modified" + ] + }, + "AlertReceiverKind": { + "description": "The possible alert delivery mechanisms for an alert receiver.", + "oneOf": [ + { + "description": "Webhook-specific alert receiver configuration.", + "type": "object", + "properties": { + "endpoint": { + "description": "The URL that webhook notification requests are sent to.", + "type": "string", + "format": "uri" + }, + "kind": { + "type": "string", + "enum": [ + "webhook" + ] + }, + "secrets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebhookSecret" + } + } + }, + "required": [ + "endpoint", + "kind", + "secrets" + ] + } + ] + }, + "AlertReceiverResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/AlertReceiver" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "AlertSubscription": { + "title": "A webhook event class subscription", + "description": "A webhook event class subscription matches either a single event class exactly, or a glob pattern including wildcards that may match multiple event classes", + "type": "string", + "pattern": "^([a-zA-Z0-9_]+|\\*|\\*\\*)(\\.([a-zA-Z0-9_]+|\\*|\\*\\*))*$" + }, + "AlertSubscriptionCreate": { + "type": "object", + "properties": { + "subscription": { + "description": "The event class pattern to subscribe to.", + "allOf": [ + { + "$ref": "#/components/schemas/AlertSubscription" + } + ] + } + }, + "required": [ + "subscription" + ] + }, + "AlertSubscriptionCreated": { + "type": "object", + "properties": { + "subscription": { + "description": "The new subscription added to the receiver.", + "allOf": [ + { + "$ref": "#/components/schemas/AlertSubscription" + } + ] + } + }, + "required": [ + "subscription" + ] + }, + "AllowList": { + "description": "Allowlist of IPs or subnets that can make requests to user-facing services.", + "type": "object", + "properties": { + "allowed_ips": { + "description": "The allowlist of IPs or subnets.", + "allOf": [ + { + "$ref": "#/components/schemas/AllowedSourceIps" + } + ] + }, + "time_created": { + "description": "Time the list was created.", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "Time the list was last modified.", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "allowed_ips", + "time_created", + "time_modified" + ] + }, + "AllowListUpdate": { + "description": "Parameters for updating allowed source IPs", + "type": "object", + "properties": { + "allowed_ips": { + "description": "The new list of allowed source IPs.", + "allOf": [ + { + "$ref": "#/components/schemas/AllowedSourceIps" + } + ] + } + }, + "required": [ + "allowed_ips" + ] + }, + "AllowedSourceIps": { + "description": "Description of source IPs allowed to reach rack services.", + "oneOf": [ + { + "description": "Allow traffic from any external IP address.", + "type": "object", + "properties": { + "allow": { + "type": "string", + "enum": [ + "any" + ] + } + }, + "required": [ + "allow" + ] + }, + { + "description": "Restrict access to a specific set of source IP addresses or subnets.\n\nAll others are prevented from reaching rack services.", + "type": "object", + "properties": { + "allow": { + "type": "string", + "enum": [ + "list" + ] + }, + "ips": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IpNet" + } + } + }, + "required": [ + "allow", + "ips" + ] + } + ] + }, + "AntiAffinityGroup": { + "description": "View of an Anti-Affinity Group", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "failure_domain": { + "$ref": "#/components/schemas/FailureDomain" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "policy": { + "$ref": "#/components/schemas/AffinityPolicy" + }, + "project_id": { + "type": "string", + "format": "uuid" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "failure_domain", + "id", + "name", + "policy", + "project_id", + "time_created", + "time_modified" + ] + }, + "AntiAffinityGroupCreate": { + "description": "Create-time parameters for an `AntiAffinityGroup`", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "failure_domain": { + "$ref": "#/components/schemas/FailureDomain" + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "policy": { + "$ref": "#/components/schemas/AffinityPolicy" + } + }, + "required": [ + "description", + "failure_domain", + "name", + "policy" + ] + }, + "AntiAffinityGroupMember": { + "description": "A member of an Anti-Affinity Group\n\nMembership in a group is not exclusive - members may belong to multiple affinity / anti-affinity groups.\n\nAnti-Affinity Groups can contain up to 32 members.", + "oneOf": [ + { + "description": "An instance belonging to this group\n\nInstances can belong to up to 16 anti-affinity groups.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "instance" + ] + }, + "value": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "run_state": { + "$ref": "#/components/schemas/InstanceState" + } + }, + "required": [ + "id", + "name", + "run_state" + ] + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "AntiAffinityGroupMemberResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/AntiAffinityGroupMember" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "AntiAffinityGroupResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/AntiAffinityGroup" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "AntiAffinityGroupUpdate": { + "description": "Updateable properties of an `AntiAffinityGroup`", + "type": "object", + "properties": { + "description": { + "nullable": true, + "type": "string" + }, + "name": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + } + }, + "AuditLogEntry": { + "description": "Audit log entry", + "type": "object", + "properties": { + "actor": { + "$ref": "#/components/schemas/AuditLogEntryActor" + }, + "auth_method": { + "nullable": true, + "description": "How the user authenticated the request. Possible values are \"session_cookie\" and \"access_token\". Optional because it will not be defined on unauthenticated requests like login attempts.", + "type": "string" + }, + "id": { + "description": "Unique identifier for the audit log entry", + "type": "string", + "format": "uuid" + }, + "operation_id": { + "description": "API endpoint ID, e.g., `project_create`", + "type": "string" + }, + "request_id": { + "description": "Request ID for tracing requests through the system", + "type": "string" + }, + "request_uri": { + "description": "URI of the request, truncated to 512 characters. Will only include host and scheme for HTTP/2 requests. For HTTP/1.1, the URI will consist of only the path and query.", + "type": "string" + }, + "result": { + "description": "Result of the operation", + "allOf": [ + { + "$ref": "#/components/schemas/AuditLogEntryResult" + } + ] + }, + "source_ip": { + "description": "IP address that made the request", + "type": "string", + "format": "ip" + }, + "time_completed": { + "description": "Time operation completed", + "type": "string", + "format": "date-time" + }, + "time_started": { + "description": "When the request was received", + "type": "string", + "format": "date-time" + }, + "user_agent": { + "nullable": true, + "description": "User agent string from the request, truncated to 256 characters.", + "type": "string" + } + }, + "required": [ + "actor", + "id", + "operation_id", + "request_id", + "request_uri", + "result", + "source_ip", + "time_completed", + "time_started" + ] + }, + "AuditLogEntryActor": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "user_builtin" + ] + }, + "user_builtin_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "kind", + "user_builtin_id" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "silo_user" + ] + }, + "silo_id": { + "type": "string", + "format": "uuid" + }, + "silo_user_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "kind", + "silo_id", + "silo_user_id" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "scim" + ] + }, + "silo_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "kind", + "silo_id" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "unauthenticated" + ] + } + }, + "required": [ + "kind" + ] + } + ] + }, + "AuditLogEntryResult": { + "description": "Result of an audit log entry", + "oneOf": [ + { + "description": "The operation completed successfully", + "type": "object", + "properties": { + "http_status_code": { + "description": "HTTP status code", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "kind": { + "type": "string", + "enum": [ + "success" + ] + } + }, + "required": [ + "http_status_code", + "kind" + ] + }, + { + "description": "The operation failed", + "type": "object", + "properties": { + "error_code": { + "nullable": true, + "type": "string" + }, + "error_message": { + "type": "string" + }, + "http_status_code": { + "description": "HTTP status code", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "kind": { + "type": "string", + "enum": [ + "error" + ] + } + }, + "required": [ + "error_message", + "http_status_code", + "kind" + ] + }, + { + "description": "After the logged operation completed, our attempt to write the result to the audit log failed, so it was automatically marked completed later by a background job. This does not imply that the operation itself timed out or failed, only our attempts to log its result.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "unknown" + ] + } + }, + "required": [ + "kind" + ] + } + ] + }, + "AuditLogEntryResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/AuditLogEntry" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "AuthzScope": { + "description": "Authorization scope for a timeseries.\n\nThis describes the level at which a user must be authorized to read data from a timeseries. For example, fleet-scoping means the data is only visible to an operator or fleet reader. Project-scoped, on the other hand, indicates that a user will see data limited to the projects on which they have read permissions.", + "oneOf": [ + { + "description": "Timeseries data is limited to fleet readers.", + "type": "string", + "enum": [ + "fleet" + ] + }, + { + "description": "Timeseries data is limited to the authorized silo for a user.", + "type": "string", + "enum": [ + "silo" + ] + }, + { + "description": "Timeseries data is limited to the authorized projects for a user.", + "type": "string", + "enum": [ + "project" + ] + }, + { + "description": "The timeseries is viewable to all without limitation.", + "type": "string", + "enum": [ + "viewable_to_all" + ] + } + ] + }, + "Baseboard": { + "description": "Properties that uniquely identify an Oxide hardware component", + "type": "object", + "properties": { + "part": { + "type": "string" + }, + "revision": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "serial": { + "type": "string" + } + }, + "required": [ + "part", + "revision", + "serial" + ] + }, + "BfdMode": { + "description": "BFD connection mode.", + "type": "string", + "enum": [ + "single_hop", + "multi_hop" + ] + }, + "BfdSessionDisable": { + "description": "Information needed to disable a BFD session", + "type": "object", + "properties": { + "remote": { + "description": "Address of the remote peer to disable a BFD session for.", + "type": "string", + "format": "ip" + }, + "switch": { + "description": "The switch to enable this session on. Must be `switch0` or `switch1`.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + }, + "required": [ + "remote", + "switch" + ] + }, + "BfdSessionEnable": { + "description": "Information about a bidirectional forwarding detection (BFD) session.", + "type": "object", + "properties": { + "detection_threshold": { + "description": "The negotiated Control packet transmission interval, multiplied by this variable, will be the Detection Time for this session (as seen by the remote system)", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "local": { + "nullable": true, + "description": "Address the Oxide switch will listen on for BFD traffic. If `None` then the unspecified address (0.0.0.0 or ::) is used.", + "type": "string", + "format": "ip" + }, + "mode": { + "description": "Select either single-hop (RFC 5881) or multi-hop (RFC 5883)", + "allOf": [ + { + "$ref": "#/components/schemas/BfdMode" + } + ] + }, + "remote": { + "description": "Address of the remote peer to establish a BFD session with.", + "type": "string", + "format": "ip" + }, + "required_rx": { + "description": "The minimum interval, in microseconds, between received BFD Control packets that this system requires", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "switch": { + "description": "The switch to enable this session on. Must be `switch0` or `switch1`.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + }, + "required": [ + "detection_threshold", + "mode", + "remote", + "required_rx", + "switch" + ] + }, + "BfdState": { + "oneOf": [ + { + "description": "A stable down state. Non-responsive to incoming messages.", + "type": "string", + "enum": [ + "admin_down" + ] + }, + { + "description": "The initial state.", + "type": "string", + "enum": [ + "down" + ] + }, + { + "description": "The peer has detected a remote peer in the down state.", + "type": "string", + "enum": [ + "init" + ] + }, + { + "description": "The peer has detected a remote peer in the up or init state while in the init state.", + "type": "string", + "enum": [ + "up" + ] + } + ] + }, + "BfdStatus": { + "type": "object", + "properties": { + "detection_threshold": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "local": { + "nullable": true, + "type": "string", + "format": "ip" + }, + "mode": { + "$ref": "#/components/schemas/BfdMode" + }, + "peer": { + "type": "string", + "format": "ip" + }, + "required_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "state": { + "$ref": "#/components/schemas/BfdState" + }, + "switch": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "detection_threshold", + "mode", + "peer", + "required_rx", + "state", + "switch" + ] + }, + "BgpAnnounceSet": { + "description": "Represents a BGP announce set by id. The id can be used with other API calls to view and manage the announce set.", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "name", + "time_created", + "time_modified" + ] + }, + "BgpAnnounceSetCreate": { + "description": "Parameters for creating a named set of BGP announcements.", + "type": "object", + "properties": { + "announcement": { + "description": "The announcements in this set.", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpAnnouncementCreate" + } + }, + "description": { + "type": "string" + }, + "name": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "announcement", + "description", + "name" + ] + }, + "BgpAnnouncement": { + "description": "A BGP announcement tied to an address lot block.", + "type": "object", + "properties": { + "address_lot_block_id": { + "description": "The address block the IP network being announced is drawn from.", + "type": "string", + "format": "uuid" + }, + "announce_set_id": { + "description": "The id of the set this announcement is a part of.", + "type": "string", + "format": "uuid" + }, + "network": { + "description": "The IP network being announced.", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + } + }, + "required": [ + "address_lot_block_id", + "announce_set_id", + "network" + ] + }, + "BgpAnnouncementCreate": { + "description": "A BGP announcement tied to a particular address lot block.", + "type": "object", + "properties": { + "address_lot_block": { + "description": "Address lot this announcement is drawn from.", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + }, + "network": { + "description": "The network being announced.", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + } + }, + "required": [ + "address_lot_block", + "network" + ] + }, + "BgpConfig": { + "description": "A base BGP configuration.", + "type": "object", + "properties": { + "asn": { + "description": "The autonomous system number of this BGP configuration.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + }, + "vrf": { + "nullable": true, + "description": "Optional virtual routing and forwarding identifier for this BGP configuration.", + "type": "string" + } + }, + "required": [ + "asn", + "description", + "id", + "name", + "time_created", + "time_modified" + ] + }, + "BgpConfigCreate": { + "description": "Parameters for creating a BGP configuration. This includes and autonomous system number (ASN) and a virtual routing and forwarding (VRF) identifier.", + "type": "object", + "properties": { + "asn": { + "description": "The autonomous system number of this BGP configuration.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "bgp_announce_set_id": { + "$ref": "#/components/schemas/NameOrId" + }, + "description": { + "type": "string" + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "vrf": { + "nullable": true, + "description": "Optional virtual routing and forwarding identifier for this BGP configuration.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + }, + "required": [ + "asn", + "bgp_announce_set_id", + "description", + "name" + ] + }, + "BgpConfigResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpConfig" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "BgpExported": { + "description": "The current status of a BGP peer.", + "type": "object", + "properties": { + "exports": { + "description": "Exported routes indexed by peer address.", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv4Net" + } + } + } + }, + "required": [ + "exports" + ] + }, + "BgpImportedRouteIpv4": { + "description": "A route imported from a BGP peer.", + "type": "object", + "properties": { + "id": { + "description": "BGP identifier of the originating router.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "nexthop": { + "description": "The nexthop the prefix is reachable through.", + "type": "string", + "format": "ipv4" + }, + "prefix": { + "description": "The destination network prefix.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4Net" + } + ] + }, + "switch": { + "description": "Switch the route is imported into.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchLocation" + } + ] + } + }, + "required": [ + "id", + "nexthop", + "prefix", + "switch" + ] + }, + "BgpMessageHistory": {}, + "BgpPeer": { + "description": "A BGP peer configuration for an interface. Includes the set of announcements that will be advertised to the peer identified by `addr`. The `bgp_config` parameter is a reference to global BGP parameters. The `interface_name` indicates what interface the peer should be contacted on.", + "type": "object", + "properties": { + "addr": { + "description": "The address of the host to peer with.", + "type": "string", + "format": "ip" + }, + "allowed_export": { + "description": "Define export policy for a peer.", + "allOf": [ + { + "$ref": "#/components/schemas/ImportExportPolicy" + } + ] + }, + "allowed_import": { + "description": "Define import policy for a peer.", + "allOf": [ + { + "$ref": "#/components/schemas/ImportExportPolicy" + } + ] + }, + "bgp_config": { + "description": "The global BGP configuration used for establishing a session with this peer.", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + }, + "communities": { + "description": "Include the provided communities in updates sent to the peer.", + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "connect_retry": { + "description": "How long to to wait between TCP connection retries (seconds).", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "delay_open": { + "description": "How long to delay sending an open request after establishing a TCP session (seconds).", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "enforce_first_as": { + "description": "Enforce that the first AS in paths received from this peer is the peer's AS.", + "type": "boolean" + }, + "hold_time": { + "description": "How long to hold peer connections between keepalives (seconds).", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "idle_hold_time": { + "description": "How long to hold a peer in idle before attempting a new session (seconds).", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "interface_name": { + "description": "The name of interface to peer on. This is relative to the port configuration this BGP peer configuration is a part of. For example this value could be phy0 to refer to a primary physical interface. Or it could be vlan47 to refer to a VLAN interface.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "keepalive": { + "description": "How often to send keepalive requests (seconds).", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "local_pref": { + "nullable": true, + "description": "Apply a local preference to routes received from this peer.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "md5_auth_key": { + "nullable": true, + "description": "Use the given key for TCP-MD5 authentication with the peer.", + "type": "string" + }, + "min_ttl": { + "nullable": true, + "description": "Require messages from a peer have a minimum IP time to live field.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "multi_exit_discriminator": { + "nullable": true, + "description": "Apply the provided multi-exit discriminator (MED) updates sent to the peer.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "remote_asn": { + "nullable": true, + "description": "Require that a peer has a specified ASN.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "vlan_id": { + "nullable": true, + "description": "Associate a VLAN ID with a peer.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "addr", + "allowed_export", + "allowed_import", + "bgp_config", + "communities", + "connect_retry", + "delay_open", + "enforce_first_as", + "hold_time", + "idle_hold_time", + "interface_name", + "keepalive" + ] + }, + "BgpPeerConfig": { + "type": "object", + "properties": { + "link_name": { + "description": "Link that the peer is reachable on. On ports that are not broken out, this is always phy0. On a 2x breakout the options are phy0 and phy1, on 4x phy0-phy3, etc.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "peers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpPeer" + } + } + }, + "required": [ + "link_name", + "peers" + ] + }, + "BgpPeerState": { + "description": "The current state of a BGP peer.", + "oneOf": [ + { + "description": "Initial state. Refuse all incoming BGP connections. No resources allocated to peer.", + "type": "string", + "enum": [ + "idle" + ] + }, + { + "description": "Waiting for the TCP connection to be completed.", + "type": "string", + "enum": [ + "connect" + ] + }, + { + "description": "Trying to acquire peer by listening for and accepting a TCP connection.", + "type": "string", + "enum": [ + "active" + ] + }, + { + "description": "Waiting for open message from peer.", + "type": "string", + "enum": [ + "open_sent" + ] + }, + { + "description": "Waiting for keepaliave or notification from peer.", + "type": "string", + "enum": [ + "open_confirm" + ] + }, + { + "description": "There is an ongoing Connection Collision that hasn't yet been resolved. Two connections are maintained until one connection receives an Open or is able to progress into Established.", + "type": "string", + "enum": [ + "connection_collision" + ] + }, + { + "description": "Synchronizing with peer.", + "type": "string", + "enum": [ + "session_setup" + ] + }, + { + "description": "Session established. Able to exchange update, notification and keepalive messages with peers.", + "type": "string", + "enum": [ + "established" + ] + } + ] + }, + "BgpPeerStatus": { + "description": "The current status of a BGP peer.", + "type": "object", + "properties": { + "addr": { + "description": "IP address of the peer.", + "type": "string", + "format": "ip" + }, + "local_asn": { + "description": "Local autonomous system number.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "remote_asn": { + "description": "Remote autonomous system number.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "state": { + "description": "State of the peer.", + "allOf": [ + { + "$ref": "#/components/schemas/BgpPeerState" + } + ] + }, + "state_duration_millis": { + "description": "Time of last state change.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "switch": { + "description": "Switch with the peer session.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchLocation" + } + ] + } + }, + "required": [ + "addr", + "local_asn", + "remote_asn", + "state", + "state_duration_millis", + "switch" + ] + }, + "BinRangedouble": { + "description": "A type storing a range over `T`.\n\nThis type supports ranges similar to the `RangeTo`, `Range` and `RangeFrom` types in the standard library. Those cover `(..end)`, `(start..end)`, and `(start..)` respectively.", + "oneOf": [ + { + "description": "A range unbounded below and exclusively above, `..end`.", + "type": "object", + "properties": { + "end": { + "type": "number", + "format": "double" + }, + "type": { + "type": "string", + "enum": [ + "range_to" + ] + } + }, + "required": [ + "end", + "type" + ] + }, + { + "description": "A range bounded inclusively below and exclusively above, `start..end`.", + "type": "object", + "properties": { + "end": { + "type": "number", + "format": "double" + }, + "start": { + "type": "number", + "format": "double" + }, + "type": { + "type": "string", + "enum": [ + "range" + ] + } + }, + "required": [ + "end", + "start", + "type" + ] + }, + { + "description": "A range bounded inclusively below and unbounded above, `start..`.", + "type": "object", + "properties": { + "start": { + "type": "number", + "format": "double" + }, + "type": { + "type": "string", + "enum": [ + "range_from" + ] + } + }, + "required": [ + "start", + "type" + ] + } + ] + }, + "BinRangefloat": { + "description": "A type storing a range over `T`.\n\nThis type supports ranges similar to the `RangeTo`, `Range` and `RangeFrom` types in the standard library. Those cover `(..end)`, `(start..end)`, and `(start..)` respectively.", + "oneOf": [ + { + "description": "A range unbounded below and exclusively above, `..end`.", + "type": "object", + "properties": { + "end": { + "type": "number", + "format": "float" + }, + "type": { + "type": "string", + "enum": [ + "range_to" + ] + } + }, + "required": [ + "end", + "type" + ] + }, + { + "description": "A range bounded inclusively below and exclusively above, `start..end`.", + "type": "object", + "properties": { + "end": { + "type": "number", + "format": "float" + }, + "start": { + "type": "number", + "format": "float" + }, + "type": { + "type": "string", + "enum": [ + "range" + ] + } + }, + "required": [ + "end", + "start", + "type" + ] + }, + { + "description": "A range bounded inclusively below and unbounded above, `start..`.", + "type": "object", + "properties": { + "start": { + "type": "number", + "format": "float" + }, + "type": { + "type": "string", + "enum": [ + "range_from" + ] + } + }, + "required": [ + "start", + "type" + ] + } + ] + }, + "BinRangeint16": { + "description": "A type storing a range over `T`.\n\nThis type supports ranges similar to the `RangeTo`, `Range` and `RangeFrom` types in the standard library. Those cover `(..end)`, `(start..end)`, and `(start..)` respectively.", + "oneOf": [ + { + "description": "A range unbounded below and exclusively above, `..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "int16" + }, + "type": { + "type": "string", + "enum": [ + "range_to" + ] + } + }, + "required": [ + "end", + "type" + ] + }, + { + "description": "A range bounded inclusively below and exclusively above, `start..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "int16" + }, + "start": { + "type": "integer", + "format": "int16" + }, + "type": { + "type": "string", + "enum": [ + "range" + ] + } + }, + "required": [ + "end", + "start", + "type" + ] + }, + { + "description": "A range bounded inclusively below and unbounded above, `start..`.", + "type": "object", + "properties": { + "start": { + "type": "integer", + "format": "int16" + }, + "type": { + "type": "string", + "enum": [ + "range_from" + ] + } + }, + "required": [ + "start", + "type" + ] + } + ] + }, + "BinRangeint32": { + "description": "A type storing a range over `T`.\n\nThis type supports ranges similar to the `RangeTo`, `Range` and `RangeFrom` types in the standard library. Those cover `(..end)`, `(start..end)`, and `(start..)` respectively.", + "oneOf": [ + { + "description": "A range unbounded below and exclusively above, `..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string", + "enum": [ + "range_to" + ] + } + }, + "required": [ + "end", + "type" + ] + }, + { + "description": "A range bounded inclusively below and exclusively above, `start..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "int32" + }, + "start": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string", + "enum": [ + "range" + ] + } + }, + "required": [ + "end", + "start", + "type" + ] + }, + { + "description": "A range bounded inclusively below and unbounded above, `start..`.", + "type": "object", + "properties": { + "start": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string", + "enum": [ + "range_from" + ] + } + }, + "required": [ + "start", + "type" + ] + } + ] + }, + "BinRangeint64": { + "description": "A type storing a range over `T`.\n\nThis type supports ranges similar to the `RangeTo`, `Range` and `RangeFrom` types in the standard library. Those cover `(..end)`, `(start..end)`, and `(start..)` respectively.", + "oneOf": [ + { + "description": "A range unbounded below and exclusively above, `..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "int64" + }, + "type": { + "type": "string", + "enum": [ + "range_to" + ] + } + }, + "required": [ + "end", + "type" + ] + }, + { + "description": "A range bounded inclusively below and exclusively above, `start..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "int64" + }, + "start": { + "type": "integer", + "format": "int64" + }, + "type": { + "type": "string", + "enum": [ + "range" + ] + } + }, + "required": [ + "end", + "start", + "type" + ] + }, + { + "description": "A range bounded inclusively below and unbounded above, `start..`.", + "type": "object", + "properties": { + "start": { + "type": "integer", + "format": "int64" + }, + "type": { + "type": "string", + "enum": [ + "range_from" + ] + } + }, + "required": [ + "start", + "type" + ] + } + ] + }, + "BinRangeint8": { + "description": "A type storing a range over `T`.\n\nThis type supports ranges similar to the `RangeTo`, `Range` and `RangeFrom` types in the standard library. Those cover `(..end)`, `(start..end)`, and `(start..)` respectively.", + "oneOf": [ + { + "description": "A range unbounded below and exclusively above, `..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "int8" + }, + "type": { + "type": "string", + "enum": [ + "range_to" + ] + } + }, + "required": [ + "end", + "type" + ] + }, + { + "description": "A range bounded inclusively below and exclusively above, `start..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "int8" + }, + "start": { + "type": "integer", + "format": "int8" + }, + "type": { + "type": "string", + "enum": [ + "range" + ] + } + }, + "required": [ + "end", + "start", + "type" + ] + }, + { + "description": "A range bounded inclusively below and unbounded above, `start..`.", + "type": "object", + "properties": { + "start": { + "type": "integer", + "format": "int8" + }, + "type": { + "type": "string", + "enum": [ + "range_from" + ] + } + }, + "required": [ + "start", + "type" + ] + } + ] + }, + "BinRangeuint16": { + "description": "A type storing a range over `T`.\n\nThis type supports ranges similar to the `RangeTo`, `Range` and `RangeFrom` types in the standard library. Those cover `(..end)`, `(start..end)`, and `(start..)` respectively.", + "oneOf": [ + { + "description": "A range unbounded below and exclusively above, `..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "range_to" + ] + } + }, + "required": [ + "end", + "type" + ] + }, + { + "description": "A range bounded inclusively below and exclusively above, `start..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "start": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "range" + ] + } + }, + "required": [ + "end", + "start", + "type" + ] + }, + { + "description": "A range bounded inclusively below and unbounded above, `start..`.", + "type": "object", + "properties": { + "start": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "range_from" + ] + } + }, + "required": [ + "start", + "type" + ] + } + ] + }, + "BinRangeuint32": { + "description": "A type storing a range over `T`.\n\nThis type supports ranges similar to the `RangeTo`, `Range` and `RangeFrom` types in the standard library. Those cover `(..end)`, `(start..end)`, and `(start..)` respectively.", + "oneOf": [ + { + "description": "A range unbounded below and exclusively above, `..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "range_to" + ] + } + }, + "required": [ + "end", + "type" + ] + }, + { + "description": "A range bounded inclusively below and exclusively above, `start..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "start": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "range" + ] + } + }, + "required": [ + "end", + "start", + "type" + ] + }, + { + "description": "A range bounded inclusively below and unbounded above, `start..`.", + "type": "object", + "properties": { + "start": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "range_from" + ] + } + }, + "required": [ + "start", + "type" + ] + } + ] + }, + "BinRangeuint64": { + "description": "A type storing a range over `T`.\n\nThis type supports ranges similar to the `RangeTo`, `Range` and `RangeFrom` types in the standard library. Those cover `(..end)`, `(start..end)`, and `(start..)` respectively.", + "oneOf": [ + { + "description": "A range unbounded below and exclusively above, `..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "range_to" + ] + } + }, + "required": [ + "end", + "type" + ] + }, + { + "description": "A range bounded inclusively below and exclusively above, `start..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "start": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "range" + ] + } + }, + "required": [ + "end", + "start", + "type" + ] + }, + { + "description": "A range bounded inclusively below and unbounded above, `start..`.", + "type": "object", + "properties": { + "start": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "range_from" + ] + } + }, + "required": [ + "start", + "type" + ] + } + ] + }, + "BinRangeuint8": { + "description": "A type storing a range over `T`.\n\nThis type supports ranges similar to the `RangeTo`, `Range` and `RangeFrom` types in the standard library. Those cover `(..end)`, `(start..end)`, and `(start..)` respectively.", + "oneOf": [ + { + "description": "A range unbounded below and exclusively above, `..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "range_to" + ] + } + }, + "required": [ + "end", + "type" + ] + }, + { + "description": "A range bounded inclusively below and exclusively above, `start..end`.", + "type": "object", + "properties": { + "end": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "start": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "range" + ] + } + }, + "required": [ + "end", + "start", + "type" + ] + }, + { + "description": "A range bounded inclusively below and unbounded above, `start..`.", + "type": "object", + "properties": { + "start": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "range_from" + ] + } + }, + "required": [ + "start", + "type" + ] + } + ] + }, + "Bindouble": { + "description": "Type storing bin edges and a count of samples within it.", + "type": "object", + "properties": { + "count": { + "description": "The total count of samples in this bin.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "range": { + "description": "The range of the support covered by this bin.", + "allOf": [ + { + "$ref": "#/components/schemas/BinRangedouble" + } + ] + } + }, + "required": [ + "count", + "range" + ] + }, + "Binfloat": { + "description": "Type storing bin edges and a count of samples within it.", + "type": "object", + "properties": { + "count": { + "description": "The total count of samples in this bin.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "range": { + "description": "The range of the support covered by this bin.", + "allOf": [ + { + "$ref": "#/components/schemas/BinRangefloat" + } + ] + } + }, + "required": [ + "count", + "range" + ] + }, + "Binint16": { + "description": "Type storing bin edges and a count of samples within it.", + "type": "object", + "properties": { + "count": { + "description": "The total count of samples in this bin.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "range": { + "description": "The range of the support covered by this bin.", + "allOf": [ + { + "$ref": "#/components/schemas/BinRangeint16" + } + ] + } + }, + "required": [ + "count", + "range" + ] + }, + "Binint32": { + "description": "Type storing bin edges and a count of samples within it.", + "type": "object", + "properties": { + "count": { + "description": "The total count of samples in this bin.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "range": { + "description": "The range of the support covered by this bin.", + "allOf": [ + { + "$ref": "#/components/schemas/BinRangeint32" + } + ] + } + }, + "required": [ + "count", + "range" + ] + }, + "Binint64": { + "description": "Type storing bin edges and a count of samples within it.", + "type": "object", + "properties": { + "count": { + "description": "The total count of samples in this bin.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "range": { + "description": "The range of the support covered by this bin.", + "allOf": [ + { + "$ref": "#/components/schemas/BinRangeint64" + } + ] + } + }, + "required": [ + "count", + "range" + ] + }, + "Binint8": { + "description": "Type storing bin edges and a count of samples within it.", + "type": "object", + "properties": { + "count": { + "description": "The total count of samples in this bin.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "range": { + "description": "The range of the support covered by this bin.", + "allOf": [ + { + "$ref": "#/components/schemas/BinRangeint8" + } + ] + } + }, + "required": [ + "count", + "range" + ] + }, + "Binuint16": { + "description": "Type storing bin edges and a count of samples within it.", + "type": "object", + "properties": { + "count": { + "description": "The total count of samples in this bin.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "range": { + "description": "The range of the support covered by this bin.", + "allOf": [ + { + "$ref": "#/components/schemas/BinRangeuint16" + } + ] + } + }, + "required": [ + "count", + "range" + ] + }, + "Binuint32": { + "description": "Type storing bin edges and a count of samples within it.", + "type": "object", + "properties": { + "count": { + "description": "The total count of samples in this bin.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "range": { + "description": "The range of the support covered by this bin.", + "allOf": [ + { + "$ref": "#/components/schemas/BinRangeuint32" + } + ] + } + }, + "required": [ + "count", + "range" + ] + }, + "Binuint64": { + "description": "Type storing bin edges and a count of samples within it.", + "type": "object", + "properties": { + "count": { + "description": "The total count of samples in this bin.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "range": { + "description": "The range of the support covered by this bin.", + "allOf": [ + { + "$ref": "#/components/schemas/BinRangeuint64" + } + ] + } + }, + "required": [ + "count", + "range" + ] + }, + "Binuint8": { + "description": "Type storing bin edges and a count of samples within it.", + "type": "object", + "properties": { + "count": { + "description": "The total count of samples in this bin.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "range": { + "description": "The range of the support covered by this bin.", + "allOf": [ + { + "$ref": "#/components/schemas/BinRangeuint8" + } + ] + } + }, + "required": [ + "count", + "range" + ] + }, + "BlockSize": { + "title": "disk block size in bytes", + "type": "integer", + "enum": [ + 512, + 2048, + 4096 + ] + }, + "ByteCount": { + "description": "Byte count to express memory or storage capacity.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "Certificate": { + "description": "View of a Certificate", + "type": "object", + "properties": { + "cert": { + "description": "PEM-formatted string containing public certificate chain", + "type": "string" + }, + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "service": { + "description": "The service using this certificate", + "allOf": [ + { + "$ref": "#/components/schemas/ServiceUsingCertificate" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "cert", + "description", + "id", + "name", + "service", + "time_created", + "time_modified" + ] + }, + "CertificateCreate": { + "description": "Create-time parameters for a `Certificate`", + "type": "object", + "properties": { + "cert": { + "description": "PEM-formatted string containing public certificate chain", + "type": "string" + }, + "description": { + "type": "string" + }, + "key": { + "description": "PEM-formatted string containing private key", + "type": "string" + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "service": { + "description": "The service using this certificate", + "allOf": [ + { + "$ref": "#/components/schemas/ServiceUsingCertificate" + } + ] + } + }, + "required": [ + "cert", + "description", + "key", + "name", + "service" + ] + }, + "CertificateResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Certificate" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "ConsoleSession": { + "description": "View of a console session", + "type": "object", + "properties": { + "id": { + "description": "A unique, immutable, system-controlled identifier for the session", + "type": "string", + "format": "uuid" + }, + "time_created": { + "type": "string", + "format": "date-time" + }, + "time_last_used": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "time_created", + "time_last_used" + ] + }, + "ConsoleSessionResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/ConsoleSession" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "Cumulativedouble": { + "description": "A cumulative or counter data type.", + "type": "object", + "properties": { + "start_time": { + "type": "string", + "format": "date-time" + }, + "value": { + "type": "number", + "format": "double" + } + }, + "required": [ + "start_time", + "value" + ] + }, + "Cumulativefloat": { + "description": "A cumulative or counter data type.", + "type": "object", + "properties": { + "start_time": { + "type": "string", + "format": "date-time" + }, + "value": { + "type": "number", + "format": "float" + } + }, + "required": [ + "start_time", + "value" + ] + }, + "Cumulativeint64": { + "description": "A cumulative or counter data type.", + "type": "object", + "properties": { + "start_time": { + "type": "string", + "format": "date-time" + }, + "value": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "start_time", + "value" + ] + }, + "Cumulativeuint64": { + "description": "A cumulative or counter data type.", + "type": "object", + "properties": { + "start_time": { + "type": "string", + "format": "date-time" + }, + "value": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "start_time", + "value" + ] + }, + "CurrentUser": { + "description": "Info about the current user", + "type": "object", + "properties": { + "display_name": { + "description": "Human-readable name that can identify the user", + "type": "string" + }, + "fleet_viewer": { + "description": "Whether this user has the viewer role on the fleet. Used by the web console to determine whether to show system-level UI.", + "type": "boolean" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "silo_admin": { + "description": "Whether this user has the admin role on their silo. Used by the web console to determine whether to show admin-only UI elements.", + "type": "boolean" + }, + "silo_id": { + "description": "Uuid of the silo to which this user belongs", + "type": "string", + "format": "uuid" + }, + "silo_name": { + "description": "Name of the silo to which this user belongs.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + }, + "required": [ + "display_name", + "fleet_viewer", + "id", + "silo_admin", + "silo_id", + "silo_name" + ] + }, + "Datum": { + "description": "A `Datum` is a single sampled data point from a metric.", + "oneOf": [ + { + "type": "object", + "properties": { + "datum": { + "type": "boolean" + }, + "type": { + "type": "string", + "enum": [ + "bool" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "type": "integer", + "format": "int8" + }, + "type": { + "type": "string", + "enum": [ + "i8" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "u8" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "type": "integer", + "format": "int16" + }, + "type": { + "type": "string", + "enum": [ + "i16" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "u16" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string", + "enum": [ + "i32" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "u32" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "type": "integer", + "format": "int64" + }, + "type": { + "type": "string", + "enum": [ + "i64" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "u64" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "type": "number", + "format": "float" + }, + "type": { + "type": "string", + "enum": [ + "f32" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "type": "number", + "format": "double" + }, + "type": { + "type": "string", + "enum": [ + "f64" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "string" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "type": { + "type": "string", + "enum": [ + "bytes" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Cumulativeint64" + }, + "type": { + "type": "string", + "enum": [ + "cumulative_i64" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Cumulativeuint64" + }, + "type": { + "type": "string", + "enum": [ + "cumulative_u64" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Cumulativefloat" + }, + "type": { + "type": "string", + "enum": [ + "cumulative_f32" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Cumulativedouble" + }, + "type": { + "type": "string", + "enum": [ + "cumulative_f64" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Histogramint8" + }, + "type": { + "type": "string", + "enum": [ + "histogram_i8" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Histogramuint8" + }, + "type": { + "type": "string", + "enum": [ + "histogram_u8" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Histogramint16" + }, + "type": { + "type": "string", + "enum": [ + "histogram_i16" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Histogramuint16" + }, + "type": { + "type": "string", + "enum": [ + "histogram_u16" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Histogramint32" + }, + "type": { + "type": "string", + "enum": [ + "histogram_i32" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Histogramuint32" + }, + "type": { + "type": "string", + "enum": [ + "histogram_u32" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Histogramint64" + }, + "type": { + "type": "string", + "enum": [ + "histogram_i64" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Histogramuint64" + }, + "type": { + "type": "string", + "enum": [ + "histogram_u64" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Histogramfloat" + }, + "type": { + "type": "string", + "enum": [ + "histogram_f32" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Histogramdouble" + }, + "type": { + "type": "string", + "enum": [ + "histogram_f64" + ] + } + }, + "required": [ + "datum", + "type" + ] + }, + { + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/MissingDatum" + }, + "type": { + "type": "string", + "enum": [ + "missing" + ] + } + }, + "required": [ + "datum", + "type" + ] + } + ] + }, + "DatumType": { + "description": "The type of an individual datum of a metric.", + "type": "string", + "enum": [ + "bool", + "i8", + "u8", + "i16", + "u16", + "i32", + "u32", + "i64", + "u64", + "f32", + "f64", + "string", + "bytes", + "cumulative_i64", + "cumulative_u64", + "cumulative_f32", + "cumulative_f64", + "histogram_i8", + "histogram_u8", + "histogram_i16", + "histogram_u16", + "histogram_i32", + "histogram_u32", + "histogram_i64", + "histogram_u64", + "histogram_f32", + "histogram_f64" + ] + }, + "DerEncodedKeyPair": { + "type": "object", + "properties": { + "private_key": { + "description": "request signing RSA private key in PKCS#1 format (base64 encoded der file)", + "type": "string" + }, + "public_cert": { + "description": "request signing public certificate (base64 encoded der file)", + "type": "string" + } + }, + "required": [ + "private_key", + "public_cert" + ] + }, + "DeviceAccessToken": { + "description": "View of a device access token", + "type": "object", + "properties": { + "id": { + "description": "A unique, immutable, system-controlled identifier for the token. Note that this ID is not the bearer token itself, which starts with \"oxide-token-\"", + "type": "string", + "format": "uuid" + }, + "time_created": { + "type": "string", + "format": "date-time" + }, + "time_expires": { + "nullable": true, + "description": "Expiration timestamp. A null value means the token does not automatically expire.", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "time_created" + ] + }, + "DeviceAccessTokenRequest": { + "type": "object", + "properties": { + "client_id": { + "type": "string", + "format": "uuid" + }, + "device_code": { + "type": "string" + }, + "grant_type": { + "type": "string" + } + }, + "required": [ + "client_id", + "device_code", + "grant_type" + ] + }, + "DeviceAccessTokenResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/DeviceAccessToken" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "DeviceAuthRequest": { + "type": "object", + "properties": { + "client_id": { + "type": "string", + "format": "uuid" + }, + "ttl_seconds": { + "nullable": true, + "description": "Optional lifetime for the access token in seconds.\n\nThis value will be validated during the confirmation step. If not specified, it defaults to the silo's max TTL, which can be seen at `/v1/auth-settings`. If specified, must not exceed the silo's max TTL.\n\nSome special logic applies when authenticating the confirmation request with an existing device token: the requested TTL must not produce an expiration time later than the authenticating token's expiration. If no TTL is specified, the expiration will be the lesser of the silo max and the authenticating token's expiration time. To get the longest allowed lifetime, omit the TTL and authenticate with a web console session.", + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + "required": [ + "client_id" + ] + }, + "DeviceAuthVerify": { + "type": "object", + "properties": { + "user_code": { + "type": "string" + } + }, + "required": [ + "user_code" + ] + }, + "Digest": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "sha256" + ] + }, + "value": { + "type": "string" + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "Disk": { + "description": "View of a Disk", + "type": "object", + "properties": { + "block_size": { + "$ref": "#/components/schemas/ByteCount" + }, + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "device_path": { + "type": "string" + }, + "disk_type": { + "$ref": "#/components/schemas/DiskType" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "image_id": { + "nullable": true, + "description": "ID of image from which disk was created, if any", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "project_id": { + "type": "string", + "format": "uuid" + }, + "size": { + "$ref": "#/components/schemas/ByteCount" + }, + "snapshot_id": { + "nullable": true, + "description": "ID of snapshot from which disk was created, if any", + "type": "string", + "format": "uuid" + }, + "state": { + "$ref": "#/components/schemas/DiskState" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "block_size", + "description", + "device_path", + "disk_type", + "id", + "name", + "project_id", + "size", + "state", + "time_created", + "time_modified" + ] + }, + "DiskBackend": { + "description": "The source of a `Disk`'s blocks", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "local" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "disk_source": { + "description": "The initial source for this disk", + "allOf": [ + { + "$ref": "#/components/schemas/DiskSource" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "distributed" + ] + } + }, + "required": [ + "disk_source", + "type" + ] + } + ] + }, + "DiskCreate": { + "description": "Create-time parameters for a `Disk`", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "disk_backend": { + "description": "The source for this `Disk`'s blocks", + "allOf": [ + { + "$ref": "#/components/schemas/DiskBackend" + } + ] + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "size": { + "description": "The total size of the Disk (in bytes)", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + } + }, + "required": [ + "description", + "disk_backend", + "name", + "size" + ] + }, + "DiskPath": { + "type": "object", + "properties": { + "disk": { + "description": "Name or ID of the disk", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + } + }, + "required": [ + "disk" + ] + }, + "DiskResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Disk" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "DiskSource": { + "description": "Different sources for a Distributed Disk", + "oneOf": [ + { + "description": "Create a blank disk", + "type": "object", + "properties": { + "block_size": { + "description": "size of blocks for this Disk. valid values are: 512, 2048, or 4096", + "allOf": [ + { + "$ref": "#/components/schemas/BlockSize" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "blank" + ] + } + }, + "required": [ + "block_size", + "type" + ] + }, + { + "description": "Create a disk from a disk snapshot", + "type": "object", + "properties": { + "snapshot_id": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "enum": [ + "snapshot" + ] + } + }, + "required": [ + "snapshot_id", + "type" + ] + }, + { + "description": "Create a disk from an image", + "type": "object", + "properties": { + "image_id": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "enum": [ + "image" + ] + } + }, + "required": [ + "image_id", + "type" + ] + }, + { + "description": "Create a blank disk that will accept bulk writes or pull blocks from an external source.", + "type": "object", + "properties": { + "block_size": { + "$ref": "#/components/schemas/BlockSize" + }, + "type": { + "type": "string", + "enum": [ + "importing_blocks" + ] + } + }, + "required": [ + "block_size", + "type" + ] + } + ] + }, + "DiskState": { + "description": "State of a Disk", + "oneOf": [ + { + "description": "Disk is being initialized", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "creating" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is ready but detached from any Instance", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "detached" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is ready to receive blocks from an external source", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "import_ready" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is importing blocks from a URL", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "importing_from_url" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is importing blocks from bulk writes", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "importing_from_bulk_writes" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is being finalized to state Detached", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "finalizing" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is undergoing maintenance", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "maintenance" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is being attached to the given Instance", + "type": "object", + "properties": { + "instance": { + "type": "string", + "format": "uuid" + }, + "state": { + "type": "string", + "enum": [ + "attaching" + ] + } + }, + "required": [ + "instance", + "state" + ] + }, + { + "description": "Disk is attached to the given Instance", + "type": "object", + "properties": { + "instance": { + "type": "string", + "format": "uuid" + }, + "state": { + "type": "string", + "enum": [ + "attached" + ] + } + }, + "required": [ + "instance", + "state" + ] + }, + { + "description": "Disk is being detached from the given Instance", + "type": "object", + "properties": { + "instance": { + "type": "string", + "format": "uuid" + }, + "state": { + "type": "string", + "enum": [ + "detaching" + ] + } + }, + "required": [ + "instance", + "state" + ] + }, + { + "description": "Disk has been destroyed", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "destroyed" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is unavailable", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "faulted" + ] + } + }, + "required": [ + "state" + ] + } + ] + }, + "DiskType": { + "type": "string", + "enum": [ + "distributed", + "local" + ] + }, + "Distributiondouble": { + "description": "A distribution is a sequence of bins and counts in those bins, and some statistical information tracked to compute the mean, standard deviation, and quantile estimates.\n\nMin, max, and the p-* quantiles are treated as optional due to the possibility of distribution operations, like subtraction.", + "type": "object", + "properties": { + "bins": { + "type": "array", + "items": { + "type": "number", + "format": "double" + } + }, + "counts": { + "type": "array", + "items": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "max": { + "nullable": true, + "type": "number", + "format": "double" + }, + "min": { + "nullable": true, + "type": "number", + "format": "double" + }, + "p50": { + "nullable": true, + "type": "number", + "format": "double" + }, + "p90": { + "nullable": true, + "type": "number", + "format": "double" + }, + "p99": { + "nullable": true, + "type": "number", + "format": "double" + }, + "squared_mean": { + "type": "number", + "format": "double" + }, + "sum_of_samples": { + "type": "number", + "format": "double" + } + }, + "required": [ + "bins", + "counts", + "squared_mean", + "sum_of_samples" + ] + }, + "Distributionint64": { + "description": "A distribution is a sequence of bins and counts in those bins, and some statistical information tracked to compute the mean, standard deviation, and quantile estimates.\n\nMin, max, and the p-* quantiles are treated as optional due to the possibility of distribution operations, like subtraction.", + "type": "object", + "properties": { + "bins": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "counts": { + "type": "array", + "items": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "max": { + "nullable": true, + "type": "integer", + "format": "int64" + }, + "min": { + "nullable": true, + "type": "integer", + "format": "int64" + }, + "p50": { + "nullable": true, + "type": "number", + "format": "double" + }, + "p90": { + "nullable": true, + "type": "number", + "format": "double" + }, + "p99": { + "nullable": true, + "type": "number", + "format": "double" + }, + "squared_mean": { + "type": "number", + "format": "double" + }, + "sum_of_samples": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "bins", + "counts", + "squared_mean", + "sum_of_samples" + ] + }, + "EphemeralIpCreate": { + "description": "Parameters for creating an ephemeral IP address for an instance.", + "type": "object", + "properties": { + "pool": { + "nullable": true, + "description": "Name or ID of the IP pool used to allocate an address. If unspecified, the default IP pool will be used.", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + } + } + }, + "Error": { + "description": "Error information from a response.", + "type": "object", + "properties": { + "error_code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "request_id": { + "type": "string" + } + }, + "required": [ + "message", + "request_id" + ] + }, + "ExternalIp": { + "oneOf": [ + { + "description": "A source NAT IP address.\n\nSNAT addresses are ephemeral addresses used only for outbound connectivity.", + "type": "object", + "properties": { + "first_port": { + "description": "The first usable port within the IP address.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "ip": { + "description": "The IP address.", + "type": "string", + "format": "ip" + }, + "ip_pool_id": { + "description": "ID of the IP Pool from which the address is taken.", + "type": "string", + "format": "uuid" + }, + "kind": { + "type": "string", + "enum": [ + "snat" + ] + }, + "last_port": { + "description": "The last usable port within the IP address.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "first_port", + "ip", + "ip_pool_id", + "kind", + "last_port" + ] + }, + { + "type": "object", + "properties": { + "ip": { + "type": "string", + "format": "ip" + }, + "ip_pool_id": { + "type": "string", + "format": "uuid" + }, + "kind": { + "type": "string", + "enum": [ + "ephemeral" + ] + } + }, + "required": [ + "ip", + "ip_pool_id", + "kind" + ] + }, + { + "description": "A Floating IP is a well-known IP address which can be attached and detached from instances.", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "instance_id": { + "nullable": true, + "description": "The ID of the instance that this Floating IP is attached to, if it is presently in use.", + "type": "string", + "format": "uuid" + }, + "ip": { + "description": "The IP address held by this resource.", + "type": "string", + "format": "ip" + }, + "ip_pool_id": { + "description": "The ID of the IP pool this resource belongs to.", + "type": "string", + "format": "uuid" + }, + "kind": { + "type": "string", + "enum": [ + "floating" + ] + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "project_id": { + "description": "The project this resource exists within.", + "type": "string", + "format": "uuid" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "ip", + "ip_pool_id", + "kind", + "name", + "project_id", + "time_created", + "time_modified" + ] + } + ] + }, + "ExternalIpCreate": { + "description": "Parameters for creating an external IP address for instances.", + "oneOf": [ + { + "description": "An IP address providing both inbound and outbound access. The address is automatically assigned from the provided IP pool or the default IP pool if not specified.", + "type": "object", + "properties": { + "pool": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "ephemeral" + ] + } + }, + "required": [ + "type" + ] + }, + { + "description": "An IP address providing both inbound and outbound access. The address is an existing floating IP object assigned to the current project.\n\nThe floating IP must not be in use by another instance or service.", + "type": "object", + "properties": { + "floating_ip": { + "$ref": "#/components/schemas/NameOrId" + }, + "type": { + "type": "string", + "enum": [ + "floating" + ] + } + }, + "required": [ + "floating_ip", + "type" + ] + } + ] + }, + "ExternalIpResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/ExternalIp" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "FailureDomain": { + "description": "Describes the scope of affinity for the purposes of co-location.", + "oneOf": [ + { + "description": "Instances are considered co-located if they are on the same sled", + "type": "string", + "enum": [ + "sled" + ] + } + ] + }, + "FieldSchema": { + "description": "The name and type information for a field of a timeseries schema.", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "field_type": { + "$ref": "#/components/schemas/FieldType" + }, + "name": { + "type": "string" + }, + "source": { + "$ref": "#/components/schemas/FieldSource" + } + }, + "required": [ + "description", + "field_type", + "name", + "source" + ] + }, + "FieldSource": { + "description": "The source from which a field is derived, the target or metric.", + "type": "string", + "enum": [ + "target", + "metric" + ] + }, + "FieldType": { + "description": "The `FieldType` identifies the data type of a target or metric field.", + "type": "string", + "enum": [ + "string", + "i8", + "u8", + "i16", + "u16", + "i32", + "u32", + "i64", + "u64", + "ip_addr", + "uuid", + "bool" + ] + }, + "FieldValue": { + "description": "The `FieldValue` contains the value of a target or metric field.", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "string" + ] + }, + "value": { + "type": "string" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "i8" + ] + }, + "value": { + "type": "integer", + "format": "int8" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "u8" + ] + }, + "value": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "i16" + ] + }, + "value": { + "type": "integer", + "format": "int16" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "u16" + ] + }, + "value": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "i32" + ] + }, + "value": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "u32" + ] + }, + "value": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "i64" + ] + }, + "value": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "u64" + ] + }, + "value": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ip_addr" + ] + }, + "value": { + "type": "string", + "format": "ip" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "uuid" + ] + }, + "value": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "bool" + ] + }, + "value": { + "type": "boolean" + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "FinalizeDisk": { + "description": "Parameters for finalizing a disk", + "type": "object", + "properties": { + "snapshot_name": { + "nullable": true, + "description": "If specified a snapshot of the disk will be created with the given name during finalization. If not specified, a snapshot for the disk will _not_ be created. A snapshot can be manually created once the disk transitions into the `Detached` state.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + } + }, + "FleetRole": { + "type": "string", + "enum": [ + "admin", + "collaborator", + "viewer" + ] + }, + "FleetRolePolicy": { + "description": "Policy for a particular resource\n\nNote that the Policy only describes access granted explicitly for this resource. The policies of parent resources can also cause a user to have access to this resource.", + "type": "object", + "properties": { + "role_assignments": { + "description": "Roles directly assigned on this resource", + "type": "array", + "items": { + "$ref": "#/components/schemas/FleetRoleRoleAssignment" + } + } + }, + "required": [ + "role_assignments" + ] + }, + "FleetRoleRoleAssignment": { + "description": "Describes the assignment of a particular role on a particular resource to a particular identity (user, group, etc.)\n\nThe resource is not part of this structure. Rather, `RoleAssignment`s are put into a `Policy` and that Policy is applied to a particular resource.", + "type": "object", + "properties": { + "identity_id": { + "type": "string", + "format": "uuid" + }, + "identity_type": { + "$ref": "#/components/schemas/IdentityType" + }, + "role_name": { + "$ref": "#/components/schemas/FleetRole" + } + }, + "required": [ + "identity_id", + "identity_type", + "role_name" + ] + }, + "FloatingIp": { + "description": "A Floating IP is a well-known IP address which can be attached and detached from instances.", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "instance_id": { + "nullable": true, + "description": "The ID of the instance that this Floating IP is attached to, if it is presently in use.", + "type": "string", + "format": "uuid" + }, + "ip": { + "description": "The IP address held by this resource.", + "type": "string", + "format": "ip" + }, + "ip_pool_id": { + "description": "The ID of the IP pool this resource belongs to.", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "project_id": { + "description": "The project this resource exists within.", + "type": "string", + "format": "uuid" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "ip", + "ip_pool_id", + "name", + "project_id", + "time_created", + "time_modified" + ] + }, + "FloatingIpAttach": { + "description": "Parameters for attaching a floating IP address to another resource", + "type": "object", + "properties": { + "kind": { + "description": "The type of `parent`'s resource", + "allOf": [ + { + "$ref": "#/components/schemas/FloatingIpParentKind" + } + ] + }, + "parent": { + "description": "Name or ID of the resource that this IP address should be attached to", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + } + }, + "required": [ + "kind", + "parent" + ] + }, + "FloatingIpCreate": { + "description": "Parameters for creating a new floating IP address for instances.", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "ip": { + "nullable": true, + "description": "An IP address to reserve for use as a floating IP. This field is optional: when not set, an address will be automatically chosen from `pool`. If set, then the IP must be available in the resolved `pool`.", + "type": "string", + "format": "ip" + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "pool": { + "nullable": true, + "description": "The parent IP pool that a floating IP is pulled from. If unset, the default pool is selected.", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + } + }, + "required": [ + "description", + "name" + ] + }, + "FloatingIpParentKind": { + "description": "The type of resource that a floating IP is attached to", + "type": "string", + "enum": [ + "instance" + ] + }, + "FloatingIpResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/FloatingIp" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "FloatingIpUpdate": { + "description": "Updateable identity-related parameters", + "type": "object", + "properties": { + "description": { + "nullable": true, + "type": "string" + }, + "name": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + } + }, + "Group": { + "description": "View of a Group", + "type": "object", + "properties": { + "display_name": { + "description": "Human-readable name that can identify the group", + "type": "string" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "silo_id": { + "description": "Uuid of the silo to which this group belongs", + "type": "string", + "format": "uuid" + } + }, + "required": [ + "display_name", + "id", + "silo_id" + ] + }, + "GroupResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Group" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "Histogramdouble": { + "description": "Histogram metric\n\nA histogram maintains the count of any number of samples, over a set of bins. Bins are specified on construction via their _left_ edges, inclusive. There can't be any \"gaps\" in the bins, and an additional bin may be added to the left, right, or both so that the bins extend to the entire range of the support.\n\nNote that any gaps, unsorted bins, or non-finite values will result in an error.", + "type": "object", + "properties": { + "bins": { + "description": "The bins of the histogram.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Bindouble" + } + }, + "max": { + "description": "The maximum value of all samples in the histogram.", + "type": "number", + "format": "double" + }, + "min": { + "description": "The minimum value of all samples in the histogram.", + "type": "number", + "format": "double" + }, + "n_samples": { + "description": "The total number of samples in the histogram.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "p50": { + "description": "p50 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p90": { + "description": "p95 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p99": { + "description": "p99 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "squared_mean": { + "description": "M2 for Welford's algorithm for variance calculation.\n\nRead about [Welford's algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm) for more information on the algorithm.", + "type": "number", + "format": "double" + }, + "start_time": { + "description": "The start time of the histogram.", + "type": "string", + "format": "date-time" + }, + "sum_of_samples": { + "description": "The sum of all samples in the histogram.", + "type": "number", + "format": "double" + } + }, + "required": [ + "bins", + "max", + "min", + "n_samples", + "p50", + "p90", + "p99", + "squared_mean", + "start_time", + "sum_of_samples" + ] + }, + "Histogramfloat": { + "description": "Histogram metric\n\nA histogram maintains the count of any number of samples, over a set of bins. Bins are specified on construction via their _left_ edges, inclusive. There can't be any \"gaps\" in the bins, and an additional bin may be added to the left, right, or both so that the bins extend to the entire range of the support.\n\nNote that any gaps, unsorted bins, or non-finite values will result in an error.", + "type": "object", + "properties": { + "bins": { + "description": "The bins of the histogram.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Binfloat" + } + }, + "max": { + "description": "The maximum value of all samples in the histogram.", + "type": "number", + "format": "float" + }, + "min": { + "description": "The minimum value of all samples in the histogram.", + "type": "number", + "format": "float" + }, + "n_samples": { + "description": "The total number of samples in the histogram.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "p50": { + "description": "p50 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p90": { + "description": "p95 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p99": { + "description": "p99 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "squared_mean": { + "description": "M2 for Welford's algorithm for variance calculation.\n\nRead about [Welford's algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm) for more information on the algorithm.", + "type": "number", + "format": "double" + }, + "start_time": { + "description": "The start time of the histogram.", + "type": "string", + "format": "date-time" + }, + "sum_of_samples": { + "description": "The sum of all samples in the histogram.", + "type": "number", + "format": "double" + } + }, + "required": [ + "bins", + "max", + "min", + "n_samples", + "p50", + "p90", + "p99", + "squared_mean", + "start_time", + "sum_of_samples" + ] + }, + "Histogramint16": { + "description": "Histogram metric\n\nA histogram maintains the count of any number of samples, over a set of bins. Bins are specified on construction via their _left_ edges, inclusive. There can't be any \"gaps\" in the bins, and an additional bin may be added to the left, right, or both so that the bins extend to the entire range of the support.\n\nNote that any gaps, unsorted bins, or non-finite values will result in an error.", + "type": "object", + "properties": { + "bins": { + "description": "The bins of the histogram.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Binint16" + } + }, + "max": { + "description": "The maximum value of all samples in the histogram.", + "type": "integer", + "format": "int16" + }, + "min": { + "description": "The minimum value of all samples in the histogram.", + "type": "integer", + "format": "int16" + }, + "n_samples": { + "description": "The total number of samples in the histogram.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "p50": { + "description": "p50 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p90": { + "description": "p95 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p99": { + "description": "p99 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "squared_mean": { + "description": "M2 for Welford's algorithm for variance calculation.\n\nRead about [Welford's algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm) for more information on the algorithm.", + "type": "number", + "format": "double" + }, + "start_time": { + "description": "The start time of the histogram.", + "type": "string", + "format": "date-time" + }, + "sum_of_samples": { + "description": "The sum of all samples in the histogram.", + "type": "integer", + "format": "int64" + } + }, + "required": [ + "bins", + "max", + "min", + "n_samples", + "p50", + "p90", + "p99", + "squared_mean", + "start_time", + "sum_of_samples" + ] + }, + "Histogramint32": { + "description": "Histogram metric\n\nA histogram maintains the count of any number of samples, over a set of bins. Bins are specified on construction via their _left_ edges, inclusive. There can't be any \"gaps\" in the bins, and an additional bin may be added to the left, right, or both so that the bins extend to the entire range of the support.\n\nNote that any gaps, unsorted bins, or non-finite values will result in an error.", + "type": "object", + "properties": { + "bins": { + "description": "The bins of the histogram.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Binint32" + } + }, + "max": { + "description": "The maximum value of all samples in the histogram.", + "type": "integer", + "format": "int32" + }, + "min": { + "description": "The minimum value of all samples in the histogram.", + "type": "integer", + "format": "int32" + }, + "n_samples": { + "description": "The total number of samples in the histogram.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "p50": { + "description": "p50 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p90": { + "description": "p95 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p99": { + "description": "p99 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "squared_mean": { + "description": "M2 for Welford's algorithm for variance calculation.\n\nRead about [Welford's algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm) for more information on the algorithm.", + "type": "number", + "format": "double" + }, + "start_time": { + "description": "The start time of the histogram.", + "type": "string", + "format": "date-time" + }, + "sum_of_samples": { + "description": "The sum of all samples in the histogram.", + "type": "integer", + "format": "int64" + } + }, + "required": [ + "bins", + "max", + "min", + "n_samples", + "p50", + "p90", + "p99", + "squared_mean", + "start_time", + "sum_of_samples" + ] + }, + "Histogramint64": { + "description": "Histogram metric\n\nA histogram maintains the count of any number of samples, over a set of bins. Bins are specified on construction via their _left_ edges, inclusive. There can't be any \"gaps\" in the bins, and an additional bin may be added to the left, right, or both so that the bins extend to the entire range of the support.\n\nNote that any gaps, unsorted bins, or non-finite values will result in an error.", + "type": "object", + "properties": { + "bins": { + "description": "The bins of the histogram.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Binint64" + } + }, + "max": { + "description": "The maximum value of all samples in the histogram.", + "type": "integer", + "format": "int64" + }, + "min": { + "description": "The minimum value of all samples in the histogram.", + "type": "integer", + "format": "int64" + }, + "n_samples": { + "description": "The total number of samples in the histogram.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "p50": { + "description": "p50 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p90": { + "description": "p95 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p99": { + "description": "p99 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "squared_mean": { + "description": "M2 for Welford's algorithm for variance calculation.\n\nRead about [Welford's algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm) for more information on the algorithm.", + "type": "number", + "format": "double" + }, + "start_time": { + "description": "The start time of the histogram.", + "type": "string", + "format": "date-time" + }, + "sum_of_samples": { + "description": "The sum of all samples in the histogram.", + "type": "integer", + "format": "int64" + } + }, + "required": [ + "bins", + "max", + "min", + "n_samples", + "p50", + "p90", + "p99", + "squared_mean", + "start_time", + "sum_of_samples" + ] + }, + "Histogramint8": { + "description": "Histogram metric\n\nA histogram maintains the count of any number of samples, over a set of bins. Bins are specified on construction via their _left_ edges, inclusive. There can't be any \"gaps\" in the bins, and an additional bin may be added to the left, right, or both so that the bins extend to the entire range of the support.\n\nNote that any gaps, unsorted bins, or non-finite values will result in an error.", + "type": "object", + "properties": { + "bins": { + "description": "The bins of the histogram.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Binint8" + } + }, + "max": { + "description": "The maximum value of all samples in the histogram.", + "type": "integer", + "format": "int8" + }, + "min": { + "description": "The minimum value of all samples in the histogram.", + "type": "integer", + "format": "int8" + }, + "n_samples": { + "description": "The total number of samples in the histogram.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "p50": { + "description": "p50 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p90": { + "description": "p95 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p99": { + "description": "p99 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "squared_mean": { + "description": "M2 for Welford's algorithm for variance calculation.\n\nRead about [Welford's algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm) for more information on the algorithm.", + "type": "number", + "format": "double" + }, + "start_time": { + "description": "The start time of the histogram.", + "type": "string", + "format": "date-time" + }, + "sum_of_samples": { + "description": "The sum of all samples in the histogram.", + "type": "integer", + "format": "int64" + } + }, + "required": [ + "bins", + "max", + "min", + "n_samples", + "p50", + "p90", + "p99", + "squared_mean", + "start_time", + "sum_of_samples" + ] + }, + "Histogramuint16": { + "description": "Histogram metric\n\nA histogram maintains the count of any number of samples, over a set of bins. Bins are specified on construction via their _left_ edges, inclusive. There can't be any \"gaps\" in the bins, and an additional bin may be added to the left, right, or both so that the bins extend to the entire range of the support.\n\nNote that any gaps, unsorted bins, or non-finite values will result in an error.", + "type": "object", + "properties": { + "bins": { + "description": "The bins of the histogram.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Binuint16" + } + }, + "max": { + "description": "The maximum value of all samples in the histogram.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "min": { + "description": "The minimum value of all samples in the histogram.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "n_samples": { + "description": "The total number of samples in the histogram.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "p50": { + "description": "p50 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p90": { + "description": "p95 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p99": { + "description": "p99 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "squared_mean": { + "description": "M2 for Welford's algorithm for variance calculation.\n\nRead about [Welford's algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm) for more information on the algorithm.", + "type": "number", + "format": "double" + }, + "start_time": { + "description": "The start time of the histogram.", + "type": "string", + "format": "date-time" + }, + "sum_of_samples": { + "description": "The sum of all samples in the histogram.", + "type": "integer", + "format": "int64" + } + }, + "required": [ + "bins", + "max", + "min", + "n_samples", + "p50", + "p90", + "p99", + "squared_mean", + "start_time", + "sum_of_samples" + ] + }, + "Histogramuint32": { + "description": "Histogram metric\n\nA histogram maintains the count of any number of samples, over a set of bins. Bins are specified on construction via their _left_ edges, inclusive. There can't be any \"gaps\" in the bins, and an additional bin may be added to the left, right, or both so that the bins extend to the entire range of the support.\n\nNote that any gaps, unsorted bins, or non-finite values will result in an error.", + "type": "object", + "properties": { + "bins": { + "description": "The bins of the histogram.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Binuint32" + } + }, + "max": { + "description": "The maximum value of all samples in the histogram.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "min": { + "description": "The minimum value of all samples in the histogram.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "n_samples": { + "description": "The total number of samples in the histogram.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "p50": { + "description": "p50 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p90": { + "description": "p95 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p99": { + "description": "p99 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "squared_mean": { + "description": "M2 for Welford's algorithm for variance calculation.\n\nRead about [Welford's algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm) for more information on the algorithm.", + "type": "number", + "format": "double" + }, + "start_time": { + "description": "The start time of the histogram.", + "type": "string", + "format": "date-time" + }, + "sum_of_samples": { + "description": "The sum of all samples in the histogram.", + "type": "integer", + "format": "int64" + } + }, + "required": [ + "bins", + "max", + "min", + "n_samples", + "p50", + "p90", + "p99", + "squared_mean", + "start_time", + "sum_of_samples" + ] + }, + "Histogramuint64": { + "description": "Histogram metric\n\nA histogram maintains the count of any number of samples, over a set of bins. Bins are specified on construction via their _left_ edges, inclusive. There can't be any \"gaps\" in the bins, and an additional bin may be added to the left, right, or both so that the bins extend to the entire range of the support.\n\nNote that any gaps, unsorted bins, or non-finite values will result in an error.", + "type": "object", + "properties": { + "bins": { + "description": "The bins of the histogram.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Binuint64" + } + }, + "max": { + "description": "The maximum value of all samples in the histogram.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "min": { + "description": "The minimum value of all samples in the histogram.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "n_samples": { + "description": "The total number of samples in the histogram.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "p50": { + "description": "p50 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p90": { + "description": "p95 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p99": { + "description": "p99 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "squared_mean": { + "description": "M2 for Welford's algorithm for variance calculation.\n\nRead about [Welford's algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm) for more information on the algorithm.", + "type": "number", + "format": "double" + }, + "start_time": { + "description": "The start time of the histogram.", + "type": "string", + "format": "date-time" + }, + "sum_of_samples": { + "description": "The sum of all samples in the histogram.", + "type": "integer", + "format": "int64" + } + }, + "required": [ + "bins", + "max", + "min", + "n_samples", + "p50", + "p90", + "p99", + "squared_mean", + "start_time", + "sum_of_samples" + ] + }, + "Histogramuint8": { + "description": "Histogram metric\n\nA histogram maintains the count of any number of samples, over a set of bins. Bins are specified on construction via their _left_ edges, inclusive. There can't be any \"gaps\" in the bins, and an additional bin may be added to the left, right, or both so that the bins extend to the entire range of the support.\n\nNote that any gaps, unsorted bins, or non-finite values will result in an error.", + "type": "object", + "properties": { + "bins": { + "description": "The bins of the histogram.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Binuint8" + } + }, + "max": { + "description": "The maximum value of all samples in the histogram.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "min": { + "description": "The minimum value of all samples in the histogram.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "n_samples": { + "description": "The total number of samples in the histogram.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "p50": { + "description": "p50 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p90": { + "description": "p95 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "p99": { + "description": "p99 Quantile", + "allOf": [ + { + "$ref": "#/components/schemas/Quantile" + } + ] + }, + "squared_mean": { + "description": "M2 for Welford's algorithm for variance calculation.\n\nRead about [Welford's algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm) for more information on the algorithm.", + "type": "number", + "format": "double" + }, + "start_time": { + "description": "The start time of the histogram.", + "type": "string", + "format": "date-time" + }, + "sum_of_samples": { + "description": "The sum of all samples in the histogram.", + "type": "integer", + "format": "int64" + } + }, + "required": [ + "bins", + "max", + "min", + "n_samples", + "p50", + "p90", + "p99", + "squared_mean", + "start_time", + "sum_of_samples" + ] + }, + "Hostname": { + "title": "An RFC-1035-compliant hostname", + "description": "A hostname identifies a host on a network, and is usually a dot-delimited sequence of labels, where each label contains only letters, digits, or the hyphen. See RFCs 1035 and 952 for more details.", + "type": "string", + "pattern": "^([a-zA-Z0-9]+[a-zA-Z0-9\\-]*(? 2**53 addresses), integer precision will be lost, in exchange for representing the entire range. In such a case the pool still has many available addresses.", + "type": "object", + "properties": { + "capacity": { + "description": "The total number of addresses in the pool.", + "type": "number", + "format": "double" + }, + "remaining": { + "description": "The number of remaining addresses in the pool.", + "type": "number", + "format": "double" + } + }, + "required": [ + "capacity", + "remaining" + ] + }, + "IpRange": { + "oneOf": [ + { + "title": "v4", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4Range" + } + ] + }, + { + "title": "v6", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Range" + } + ] + } + ] + }, + "IpVersion": { + "description": "The IP address version.", + "type": "string", + "enum": [ + "v4", + "v6" + ] + }, + "Ipv4Assignment": { + "description": "How a VPC-private IP address is assigned to a network interface.", + "oneOf": [ + { + "description": "Automatically assign an IP address from the VPC Subnet.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "auto" + ] + } + }, + "required": [ + "type" + ] + }, + { + "description": "Explicitly assign a specific address, if available.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "explicit" + ] + }, + "value": { + "type": "string", + "format": "ipv4" + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "Ipv4Net": { + "example": "192.168.1.0/24", + "title": "An IPv4 subnet", + "description": "An IPv4 subnet, including prefix and prefix length", + "x-rust-type": { + "crate": "oxnet", + "path": "oxnet::Ipv4Net", + "version": "0.1.0" + }, + "type": "string", + "pattern": "^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])/([0-9]|1[0-9]|2[0-9]|3[0-2])$" + }, + "Ipv4Range": { + "description": "A non-decreasing IPv4 address range, inclusive of both ends.\n\nThe first address must be less than or equal to the last address.", + "type": "object", + "properties": { + "first": { + "type": "string", + "format": "ipv4" + }, + "last": { + "type": "string", + "format": "ipv4" + } + }, + "required": [ + "first", + "last" + ] + }, + "Ipv6Assignment": { + "description": "How a VPC-private IP address is assigned to a network interface.", + "oneOf": [ + { + "description": "Automatically assign an IP address from the VPC Subnet.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "auto" + ] + } + }, + "required": [ + "type" + ] + }, + { + "description": "Explicitly assign a specific address, if available.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "explicit" + ] + }, + "value": { + "type": "string", + "format": "ipv6" + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "Ipv6Net": { + "example": "fd12:3456::/64", + "title": "An IPv6 subnet", + "description": "An IPv6 subnet, including prefix and subnet mask", + "x-rust-type": { + "crate": "oxnet", + "path": "oxnet::Ipv6Net", + "version": "0.1.0" + }, + "type": "string", + "pattern": "^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$" + }, + "Ipv6Range": { + "description": "A non-decreasing IPv6 address range, inclusive of both ends.\n\nThe first address must be less than or equal to the last address.", + "type": "object", + "properties": { + "first": { + "type": "string", + "format": "ipv6" + }, + "last": { + "type": "string", + "format": "ipv6" + } + }, + "required": [ + "first", + "last" + ] + }, + "L4PortRange": { + "example": "22", + "title": "A range of IP ports", + "description": "An inclusive-inclusive range of IP ports. The second port may be omitted to represent a single port.", + "type": "string", + "pattern": "^[0-9]{1,5}(-[0-9]{1,5})?$", + "minLength": 1, + "maxLength": 11 + }, + "LinkConfigCreate": { + "description": "Switch link configuration.", + "type": "object", + "properties": { + "autoneg": { + "description": "Whether or not to set autonegotiation.", + "type": "boolean" + }, + "fec": { + "nullable": true, + "description": "The requested forward-error correction method. If this is not specified, the standard FEC for the underlying media will be applied if it can be determined.", + "allOf": [ + { + "$ref": "#/components/schemas/LinkFec" + } + ] + }, + "link_name": { + "description": "Link name. On ports that are not broken out, this is always phy0. On a 2x breakout the options are phy0 and phy1, on 4x phy0-phy3, etc.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "lldp": { + "description": "The link-layer discovery protocol (LLDP) configuration for the link.", + "allOf": [ + { + "$ref": "#/components/schemas/LldpLinkConfigCreate" + } + ] + }, + "mtu": { + "description": "Maximum transmission unit for the link.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "speed": { + "description": "The speed of the link.", + "allOf": [ + { + "$ref": "#/components/schemas/LinkSpeed" + } + ] + }, + "tx_eq": { + "nullable": true, + "description": "Optional tx_eq settings.", + "allOf": [ + { + "$ref": "#/components/schemas/TxEqConfig" + } + ] + } + }, + "required": [ + "autoneg", + "link_name", + "lldp", + "mtu", + "speed" + ] + }, + "LinkFec": { + "description": "The forward error correction mode of a link.", + "oneOf": [ + { + "description": "Firecode forward error correction.", + "type": "string", + "enum": [ + "firecode" + ] + }, + { + "description": "No forward error correction.", + "type": "string", + "enum": [ + "none" + ] + }, + { + "description": "Reed-Solomon forward error correction.", + "type": "string", + "enum": [ + "rs" + ] + } + ] + }, + "LinkSpeed": { + "description": "The speed of a link.", + "oneOf": [ + { + "description": "Zero gigabits per second.", + "type": "string", + "enum": [ + "speed0_g" + ] + }, + { + "description": "1 gigabit per second.", + "type": "string", + "enum": [ + "speed1_g" + ] + }, + { + "description": "10 gigabits per second.", + "type": "string", + "enum": [ + "speed10_g" + ] + }, + { + "description": "25 gigabits per second.", + "type": "string", + "enum": [ + "speed25_g" + ] + }, + { + "description": "40 gigabits per second.", + "type": "string", + "enum": [ + "speed40_g" + ] + }, + { + "description": "50 gigabits per second.", + "type": "string", + "enum": [ + "speed50_g" + ] + }, + { + "description": "100 gigabits per second.", + "type": "string", + "enum": [ + "speed100_g" + ] + }, + { + "description": "200 gigabits per second.", + "type": "string", + "enum": [ + "speed200_g" + ] + }, + { + "description": "400 gigabits per second.", + "type": "string", + "enum": [ + "speed400_g" + ] + } + ] + }, + "LldpLinkConfig": { + "description": "A link layer discovery protocol (LLDP) service configuration.", + "type": "object", + "properties": { + "chassis_id": { + "nullable": true, + "description": "The LLDP chassis identifier TLV.", + "type": "string" + }, + "enabled": { + "description": "Whether or not the LLDP service is enabled.", + "type": "boolean" + }, + "id": { + "description": "The id of this LLDP service instance.", + "type": "string", + "format": "uuid" + }, + "link_description": { + "nullable": true, + "description": "The LLDP link description TLV.", + "type": "string" + }, + "link_name": { + "nullable": true, + "description": "The LLDP link name TLV.", + "type": "string" + }, + "management_ip": { + "nullable": true, + "description": "The LLDP management IP TLV.", + "type": "string", + "format": "ip" + }, + "system_description": { + "nullable": true, + "description": "The LLDP system description TLV.", + "type": "string" + }, + "system_name": { + "nullable": true, + "description": "The LLDP system name TLV.", + "type": "string" + } + }, + "required": [ + "enabled", + "id" + ] + }, + "LldpLinkConfigCreate": { + "description": "The LLDP configuration associated with a port.", + "type": "object", + "properties": { + "chassis_id": { + "nullable": true, + "description": "The LLDP chassis identifier TLV.", + "type": "string" + }, + "enabled": { + "description": "Whether or not LLDP is enabled.", + "type": "boolean" + }, + "link_description": { + "nullable": true, + "description": "The LLDP link description TLV.", + "type": "string" + }, + "link_name": { + "nullable": true, + "description": "The LLDP link name TLV.", + "type": "string" + }, + "management_ip": { + "nullable": true, + "description": "The LLDP management IP TLV.", + "type": "string", + "format": "ip" + }, + "system_description": { + "nullable": true, + "description": "The LLDP system description TLV.", + "type": "string" + }, + "system_name": { + "nullable": true, + "description": "The LLDP system name TLV.", + "type": "string" + } + }, + "required": [ + "enabled" + ] + }, + "LldpNeighbor": { + "description": "Information about LLDP advertisements from other network entities directly connected to a switch port. This structure contains both metadata about when and where the neighbor was seen, as well as the specific information the neighbor was advertising.", + "type": "object", + "properties": { + "chassis_id": { + "description": "The LLDP chassis identifier advertised by the neighbor", + "type": "string" + }, + "first_seen": { + "description": "Initial sighting of this LldpNeighbor", + "type": "string", + "format": "date-time" + }, + "last_seen": { + "description": "Most recent sighting of this LldpNeighbor", + "type": "string", + "format": "date-time" + }, + "link_description": { + "nullable": true, + "description": "The LLDP link description advertised by the neighbor", + "type": "string" + }, + "link_name": { + "description": "The LLDP link name advertised by the neighbor", + "type": "string" + }, + "local_port": { + "description": "The port on which the neighbor was seen", + "type": "string" + }, + "management_ip": { + "description": "The LLDP management IP(s) advertised by the neighbor", + "type": "array", + "items": { + "$ref": "#/components/schemas/ManagementAddress" + } + }, + "system_description": { + "nullable": true, + "description": "The LLDP system description advertised by the neighbor", + "type": "string" + }, + "system_name": { + "nullable": true, + "description": "The LLDP system name advertised by the neighbor", + "type": "string" + } + }, + "required": [ + "chassis_id", + "first_seen", + "last_seen", + "link_name", + "local_port", + "management_ip" + ] + }, + "LldpNeighborResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/LldpNeighbor" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "LoopbackAddress": { + "description": "A loopback address is an address that is assigned to a rack switch but is not associated with any particular port.", + "type": "object", + "properties": { + "address": { + "description": "The loopback IP address and prefix length.", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + }, + "address_lot_block_id": { + "description": "The address lot block this address came from.", + "type": "string", + "format": "uuid" + }, + "id": { + "description": "The id of the loopback address.", + "type": "string", + "format": "uuid" + }, + "rack_id": { + "description": "The id of the rack where this loopback address is assigned.", + "type": "string", + "format": "uuid" + }, + "switch_location": { + "description": "Switch location where this loopback address is assigned.", + "type": "string" + } + }, + "required": [ + "address", + "address_lot_block_id", + "id", + "rack_id", + "switch_location" + ] + }, + "LoopbackAddressCreate": { + "description": "Parameters for creating a loopback address on a particular rack switch.", + "type": "object", + "properties": { + "address": { + "description": "The address to create.", + "type": "string", + "format": "ip" + }, + "address_lot": { + "description": "The name or id of the address lot this loopback address will pull an address from.", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + }, + "anycast": { + "description": "Address is an anycast address. This allows the address to be assigned to multiple locations simultaneously.", + "type": "boolean" + }, + "mask": { + "description": "The subnet mask to use for the address.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "rack_id": { + "description": "The rack containing the switch this loopback address will be configured on.", + "type": "string", + "format": "uuid" + }, + "switch_location": { + "description": "The location of the switch within the rack this loopback address will be configured on.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + }, + "required": [ + "address", + "address_lot", + "anycast", + "mask", + "rack_id", + "switch_location" + ] + }, + "LoopbackAddressResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/LoopbackAddress" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "MacAddr": { + "example": "ff:ff:ff:ff:ff:ff", + "title": "A MAC address", + "description": "A Media Access Control address, in EUI-48 format", + "type": "string", + "pattern": "^([0-9a-fA-F]{0,2}:){5}[0-9a-fA-F]{0,2}$", + "minLength": 5, + "maxLength": 17 + }, + "ManagementAddress": { + "type": "object", + "properties": { + "addr": { + "$ref": "#/components/schemas/NetworkAddress" + }, + "interface_num": { + "$ref": "#/components/schemas/InterfaceNum" + }, + "oid": { + "nullable": true, + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + } + }, + "required": [ + "addr", + "interface_num" + ] + }, + "Measurement": { + "description": "A `Measurement` is a timestamped datum from a single metric", + "type": "object", + "properties": { + "datum": { + "$ref": "#/components/schemas/Datum" + }, + "timestamp": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "datum", + "timestamp" + ] + }, + "MeasurementResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Measurement" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "MetricType": { + "description": "The type of the metric itself, indicating what its values represent.", + "oneOf": [ + { + "description": "The value represents an instantaneous measurement in time.", + "type": "string", + "enum": [ + "gauge" + ] + }, + { + "description": "The value represents a difference between two points in time.", + "type": "string", + "enum": [ + "delta" + ] + }, + { + "description": "The value represents an accumulation between two points in time.", + "type": "string", + "enum": [ + "cumulative" + ] + } + ] + }, + "MissingDatum": { + "type": "object", + "properties": { + "datum_type": { + "$ref": "#/components/schemas/DatumType" + }, + "start_time": { + "nullable": true, + "type": "string", + "format": "date-time" + } + }, + "required": [ + "datum_type" + ] + }, + "MulticastGroup": { + "description": "View of a Multicast Group", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "ip_pool_id": { + "description": "The ID of the IP pool this resource belongs to.", + "type": "string", + "format": "uuid" + }, + "multicast_ip": { + "description": "The multicast IP address held by this resource.", + "type": "string", + "format": "ip" + }, + "mvlan": { + "nullable": true, + "description": "Multicast VLAN (MVLAN) for egress multicast traffic to upstream networks. None means no VLAN tagging on egress.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "source_ips": { + "description": "Source IP addresses for Source-Specific Multicast (SSM). Empty array means any source is allowed.", + "type": "array", + "items": { + "type": "string", + "format": "ip" + } + }, + "state": { + "description": "Current state of the multicast group.", + "type": "string" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "ip_pool_id", + "multicast_ip", + "name", + "source_ips", + "state", + "time_created", + "time_modified" + ] + }, + "MulticastGroupCreate": { + "description": "Create-time parameters for a multicast group.", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "multicast_ip": { + "nullable": true, + "description": "The multicast IP address to allocate. If None, one will be allocated from the default pool.", + "default": null, + "type": "string", + "format": "ip" + }, + "mvlan": { + "nullable": true, + "description": "Multicast VLAN (MVLAN) for egress multicast traffic to upstream networks. Tags packets leaving the rack to traverse VLAN-segmented upstream networks.\n\nValid range: 2-4094 (VLAN IDs 0-1 are reserved by IEEE 802.1Q standard).", + "default": null, + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "pool": { + "nullable": true, + "description": "Name or ID of the IP pool to allocate from. If None, uses the default multicast pool.", + "default": null, + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + }, + "source_ips": { + "nullable": true, + "description": "Source IP addresses for Source-Specific Multicast (SSM).\n\nNone uses default behavior (Any-Source Multicast). Empty list explicitly allows any source (Any-Source Multicast). Non-empty list restricts to specific sources (SSM).", + "default": null, + "type": "array", + "items": { + "type": "string", + "format": "ip" + } + } + }, + "required": [ + "description", + "name" + ] + }, + "MulticastGroupMember": { + "description": "View of a Multicast Group Member (instance belonging to a multicast group)", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "instance_id": { + "description": "The ID of the instance that is a member of this group.", + "type": "string", + "format": "uuid" + }, + "multicast_group_id": { + "description": "The ID of the multicast group this member belongs to.", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "state": { + "description": "Current state of the multicast group membership.", + "type": "string" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "instance_id", + "multicast_group_id", + "name", + "state", + "time_created", + "time_modified" + ] + }, + "MulticastGroupMemberAdd": { + "description": "Parameters for adding an instance to a multicast group.", + "type": "object", + "properties": { + "instance": { + "description": "Name or ID of the instance to add to the multicast group", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + } + }, + "required": [ + "instance" + ] + }, + "MulticastGroupMemberResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/MulticastGroupMember" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "MulticastGroupResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/MulticastGroup" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "MulticastGroupUpdate": { + "description": "Update-time parameters for a multicast group.", + "type": "object", + "properties": { + "description": { + "nullable": true, + "type": "string" + }, + "mvlan": { + "nullable": true, + "description": "Multicast VLAN (MVLAN) for egress multicast traffic to upstream networks. Set to null to clear the MVLAN. Valid range: 2-4094 when provided. Omit the field to leave mvlan unchanged.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "name": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "source_ips": { + "nullable": true, + "type": "array", + "items": { + "type": "string", + "format": "ip" + } + } + } + }, + "Name": { + "title": "A name unique within the parent collection", + "description": "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID, but they may contain a UUID. They can be at most 63 characters long.", + "type": "string", + "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$", + "minLength": 1, + "maxLength": 63 + }, + "NameOrId": { + "oneOf": [ + { + "title": "id", + "allOf": [ + { + "type": "string", + "format": "uuid" + } + ] + }, + { + "title": "name", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + ] + }, + "NetworkAddress": { + "oneOf": [ + { + "type": "object", + "properties": { + "ip_addr": { + "type": "string", + "format": "ip" + } + }, + "required": [ + "ip_addr" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "i_e_e_e802": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + } + }, + "required": [ + "i_e_e_e802" + ], + "additionalProperties": false + } + ] + }, + "NetworkInterface": { + "description": "Information required to construct a virtual network interface", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "ip": { + "type": "string", + "format": "ip" + }, + "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 + }, + "subnet": { + "$ref": "#/components/schemas/IpNet" + }, + "transit_ips": { + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/IpNet" + } + }, + "vni": { + "$ref": "#/components/schemas/Vni" + } + }, + "required": [ + "id", + "ip", + "kind", + "mac", + "name", + "primary", + "slot", + "subnet", + "vni" + ] + }, + "NetworkInterfaceKind": { + "description": "The type of network interface", + "oneOf": [ + { + "description": "A vNIC attached to a guest instance", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "enum": [ + "instance" + ] + } + }, + "required": [ + "id", + "type" + ] + }, + { + "description": "A vNIC associated with an internal service", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "enum": [ + "service" + ] + } + }, + "required": [ + "id", + "type" + ] + }, + { + "description": "A vNIC associated with a probe", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "enum": [ + "probe" + ] + } + }, + "required": [ + "id", + "type" + ] + } + ] + }, + "OxqlQueryResult": { + "description": "The result of a successful OxQL query.", + "type": "object", + "properties": { + "tables": { + "description": "Tables resulting from the query, each containing timeseries.", + "type": "array", + "items": { + "$ref": "#/components/schemas/OxqlTable" + } + } + }, + "required": [ + "tables" + ] + }, + "OxqlTable": { + "description": "A table represents one or more timeseries with the same schema.\n\nA table is the result of an OxQL query. It contains a name, usually the name of the timeseries schema from which the data is derived, and any number of timeseries, which contain the actual data.", + "type": "object", + "properties": { + "name": { + "description": "The name of the table.", + "type": "string" + }, + "timeseries": { + "description": "The set of timeseries in the table, ordered by key.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Timeseries" + } + } + }, + "required": [ + "name", + "timeseries" + ] + }, + "Password": { + "title": "A password used to authenticate a user", + "description": "Passwords may be subject to additional constraints.", + "type": "string", + "maxLength": 512 + }, + "PhysicalDisk": { + "description": "View of a Physical Disk\n\nPhysical disks reside in a particular sled and are used to store both Instance Disk data as well as internal metadata.", + "type": "object", + "properties": { + "form_factor": { + "$ref": "#/components/schemas/PhysicalDiskKind" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "model": { + "type": "string" + }, + "policy": { + "description": "The operator-defined policy for a physical disk.", + "allOf": [ + { + "$ref": "#/components/schemas/PhysicalDiskPolicy" + } + ] + }, + "serial": { + "type": "string" + }, + "sled_id": { + "nullable": true, + "description": "The sled to which this disk is attached, if any.", + "type": "string", + "format": "uuid" + }, + "state": { + "description": "The current state Nexus believes the disk to be in.", + "allOf": [ + { + "$ref": "#/components/schemas/PhysicalDiskState" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + }, + "vendor": { + "type": "string" + } + }, + "required": [ + "form_factor", + "id", + "model", + "policy", + "serial", + "state", + "time_created", + "time_modified", + "vendor" + ] + }, + "PhysicalDiskKind": { + "description": "Describes the form factor of physical disks.", + "type": "string", + "enum": [ + "m2", + "u2" + ] + }, + "PhysicalDiskPolicy": { + "description": "The operator-defined policy of a physical disk.", + "oneOf": [ + { + "description": "The operator has indicated that the disk is in-service.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "in_service" + ] + } + }, + "required": [ + "kind" + ] + }, + { + "description": "The operator has indicated that the disk has been permanently removed from service.\n\nThis is a terminal state: once a particular disk ID is expunged, it will never return to service. (The actual hardware may be reused, but it will be treated as a brand-new disk.)\n\nAn expunged disk is always non-provisionable.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "expunged" + ] + } + }, + "required": [ + "kind" + ] + } + ] + }, + "PhysicalDiskResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/PhysicalDisk" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "PhysicalDiskState": { + "description": "The current state of the disk, as determined by Nexus.", + "oneOf": [ + { + "description": "The disk is currently active, and has resources allocated on it.", + "type": "string", + "enum": [ + "active" + ] + }, + { + "description": "The disk has been permanently removed from service.\n\nThis is a terminal state: once a particular disk ID is decommissioned, it will never return to service. (The actual hardware may be reused, but it will be treated as a brand-new disk.)", + "type": "string", + "enum": [ + "decommissioned" + ] + } + ] + }, + "Ping": { + "type": "object", + "properties": { + "status": { + "description": "Whether the external API is reachable. Will always be Ok if the endpoint returns anything at all.", + "allOf": [ + { + "$ref": "#/components/schemas/PingStatus" + } + ] + } + }, + "required": [ + "status" + ] + }, + "PingStatus": { + "type": "string", + "enum": [ + "ok" + ] + }, + "Points": { + "description": "Timepoints and values for one timeseries.", + "type": "object", + "properties": { + "start_times": { + "nullable": true, + "type": "array", + "items": { + "type": "string", + "format": "date-time" + } + }, + "timestamps": { + "type": "array", + "items": { + "type": "string", + "format": "date-time" + } + }, + "values": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Values" + } + } + }, + "required": [ + "timestamps", + "values" + ] + }, + "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" + ] + } + ] + }, + "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" + ] + }, + "PrivateIpv6Stack": { + "description": "The VPC-private IPv6 stack for a network interface", + "type": "object", + "properties": { + "ip": { + "description": "The VPC-private IPv6 address for the interface.", + "type": "string", + "format": "ipv6" + }, + "transit_ips": { + "description": "A set of additional IPv6 networks that this interface may send and receive traffic on.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv6Net" + } + } + }, + "required": [ + "ip", + "transit_ips" + ] + }, + "PrivateIpv6StackCreate": { + "description": "Configuration for a network interface's IPv6 addressing.", + "type": "object", + "properties": { + "ip": { + "description": "The VPC-private address to assign to the interface.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Assignment" + } + ] + }, + "transit_ips": { + "description": "Additional IP networks the interface can send / receive on.", + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv6Net" + } + } + }, + "required": [ + "ip" + ] + }, + "Probe": { + "description": "Identity-related metadata that's included in nearly all public API objects", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "sled": { + "type": "string", + "format": "uuid" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "name", + "sled", + "time_created", + "time_modified" + ] + }, + "ProbeCreate": { + "description": "Create time parameters for probes.", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "ip_pool": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "sled": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "description", + "name", + "sled" + ] + }, + "ProbeExternalIp": { + "type": "object", + "properties": { + "first_port": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "ip": { + "type": "string", + "format": "ip" + }, + "kind": { + "$ref": "#/components/schemas/ProbeExternalIpKind" + }, + "last_port": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "first_port", + "ip", + "kind", + "last_port" + ] + }, + "ProbeExternalIpKind": { + "type": "string", + "enum": [ + "snat", + "floating", + "ephemeral" + ] + }, + "ProbeInfo": { + "type": "object", + "properties": { + "external_ips": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProbeExternalIp" + } + }, + "id": { + "type": "string", + "format": "uuid" + }, + "interface": { + "$ref": "#/components/schemas/NetworkInterface" + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "sled": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "external_ips", + "id", + "interface", + "name", + "sled" + ] + }, + "ProbeInfoResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/ProbeInfo" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "Project": { + "description": "View of a Project", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "name", + "time_created", + "time_modified" + ] + }, + "ProjectCreate": { + "description": "Create-time parameters for a `Project`", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "description", + "name" + ] + }, + "ProjectResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Project" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "ProjectRole": { + "type": "string", + "enum": [ + "admin", + "collaborator", + "limited_collaborator", + "viewer" + ] + }, + "ProjectRolePolicy": { + "description": "Policy for a particular resource\n\nNote that the Policy only describes access granted explicitly for this resource. The policies of parent resources can also cause a user to have access to this resource.", + "type": "object", + "properties": { + "role_assignments": { + "description": "Roles directly assigned on this resource", + "type": "array", + "items": { + "$ref": "#/components/schemas/ProjectRoleRoleAssignment" + } + } + }, + "required": [ + "role_assignments" + ] + }, + "ProjectRoleRoleAssignment": { + "description": "Describes the assignment of a particular role on a particular resource to a particular identity (user, group, etc.)\n\nThe resource is not part of this structure. Rather, `RoleAssignment`s are put into a `Policy` and that Policy is applied to a particular resource.", + "type": "object", + "properties": { + "identity_id": { + "type": "string", + "format": "uuid" + }, + "identity_type": { + "$ref": "#/components/schemas/IdentityType" + }, + "role_name": { + "$ref": "#/components/schemas/ProjectRole" + } + }, + "required": [ + "identity_id", + "identity_type", + "role_name" + ] + }, + "ProjectUpdate": { + "description": "Updateable properties of a `Project`", + "type": "object", + "properties": { + "description": { + "nullable": true, + "type": "string" + }, + "name": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + } + }, + "Quantile": { + "description": "Structure for estimating the p-quantile of a population.\n\nThis is based on the P² algorithm for estimating quantiles using constant space.\n\nThe algorithm consists of maintaining five markers: the minimum, the p/2-, p-, and (1 + p)/2 quantiles, and the maximum.", + "type": "object", + "properties": { + "desired_marker_positions": { + "description": "The desired marker positions.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "minItems": 5, + "maxItems": 5 + }, + "marker_heights": { + "description": "The heights of the markers.", + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "minItems": 5, + "maxItems": 5 + }, + "marker_positions": { + "description": "The positions of the markers.\n\nWe track sample size in the 5th position, as useful observations won't start until we've filled the heights at the 6th sample anyway This does deviate from the paper, but it's a more useful representation that works according to the paper's algorithm.", + "type": "array", + "items": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "minItems": 5, + "maxItems": 5 + }, + "p": { + "description": "The p value for the quantile.", + "type": "number", + "format": "double" + } + }, + "required": [ + "desired_marker_positions", + "marker_heights", + "marker_positions", + "p" + ] + }, + "Rack": { + "description": "View of an Rack", + "type": "object", + "properties": { + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "time_created", + "time_modified" + ] + }, + "RackResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Rack" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "Route": { + "description": "A route to a destination network through a gateway address.", + "type": "object", + "properties": { + "dst": { + "description": "The route destination.", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + }, + "gw": { + "description": "The route gateway.", + "type": "string", + "format": "ip" + }, + "rib_priority": { + "nullable": true, + "description": "Route RIB priority. Higher priority indicates precedence within and across protocols.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "vid": { + "nullable": true, + "description": "VLAN id the gateway is reachable over.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "dst", + "gw" + ] + }, + "RouteConfig": { + "description": "Route configuration data associated with a switch port configuration.", + "type": "object", + "properties": { + "link_name": { + "description": "Link name. On ports that are not broken out, this is always phy0. On a 2x breakout the options are phy0 and phy1, on 4x phy0-phy3, etc.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "routes": { + "description": "The set of routes assigned to a switch port.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Route" + } + } + }, + "required": [ + "link_name", + "routes" + ] + }, + "RouteDestination": { + "description": "A `RouteDestination` is used to match traffic with a routing rule based on the destination of that traffic.\n\nWhen traffic is to be sent to a destination that is within a given `RouteDestination`, the corresponding `RouterRoute` applies, and traffic will be forward to the `RouteTarget` for that rule.", + "oneOf": [ + { + "description": "Route applies to traffic destined for the specified IP address", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ip" + ] + }, + "value": { + "type": "string", + "format": "ip" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "Route applies to traffic destined for the specified IP subnet", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ip_net" + ] + }, + "value": { + "$ref": "#/components/schemas/IpNet" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "Route applies to traffic destined for the specified VPC", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "vpc" + ] + }, + "value": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "Route applies to traffic destined for the specified VPC subnet", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "subnet" + ] + }, + "value": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "RouteTarget": { + "description": "A `RouteTarget` describes the possible locations that traffic matching a route destination can be sent.", + "oneOf": [ + { + "description": "Forward traffic to a particular IP address.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ip" + ] + }, + "value": { + "type": "string", + "format": "ip" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "Forward traffic to a VPC", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "vpc" + ] + }, + "value": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "Forward traffic to a VPC Subnet", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "subnet" + ] + }, + "value": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "Forward traffic to a specific instance", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "instance" + ] + }, + "value": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "Forward traffic to an internet gateway", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "internet_gateway" + ] + }, + "value": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "Drop matching traffic", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "drop" + ] + } + }, + "required": [ + "type" + ] + } + ] + }, + "RouterRoute": { + "description": "A route defines a rule that governs where traffic should be sent based on its destination.", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "destination": { + "description": "Selects which traffic this routing rule will apply to", + "allOf": [ + { + "$ref": "#/components/schemas/RouteDestination" + } + ] + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "kind": { + "description": "Describes the kind of router. Set at creation. `read-only`", + "allOf": [ + { + "$ref": "#/components/schemas/RouterRouteKind" + } + ] + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "target": { + "description": "The location that matched packets should be forwarded to", + "allOf": [ + { + "$ref": "#/components/schemas/RouteTarget" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + }, + "vpc_router_id": { + "description": "The ID of the VPC Router to which the route belongs", + "type": "string", + "format": "uuid" + } + }, + "required": [ + "description", + "destination", + "id", + "kind", + "name", + "target", + "time_created", + "time_modified", + "vpc_router_id" + ] + }, + "RouterRouteCreate": { + "description": "Create-time parameters for a `RouterRoute`", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "destination": { + "description": "Selects which traffic this routing rule will apply to.", + "allOf": [ + { + "$ref": "#/components/schemas/RouteDestination" + } + ] + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "target": { + "description": "The location that matched packets should be forwarded to.", + "allOf": [ + { + "$ref": "#/components/schemas/RouteTarget" + } + ] + } + }, + "required": [ + "description", + "destination", + "name", + "target" + ] + }, + "RouterRouteKind": { + "description": "The kind of a `RouterRoute`\n\nThe kind determines certain attributes such as if the route is modifiable and describes how or where the route was created.", + "oneOf": [ + { + "description": "Determines the default destination of traffic, such as whether it goes to the internet or not.\n\n`Destination: An Internet Gateway` `Modifiable: true`", + "type": "string", + "enum": [ + "default" + ] + }, + { + "description": "Automatically added for each VPC Subnet in the VPC\n\n`Destination: A VPC Subnet` `Modifiable: false`", + "type": "string", + "enum": [ + "vpc_subnet" + ] + }, + { + "description": "Automatically added when VPC peering is established\n\n`Destination: A different VPC` `Modifiable: false`", + "type": "string", + "enum": [ + "vpc_peering" + ] + }, + { + "description": "Created by a user; see `RouteTarget`\n\n`Destination: User defined` `Modifiable: true`", + "type": "string", + "enum": [ + "custom" + ] + } + ] + }, + "RouterRouteResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/RouterRoute" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "RouterRouteUpdate": { + "description": "Updateable properties of a `RouterRoute`", + "type": "object", + "properties": { + "description": { + "nullable": true, + "type": "string" + }, + "destination": { + "description": "Selects which traffic this routing rule will apply to.", + "allOf": [ + { + "$ref": "#/components/schemas/RouteDestination" + } + ] + }, + "name": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "target": { + "description": "The location that matched packets should be forwarded to.", + "allOf": [ + { + "$ref": "#/components/schemas/RouteTarget" + } + ] + } + }, + "required": [ + "destination", + "target" + ] + }, + "SamlIdentityProvider": { + "description": "Identity-related metadata that's included in nearly all public API objects", + "type": "object", + "properties": { + "acs_url": { + "description": "Service provider endpoint where the response will be sent", + "type": "string" + }, + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "group_attribute_name": { + "nullable": true, + "description": "If set, attributes with this name will be considered to denote a user's group membership, where the values will be the group names.", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "idp_entity_id": { + "description": "IdP's entity id", + "type": "string" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "public_cert": { + "nullable": true, + "description": "Optional request signing public certificate (base64 encoded der file)", + "type": "string" + }, + "slo_url": { + "description": "Service provider endpoint where the idp should send log out requests", + "type": "string" + }, + "sp_client_id": { + "description": "SP's client id", + "type": "string" + }, + "technical_contact_email": { + "description": "Customer's technical contact for saml configuration", + "type": "string" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "acs_url", + "description", + "id", + "idp_entity_id", + "name", + "slo_url", + "sp_client_id", + "technical_contact_email", + "time_created", + "time_modified" + ] + }, + "SamlIdentityProviderCreate": { + "description": "Create-time identity-related parameters", + "type": "object", + "properties": { + "acs_url": { + "description": "service provider endpoint where the response will be sent", + "type": "string" + }, + "description": { + "type": "string" + }, + "group_attribute_name": { + "nullable": true, + "description": "If set, SAML attributes with this name will be considered to denote a user's group membership, where the attribute value(s) should be a comma-separated list of group names.", + "type": "string" + }, + "idp_entity_id": { + "description": "idp's entity id", + "type": "string" + }, + "idp_metadata_source": { + "description": "the source of an identity provider metadata descriptor", + "allOf": [ + { + "$ref": "#/components/schemas/IdpMetadataSource" + } + ] + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "signing_keypair": { + "nullable": true, + "description": "request signing key pair", + "default": null, + "allOf": [ + { + "$ref": "#/components/schemas/DerEncodedKeyPair" + } + ] + }, + "slo_url": { + "description": "service provider endpoint where the idp should send log out requests", + "type": "string" + }, + "sp_client_id": { + "description": "sp's client id", + "type": "string" + }, + "technical_contact_email": { + "description": "customer's technical contact for saml configuration", + "type": "string" + } + }, + "required": [ + "acs_url", + "description", + "idp_entity_id", + "idp_metadata_source", + "name", + "slo_url", + "sp_client_id", + "technical_contact_email" + ] + }, + "ScimClientBearerToken": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "time_created": { + "type": "string", + "format": "date-time" + }, + "time_expires": { + "nullable": true, + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "time_created" + ] + }, + "ScimClientBearerTokenValue": { + "description": "The POST response is the only time the generated bearer token is returned to the client.", + "type": "object", + "properties": { + "bearer_token": { + "type": "string" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "time_created": { + "type": "string", + "format": "date-time" + }, + "time_expires": { + "nullable": true, + "type": "string", + "format": "date-time" + } + }, + "required": [ + "bearer_token", + "id", + "time_created" + ] + }, + "ServiceIcmpConfig": { + "description": "Configuration of inbound ICMP allowed by API services.", + "type": "object", + "properties": { + "enabled": { + "description": "When enabled, Nexus is able to receive ICMP Destination Unreachable type 3 (port unreachable) and type 4 (fragmentation needed), Redirect, and Time Exceeded messages. These enable Nexus to perform Path MTU discovery and better cope with fragmentation issues. Otherwise all inbound ICMP traffic will be dropped.", + "type": "boolean" + } + }, + "required": [ + "enabled" + ] + }, + "ServiceUsingCertificate": { + "description": "The service intended to use this certificate.", + "oneOf": [ + { + "description": "This certificate is intended for access to the external API.", + "type": "string", + "enum": [ + "external_api" + ] + } + ] + }, + "SetTargetReleaseParams": { + "description": "Parameters for PUT requests to `/v1/system/update/target-release`.", + "type": "object", + "properties": { + "system_version": { + "description": "Version of the system software to make the target release.", + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + } + }, + "required": [ + "system_version" + ] + }, + "Silo": { + "description": "View of a Silo\n\nA Silo is the highest level unit of isolation.", + "type": "object", + "properties": { + "admin_group_name": { + "nullable": true, + "description": "Optionally, silos can have a group name that is automatically granted the silo admin role.", + "type": "string" + }, + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "discoverable": { + "description": "A silo where discoverable is false can be retrieved only by its id - it will not be part of the \"list all silos\" output.", + "type": "boolean" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "identity_mode": { + "description": "How users and groups are managed in this Silo", + "allOf": [ + { + "$ref": "#/components/schemas/SiloIdentityMode" + } + ] + }, + "mapped_fleet_roles": { + "description": "Mapping of which Fleet roles are conferred by each Silo role\n\nThe default is that no Fleet roles are conferred by any Silo roles unless there's a corresponding entry in this map.", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FleetRole" + }, + "uniqueItems": true + } + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "discoverable", + "id", + "identity_mode", + "mapped_fleet_roles", + "name", + "time_created", + "time_modified" + ] + }, + "SiloAuthSettings": { + "description": "View of silo authentication settings", + "type": "object", + "properties": { + "device_token_max_ttl_seconds": { + "nullable": true, + "description": "Maximum lifetime of a device token in seconds. If set to null, users will be able to create tokens that do not expire.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "silo_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "silo_id" + ] + }, + "SiloAuthSettingsUpdate": { + "description": "Updateable properties of a silo's settings.", + "type": "object", + "properties": { + "device_token_max_ttl_seconds": { + "nullable": true, + "description": "Maximum lifetime of a device token in seconds. If set to null, users will be able to create tokens that do not expire.", + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + "required": [ + "device_token_max_ttl_seconds" + ] + }, + "SiloCreate": { + "description": "Create-time parameters for a `Silo`", + "type": "object", + "properties": { + "admin_group_name": { + "nullable": true, + "description": "If set, this group will be created during Silo creation and granted the \"Silo Admin\" role. Identity providers can assert that users belong to this group and those users can log in and further initialize the Silo.\n\nNote that if configuring a SAML based identity provider, group_attribute_name must be set for users to be considered part of a group. See `SamlIdentityProviderCreate` for more information.", + "type": "string" + }, + "description": { + "type": "string" + }, + "discoverable": { + "type": "boolean" + }, + "identity_mode": { + "$ref": "#/components/schemas/SiloIdentityMode" + }, + "mapped_fleet_roles": { + "description": "Mapping of which Fleet roles are conferred by each Silo role\n\nThe default is that no Fleet roles are conferred by any Silo roles unless there's a corresponding entry in this map.", + "default": {}, + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FleetRole" + }, + "uniqueItems": true + } + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "quotas": { + "description": "Limits the amount of provisionable CPU, memory, and storage in the Silo. CPU and memory are only consumed by running instances, while storage is consumed by any disk or snapshot. A value of 0 means that resource is *not* provisionable.", + "allOf": [ + { + "$ref": "#/components/schemas/SiloQuotasCreate" + } + ] + }, + "tls_certificates": { + "description": "Initial TLS certificates to be used for the new Silo's console and API endpoints. These should be valid for the Silo's DNS name(s).", + "type": "array", + "items": { + "$ref": "#/components/schemas/CertificateCreate" + } + } + }, + "required": [ + "description", + "discoverable", + "identity_mode", + "name", + "quotas", + "tls_certificates" + ] + }, + "SiloIdentityMode": { + "description": "Describes how identities are managed and users are authenticated in this Silo", + "oneOf": [ + { + "description": "Users are authenticated with SAML using an external authentication provider. The system updates information about users and groups only during successful authentication (i.e,. \"JIT provisioning\" of users and groups).", + "type": "string", + "enum": [ + "saml_jit" + ] + }, + { + "description": "The system is the source of truth about users. There is no linkage to an external authentication provider or identity provider.", + "type": "string", + "enum": [ + "local_only" + ] + }, + { + "description": "Users are authenticated with SAML using an external authentication provider. Users and groups are managed with SCIM API calls, likely from the same authentication provider.", + "type": "string", + "enum": [ + "saml_scim" + ] + } + ] + }, + "SiloIpPool": { + "description": "An IP pool in the context of a silo", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "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. There can be at most one default for a given silo.", + "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", + "is_default", + "name", + "time_created", + "time_modified" + ] + }, + "SiloIpPoolResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/SiloIpPool" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "SiloQuotas": { + "description": "A collection of resource counts used to set the virtual capacity of a silo", + "type": "object", + "properties": { + "cpus": { + "description": "Number of virtual CPUs", + "type": "integer", + "format": "int64" + }, + "memory": { + "description": "Amount of memory in bytes", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + }, + "silo_id": { + "type": "string", + "format": "uuid" + }, + "storage": { + "description": "Amount of disk storage in bytes", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + } + }, + "required": [ + "cpus", + "memory", + "silo_id", + "storage" + ] + }, + "SiloQuotasCreate": { + "description": "The amount of provisionable resources for a Silo", + "type": "object", + "properties": { + "cpus": { + "description": "The amount of virtual CPUs available for running instances in the Silo", + "type": "integer", + "format": "int64" + }, + "memory": { + "description": "The amount of RAM (in bytes) available for running instances in the Silo", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + }, + "storage": { + "description": "The amount of storage (in bytes) available for disks or snapshots", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + } + }, + "required": [ + "cpus", + "memory", + "storage" + ] + }, + "SiloQuotasResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/SiloQuotas" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "SiloQuotasUpdate": { + "description": "Updateable properties of a Silo's resource limits. If a value is omitted it will not be updated.", + "type": "object", + "properties": { + "cpus": { + "nullable": true, + "description": "The amount of virtual CPUs available for running instances in the Silo", + "type": "integer", + "format": "int64" + }, + "memory": { + "nullable": true, + "description": "The amount of RAM (in bytes) available for running instances in the Silo", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + }, + "storage": { + "nullable": true, + "description": "The amount of storage (in bytes) available for disks or snapshots", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + } + } + }, + "SiloResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Silo" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "SiloRole": { + "type": "string", + "enum": [ + "admin", + "collaborator", + "limited_collaborator", + "viewer" + ] + }, + "SiloRolePolicy": { + "description": "Policy for a particular resource\n\nNote that the Policy only describes access granted explicitly for this resource. The policies of parent resources can also cause a user to have access to this resource.", + "type": "object", + "properties": { + "role_assignments": { + "description": "Roles directly assigned on this resource", + "type": "array", + "items": { + "$ref": "#/components/schemas/SiloRoleRoleAssignment" + } + } + }, + "required": [ + "role_assignments" + ] + }, + "SiloRoleRoleAssignment": { + "description": "Describes the assignment of a particular role on a particular resource to a particular identity (user, group, etc.)\n\nThe resource is not part of this structure. Rather, `RoleAssignment`s are put into a `Policy` and that Policy is applied to a particular resource.", + "type": "object", + "properties": { + "identity_id": { + "type": "string", + "format": "uuid" + }, + "identity_type": { + "$ref": "#/components/schemas/IdentityType" + }, + "role_name": { + "$ref": "#/components/schemas/SiloRole" + } + }, + "required": [ + "identity_id", + "identity_type", + "role_name" + ] + }, + "SiloUtilization": { + "description": "View of a silo's resource utilization and capacity", + "type": "object", + "properties": { + "allocated": { + "description": "Accounts for the total amount of resources reserved for silos via their quotas", + "allOf": [ + { + "$ref": "#/components/schemas/VirtualResourceCounts" + } + ] + }, + "provisioned": { + "description": "Accounts for resources allocated by in silos like CPU or memory for running instances and storage for disks and snapshots Note that CPU and memory resources associated with a stopped instances are not counted here", + "allOf": [ + { + "$ref": "#/components/schemas/VirtualResourceCounts" + } + ] + }, + "silo_id": { + "type": "string", + "format": "uuid" + }, + "silo_name": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "allocated", + "provisioned", + "silo_id", + "silo_name" + ] + }, + "SiloUtilizationResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/SiloUtilization" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "Sled": { + "description": "An operator's view of a Sled.", + "type": "object", + "properties": { + "baseboard": { + "$ref": "#/components/schemas/Baseboard" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "policy": { + "description": "The operator-defined policy of a sled.", + "allOf": [ + { + "$ref": "#/components/schemas/SledPolicy" + } + ] + }, + "rack_id": { + "description": "The rack to which this Sled is currently attached", + "type": "string", + "format": "uuid" + }, + "state": { + "description": "The current state of the sled.", + "allOf": [ + { + "$ref": "#/components/schemas/SledState" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + }, + "usable_hardware_threads": { + "description": "The number of hardware threads which can execute on this sled", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "usable_physical_ram": { + "description": "Amount of RAM which may be used by the Sled's OS", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + } + }, + "required": [ + "baseboard", + "id", + "policy", + "rack_id", + "state", + "time_created", + "time_modified", + "usable_hardware_threads", + "usable_physical_ram" + ] + }, + "SledId": { + "description": "The unique ID of a sled.", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "id" + ] + }, + "SledInstance": { + "description": "An operator's view of an instance running on a given sled", + "type": "object", + "properties": { + "active_sled_id": { + "type": "string", + "format": "uuid" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "memory": { + "type": "integer", + "format": "int64" + }, + "migration_id": { + "nullable": true, + "type": "string", + "format": "uuid" + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "ncpus": { + "type": "integer", + "format": "int64" + }, + "project_name": { + "$ref": "#/components/schemas/Name" + }, + "silo_name": { + "$ref": "#/components/schemas/Name" + }, + "state": { + "$ref": "#/components/schemas/InstanceState" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "active_sled_id", + "id", + "memory", + "name", + "ncpus", + "project_name", + "silo_name", + "state", + "time_created", + "time_modified" + ] + }, + "SledInstanceResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/SledInstance" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "SledPolicy": { + "description": "The operator-defined policy of a sled.", + "oneOf": [ + { + "description": "The operator has indicated that the sled is in-service.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "in_service" + ] + }, + "provision_policy": { + "description": "Determines whether new resources can be provisioned onto the sled.", + "allOf": [ + { + "$ref": "#/components/schemas/SledProvisionPolicy" + } + ] + } + }, + "required": [ + "kind", + "provision_policy" + ] + }, + { + "description": "The operator has indicated that the sled has been permanently removed from service.\n\nThis is a terminal state: once a particular sled ID is expunged, it will never return to service. (The actual hardware may be reused, but it will be treated as a brand-new sled.)\n\nAn expunged sled is always non-provisionable.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "expunged" + ] + } + }, + "required": [ + "kind" + ] + } + ] + }, + "SledProvisionPolicy": { + "description": "The operator-defined provision policy of a sled.\n\nThis controls whether new resources are going to be provisioned on this sled.", + "oneOf": [ + { + "description": "New resources will be provisioned on this sled.", + "type": "string", + "enum": [ + "provisionable" + ] + }, + { + "description": "New resources will not be provisioned on this sled. However, if the sled is currently in service, existing resources will continue to be on this sled unless manually migrated off.", + "type": "string", + "enum": [ + "non_provisionable" + ] + } + ] + }, + "SledProvisionPolicyParams": { + "description": "Parameters for `sled_set_provision_policy`.", + "type": "object", + "properties": { + "state": { + "description": "The provision state.", + "allOf": [ + { + "$ref": "#/components/schemas/SledProvisionPolicy" + } + ] + } + }, + "required": [ + "state" + ] + }, + "SledProvisionPolicyResponse": { + "description": "Response to `sled_set_provision_policy`.", + "type": "object", + "properties": { + "new_state": { + "description": "The new provision state.", + "allOf": [ + { + "$ref": "#/components/schemas/SledProvisionPolicy" + } + ] + }, + "old_state": { + "description": "The old provision state.", + "allOf": [ + { + "$ref": "#/components/schemas/SledProvisionPolicy" + } + ] + } + }, + "required": [ + "new_state", + "old_state" + ] + }, + "SledResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Sled" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "SledState": { + "description": "The current state of the sled.", + "oneOf": [ + { + "description": "The sled is currently active, and has resources allocated on it.", + "type": "string", + "enum": [ + "active" + ] + }, + { + "description": "The sled has been permanently removed from service.\n\nThis is a terminal state: once a particular sled ID is decommissioned, it will never return to service. (The actual hardware may be reused, but it will be treated as a brand-new sled.)", + "type": "string", + "enum": [ + "decommissioned" + ] + } + ] + }, + "Snapshot": { + "description": "View of a Snapshot", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "disk_id": { + "type": "string", + "format": "uuid" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "project_id": { + "type": "string", + "format": "uuid" + }, + "size": { + "$ref": "#/components/schemas/ByteCount" + }, + "state": { + "$ref": "#/components/schemas/SnapshotState" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "disk_id", + "id", + "name", + "project_id", + "size", + "state", + "time_created", + "time_modified" + ] + }, + "SnapshotCreate": { + "description": "Create-time parameters for a `Snapshot`", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "disk": { + "description": "The disk to be snapshotted", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + }, + "name": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "description", + "disk", + "name" + ] + }, + "SnapshotResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Snapshot" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "SnapshotState": { + "type": "string", + "enum": [ + "creating", + "ready", + "faulted", + "destroyed" + ] + }, + "SshKey": { + "description": "View of an SSH Key", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "public_key": { + "description": "SSH public key, e.g., `\"ssh-ed25519 AAAAC3NzaC...\"`", + "type": "string" + }, + "silo_user_id": { + "description": "The user to whom this key belongs", + "type": "string", + "format": "uuid" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "name", + "public_key", + "silo_user_id", + "time_created", + "time_modified" + ] + }, + "SshKeyCreate": { + "description": "Create-time parameters for an `SshKey`", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "public_key": { + "description": "SSH public key, e.g., `\"ssh-ed25519 AAAAC3NzaC...\"`", + "type": "string" + } + }, + "required": [ + "description", + "name", + "public_key" + ] + }, + "SshKeyResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/SshKey" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "SupportBundleCreate": { + "type": "object", + "properties": { + "user_comment": { + "nullable": true, + "description": "User comment for the support bundle", + "type": "string" + } + } + }, + "SupportBundleInfo": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "reason_for_creation": { + "type": "string" + }, + "reason_for_failure": { + "nullable": true, + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/SupportBundleState" + }, + "time_created": { + "type": "string", + "format": "date-time" + }, + "user_comment": { + "nullable": true, + "type": "string" + } + }, + "required": [ + "id", + "reason_for_creation", + "state", + "time_created" + ] + }, + "SupportBundleInfoResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/SupportBundleInfo" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "SupportBundleState": { + "oneOf": [ + { + "description": "Support Bundle still actively being collected.\n\nThis is the initial state for a Support Bundle, and it will automatically transition to either \"Failing\" or \"Active\".\n\nIf a user no longer wants to access a Support Bundle, they can request cancellation, which will transition to the \"Destroying\" state.", + "type": "string", + "enum": [ + "collecting" + ] + }, + { + "description": "Support Bundle is being destroyed.\n\nOnce backing storage has been freed, this bundle is destroyed.", + "type": "string", + "enum": [ + "destroying" + ] + }, + { + "description": "Support Bundle was not created successfully, or was created and has lost backing storage.\n\nThe record of the bundle still exists for readability, but the only valid operation on these bundles is to destroy them.", + "type": "string", + "enum": [ + "failed" + ] + }, + { + "description": "Support Bundle has been processed, and is ready for usage.", + "type": "string", + "enum": [ + "active" + ] + } + ] + }, + "SupportBundleUpdate": { + "type": "object", + "properties": { + "user_comment": { + "nullable": true, + "description": "User comment for the support bundle", + "type": "string" + } + } + }, + "Switch": { + "description": "An operator's view of a Switch.", + "type": "object", + "properties": { + "baseboard": { + "$ref": "#/components/schemas/Baseboard" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "rack_id": { + "description": "The rack to which this Switch is currently attached", + "type": "string", + "format": "uuid" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "baseboard", + "id", + "rack_id", + "time_created", + "time_modified" + ] + }, + "SwitchBgpHistory": { + "description": "BGP message history for a particular switch.", + "type": "object", + "properties": { + "history": { + "description": "Message history indexed by peer address.", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/BgpMessageHistory" + } + }, + "switch": { + "description": "Switch this message history is associated with.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchLocation" + } + ] + } + }, + "required": [ + "history", + "switch" + ] + }, + "SwitchInterfaceConfig": { + "description": "A switch port interface configuration for a port settings object.", + "type": "object", + "properties": { + "id": { + "description": "A unique identifier for this switch interface.", + "type": "string", + "format": "uuid" + }, + "interface_name": { + "description": "The name of this switch interface.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "kind": { + "description": "The switch interface kind.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchInterfaceKind2" + } + ] + }, + "port_settings_id": { + "description": "The port settings object this switch interface configuration belongs to.", + "type": "string", + "format": "uuid" + }, + "v6_enabled": { + "description": "Whether or not IPv6 is enabled on this interface.", + "type": "boolean" + } + }, + "required": [ + "id", + "interface_name", + "kind", + "port_settings_id", + "v6_enabled" + ] + }, + "SwitchInterfaceConfigCreate": { + "description": "A layer-3 switch interface configuration. When IPv6 is enabled, a link local address will be created for the interface.", + "type": "object", + "properties": { + "kind": { + "description": "What kind of switch interface this configuration represents.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchInterfaceKind" + } + ] + }, + "link_name": { + "description": "Link name. On ports that are not broken out, this is always phy0. On a 2x breakout the options are phy0 and phy1, on 4x phy0-phy3, etc.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "v6_enabled": { + "description": "Whether or not IPv6 is enabled.", + "type": "boolean" + } + }, + "required": [ + "kind", + "link_name", + "v6_enabled" + ] + }, + "SwitchInterfaceKind": { + "description": "Indicates the kind for a switch interface.", + "oneOf": [ + { + "description": "Primary interfaces are associated with physical links. There is exactly one primary interface per physical link.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "primary" + ] + } + }, + "required": [ + "type" + ] + }, + { + "description": "VLAN interfaces allow physical interfaces to be multiplexed onto multiple logical links, each distinguished by a 12-bit 802.1Q Ethernet tag.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "vlan" + ] + }, + "vid": { + "description": "The virtual network id (VID) that distinguishes this interface and is used for producing and consuming 802.1Q Ethernet tags. This field has a maximum value of 4095 as 802.1Q tags are twelve bits.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "type", + "vid" + ] + }, + { + "description": "Loopback interfaces are anchors for IP addresses that are not specific to any particular port.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "loopback" + ] + } + }, + "required": [ + "type" + ] + } + ] + }, + "SwitchInterfaceKind2": { + "description": "Describes the kind of an switch interface.", + "oneOf": [ + { + "description": "Primary interfaces are associated with physical links. There is exactly one primary interface per physical link.", + "type": "string", + "enum": [ + "primary" + ] + }, + { + "description": "VLAN interfaces allow physical interfaces to be multiplexed onto multiple logical links, each distinguished by a 12-bit 802.1Q Ethernet tag.", + "type": "string", + "enum": [ + "vlan" + ] + }, + { + "description": "Loopback interfaces are anchors for IP addresses that are not specific to any particular port.", + "type": "string", + "enum": [ + "loopback" + ] + } + ] + }, + "SwitchLinkState": {}, + "SwitchLocation": { + "description": "Identifies switch physical location", + "oneOf": [ + { + "description": "Switch in upper slot", + "type": "string", + "enum": [ + "switch0" + ] + }, + { + "description": "Switch in lower slot", + "type": "string", + "enum": [ + "switch1" + ] + } + ] + }, + "SwitchPort": { + "description": "A switch port represents a physical external port on a rack switch.", + "type": "object", + "properties": { + "id": { + "description": "The id of the switch port.", + "type": "string", + "format": "uuid" + }, + "port_name": { + "description": "The name of this switch port.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "port_settings_id": { + "nullable": true, + "description": "The primary settings group of this switch port. Will be `None` until this switch port is configured.", + "type": "string", + "format": "uuid" + }, + "rack_id": { + "description": "The rack this switch port belongs to.", + "type": "string", + "format": "uuid" + }, + "switch_location": { + "description": "The switch location of this switch port.", + "type": "string" + } + }, + "required": [ + "id", + "port_name", + "rack_id", + "switch_location" + ] + }, + "SwitchPortAddressView": { + "description": "An IP address configuration for a port settings object.", + "type": "object", + "properties": { + "address": { + "description": "The IP address and prefix.", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + }, + "address_lot_block_id": { + "description": "The id of the address lot block this address is drawn from.", + "type": "string", + "format": "uuid" + }, + "address_lot_id": { + "description": "The id of the address lot this address is drawn from.", + "type": "string", + "format": "uuid" + }, + "address_lot_name": { + "description": "The name of the address lot this address is drawn from.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "interface_name": { + "description": "The interface name this address belongs to.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "port_settings_id": { + "description": "The port settings object this address configuration belongs to.", + "type": "string", + "format": "uuid" + }, + "vlan_id": { + "nullable": true, + "description": "An optional VLAN ID", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "address", + "address_lot_block_id", + "address_lot_id", + "address_lot_name", + "interface_name", + "port_settings_id" + ] + }, + "SwitchPortApplySettings": { + "description": "Parameters for applying settings to switch ports.", + "type": "object", + "properties": { + "port_settings": { + "description": "A name or id to use when applying switch port settings.", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + } + }, + "required": [ + "port_settings" + ] + }, + "SwitchPortConfig": { + "description": "A physical port configuration for a port settings object.", + "type": "object", + "properties": { + "geometry": { + "description": "The physical link geometry of the port.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchPortGeometry2" + } + ] + }, + "port_settings_id": { + "description": "The id of the port settings object this configuration belongs to.", + "type": "string", + "format": "uuid" + } + }, + "required": [ + "geometry", + "port_settings_id" + ] + }, + "SwitchPortConfigCreate": { + "description": "Physical switch port configuration.", + "type": "object", + "properties": { + "geometry": { + "description": "Link geometry for the switch port.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchPortGeometry" + } + ] + } + }, + "required": [ + "geometry" + ] + }, + "SwitchPortGeometry": { + "description": "The link geometry associated with a switch port.", + "oneOf": [ + { + "description": "The port contains a single QSFP28 link with four lanes.", + "type": "string", + "enum": [ + "qsfp28x1" + ] + }, + { + "description": "The port contains two QSFP28 links each with two lanes.", + "type": "string", + "enum": [ + "qsfp28x2" + ] + }, + { + "description": "The port contains four SFP28 links each with one lane.", + "type": "string", + "enum": [ + "sfp28x4" + ] + } + ] + }, + "SwitchPortGeometry2": { + "description": "The link geometry associated with a switch port.", + "oneOf": [ + { + "description": "The port contains a single QSFP28 link with four lanes.", + "type": "string", + "enum": [ + "qsfp28x1" + ] + }, + { + "description": "The port contains two QSFP28 links each with two lanes.", + "type": "string", + "enum": [ + "qsfp28x2" + ] + }, + { + "description": "The port contains four SFP28 links each with one lane.", + "type": "string", + "enum": [ + "sfp28x4" + ] + } + ] + }, + "SwitchPortLinkConfig": { + "description": "A link configuration for a port settings object.", + "type": "object", + "properties": { + "autoneg": { + "description": "Whether or not the link has autonegotiation enabled.", + "type": "boolean" + }, + "fec": { + "nullable": true, + "description": "The requested forward-error correction method. If this is not specified, the standard FEC for the underlying media will be applied if it can be determined.", + "allOf": [ + { + "$ref": "#/components/schemas/LinkFec" + } + ] + }, + "link_name": { + "description": "The name of this link.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "lldp_link_config": { + "nullable": true, + "description": "The link-layer discovery protocol service configuration for this link.", + "allOf": [ + { + "$ref": "#/components/schemas/LldpLinkConfig" + } + ] + }, + "mtu": { + "description": "The maximum transmission unit for this link.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "port_settings_id": { + "description": "The port settings this link configuration belongs to.", + "type": "string", + "format": "uuid" + }, + "speed": { + "description": "The configured speed of the link.", + "allOf": [ + { + "$ref": "#/components/schemas/LinkSpeed" + } + ] + }, + "tx_eq_config": { + "nullable": true, + "description": "The tx_eq configuration for this link.", + "allOf": [ + { + "$ref": "#/components/schemas/TxEqConfig2" + } + ] + } + }, + "required": [ + "autoneg", + "link_name", + "mtu", + "port_settings_id", + "speed" + ] + }, + "SwitchPortResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/SwitchPort" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "SwitchPortRouteConfig": { + "description": "A route configuration for a port settings object.", + "type": "object", + "properties": { + "dst": { + "description": "The route's destination network.", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + }, + "gw": { + "description": "The route's gateway address.", + "type": "string", + "format": "ip" + }, + "interface_name": { + "description": "The interface name this route configuration is assigned to.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "port_settings_id": { + "description": "The port settings object this route configuration belongs to.", + "type": "string", + "format": "uuid" + }, + "rib_priority": { + "nullable": true, + "description": "Route RIB priority. Higher priority indicates precedence within and across protocols.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "vlan_id": { + "nullable": true, + "description": "The VLAN identifier for the route. Use this if the gateway is reachable over an 802.1Q tagged L2 segment.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "dst", + "gw", + "interface_name", + "port_settings_id" + ] + }, + "SwitchPortSettings": { + "description": "This structure contains all port settings information in one place. It's a convenience data structure for getting a complete view of a particular port's settings.", + "type": "object", + "properties": { + "addresses": { + "description": "Layer 3 IP address settings.", + "type": "array", + "items": { + "$ref": "#/components/schemas/SwitchPortAddressView" + } + }, + "bgp_peers": { + "description": "BGP peer settings.", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpPeer" + } + }, + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "groups": { + "description": "Switch port settings included from other switch port settings groups.", + "type": "array", + "items": { + "$ref": "#/components/schemas/SwitchPortSettingsGroups" + } + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "interfaces": { + "description": "Layer 3 interface settings.", + "type": "array", + "items": { + "$ref": "#/components/schemas/SwitchInterfaceConfig" + } + }, + "links": { + "description": "Layer 2 link settings.", + "type": "array", + "items": { + "$ref": "#/components/schemas/SwitchPortLinkConfig" + } + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "port": { + "description": "Layer 1 physical port settings.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchPortConfig" + } + ] + }, + "routes": { + "description": "IP route settings.", + "type": "array", + "items": { + "$ref": "#/components/schemas/SwitchPortRouteConfig" + } + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + }, + "vlan_interfaces": { + "description": "Vlan interface settings.", + "type": "array", + "items": { + "$ref": "#/components/schemas/SwitchVlanInterfaceConfig" + } + } + }, + "required": [ + "addresses", + "bgp_peers", + "description", + "groups", + "id", + "interfaces", + "links", + "name", + "port", + "routes", + "time_created", + "time_modified", + "vlan_interfaces" + ] + }, + "SwitchPortSettingsCreate": { + "description": "Parameters for creating switch port settings. Switch port settings are the central data structure for setting up external networking. Switch port settings include link, interface, route, address and dynamic network protocol configuration.", + "type": "object", + "properties": { + "addresses": { + "description": "Address configurations.", + "type": "array", + "items": { + "$ref": "#/components/schemas/AddressConfig" + } + }, + "bgp_peers": { + "description": "BGP peer configurations.", + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpPeerConfig" + } + }, + "description": { + "type": "string" + }, + "groups": { + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/NameOrId" + } + }, + "interfaces": { + "description": "Interface configurations.", + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/SwitchInterfaceConfigCreate" + } + }, + "links": { + "description": "Link configurations.", + "type": "array", + "items": { + "$ref": "#/components/schemas/LinkConfigCreate" + } + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "port_config": { + "$ref": "#/components/schemas/SwitchPortConfigCreate" + }, + "routes": { + "description": "Route configurations.", + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/RouteConfig" + } + } + }, + "required": [ + "addresses", + "description", + "links", + "name", + "port_config" + ] + }, + "SwitchPortSettingsGroups": { + "description": "This structure maps a port settings object to a port settings groups. Port settings objects may inherit settings from groups. This mapping defines the relationship between settings objects and the groups they reference.", + "type": "object", + "properties": { + "port_settings_group_id": { + "description": "The id of a port settings group being referenced by a port settings object.", + "type": "string", + "format": "uuid" + }, + "port_settings_id": { + "description": "The id of a port settings object referencing a port settings group.", + "type": "string", + "format": "uuid" + } + }, + "required": [ + "port_settings_group_id", + "port_settings_id" + ] + }, + "SwitchPortSettingsIdentity": { + "description": "A switch port settings identity whose id may be used to view additional details.", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "name", + "time_created", + "time_modified" + ] + }, + "SwitchPortSettingsIdentityResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/SwitchPortSettingsIdentity" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "SwitchResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Switch" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "SwitchVlanInterfaceConfig": { + "description": "A switch port VLAN interface configuration for a port settings object.", + "type": "object", + "properties": { + "interface_config_id": { + "description": "The switch interface configuration this VLAN interface configuration belongs to.", + "type": "string", + "format": "uuid" + }, + "vlan_id": { + "description": "The virtual network id for this interface that is used for producing and consuming 802.1Q Ethernet tags. This field has a maximum value of 4095 as 802.1Q tags are twelve bits.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "interface_config_id", + "vlan_id" + ] + }, + "TargetRelease": { + "description": "View of a system software target release", + "type": "object", + "properties": { + "time_requested": { + "description": "Time this was set as the target release", + "type": "string", + "format": "date-time" + }, + "version": { + "description": "The specified release of the rack's system software", + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + } + }, + "required": [ + "time_requested", + "version" + ] + }, + "Timeseries": { + "description": "A timeseries contains a timestamped set of values from one source.\n\nThis includes the typed key-value pairs that uniquely identify it, and the set of timestamps and data values from it.", + "type": "object", + "properties": { + "fields": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/FieldValue" + } + }, + "points": { + "$ref": "#/components/schemas/Points" + } + }, + "required": [ + "fields", + "points" + ] + }, + "TimeseriesDescription": { + "description": "Text descriptions for the target and metric of a timeseries.", + "type": "object", + "properties": { + "metric": { + "type": "string" + }, + "target": { + "type": "string" + } + }, + "required": [ + "metric", + "target" + ] + }, + "TimeseriesName": { + "title": "The name of a timeseries", + "description": "Names are constructed by concatenating the target and metric names with ':'. Target and metric names must be lowercase alphanumeric characters with '_' separating words.", + "type": "string", + "pattern": "^(([a-z]+[a-z0-9]*)(_([a-z0-9]+))*):(([a-z]+[a-z0-9]*)(_([a-z0-9]+))*)$" + }, + "TimeseriesQuery": { + "description": "A timeseries query string, written in the Oximeter query language.", + "type": "object", + "properties": { + "query": { + "description": "A timeseries query string, written in the Oximeter query language.", + "type": "string" + } + }, + "required": [ + "query" + ] + }, + "TimeseriesSchema": { + "description": "The schema for a timeseries.\n\nThis includes the name of the timeseries, as well as the datum type of its metric and the schema for each field.", + "type": "object", + "properties": { + "authz_scope": { + "$ref": "#/components/schemas/AuthzScope" + }, + "created": { + "type": "string", + "format": "date-time" + }, + "datum_type": { + "$ref": "#/components/schemas/DatumType" + }, + "description": { + "$ref": "#/components/schemas/TimeseriesDescription" + }, + "field_schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FieldSchema" + }, + "uniqueItems": true + }, + "timeseries_name": { + "$ref": "#/components/schemas/TimeseriesName" + }, + "units": { + "$ref": "#/components/schemas/Units" + }, + "version": { + "type": "integer", + "format": "uint8", + "minimum": 1 + } + }, + "required": [ + "authz_scope", + "created", + "datum_type", + "description", + "field_schema", + "timeseries_name", + "units", + "version" + ] + }, + "TimeseriesSchemaResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeseriesSchema" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "TufRepo": { + "description": "Metadata about a TUF repository", + "type": "object", + "properties": { + "file_name": { + "description": "The file name of the repository, as reported by the client that uploaded it\n\nThis is intended for debugging. The file name may not match any particular pattern, and even if it does, it may not be accurate since it's just what the client reported.", + "type": "string" + }, + "hash": { + "description": "The hash of the repository", + "type": "string", + "format": "hex string (32 bytes)" + }, + "system_version": { + "description": "The system version for this repository\n\nThe system version is a top-level version number applied to all the software in the repository.", + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + }, + "time_created": { + "description": "Time the repository was uploaded", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "file_name", + "hash", + "system_version", + "time_created" + ] + }, + "TufRepoResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/TufRepo" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "TufRepoUpload": { + "type": "object", + "properties": { + "repo": { + "$ref": "#/components/schemas/TufRepo" + }, + "status": { + "$ref": "#/components/schemas/TufRepoUploadStatus" + } + }, + "required": [ + "repo", + "status" + ] + }, + "TufRepoUploadStatus": { + "description": "Whether the uploaded TUF repo already existed or was new and had to be inserted. Part of `TufRepoUpload`.", + "oneOf": [ + { + "description": "The repository already existed in the database", + "type": "string", + "enum": [ + "already_exists" + ] + }, + { + "description": "The repository did not exist, and was inserted into the database", + "type": "string", + "enum": [ + "inserted" + ] + } + ] + }, + "TxEqConfig": { + "description": "Per-port tx-eq overrides. This can be used to fine-tune the transceiver equalization settings to improve signal integrity.", + "type": "object", + "properties": { + "main": { + "nullable": true, + "description": "Main tap", + "type": "integer", + "format": "int32" + }, + "post1": { + "nullable": true, + "description": "Post-cursor tap1", + "type": "integer", + "format": "int32" + }, + "post2": { + "nullable": true, + "description": "Post-cursor tap2", + "type": "integer", + "format": "int32" + }, + "pre1": { + "nullable": true, + "description": "Pre-cursor tap1", + "type": "integer", + "format": "int32" + }, + "pre2": { + "nullable": true, + "description": "Pre-cursor tap2", + "type": "integer", + "format": "int32" + } + } + }, + "TxEqConfig2": { + "description": "Per-port tx-eq overrides. This can be used to fine-tune the transceiver equalization settings to improve signal integrity.", + "type": "object", + "properties": { + "main": { + "nullable": true, + "description": "Main tap", + "type": "integer", + "format": "int32" + }, + "post1": { + "nullable": true, + "description": "Post-cursor tap1", + "type": "integer", + "format": "int32" + }, + "post2": { + "nullable": true, + "description": "Post-cursor tap2", + "type": "integer", + "format": "int32" + }, + "pre1": { + "nullable": true, + "description": "Pre-cursor tap1", + "type": "integer", + "format": "int32" + }, + "pre2": { + "nullable": true, + "description": "Pre-cursor tap2", + "type": "integer", + "format": "int32" + } + } + }, + "UninitializedSled": { + "description": "A sled that has not been added to an initialized rack yet", + "type": "object", + "properties": { + "baseboard": { + "$ref": "#/components/schemas/Baseboard" + }, + "cubby": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "rack_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "baseboard", + "cubby", + "rack_id" + ] + }, + "UninitializedSledId": { + "description": "The unique hardware ID for a sled", + "type": "object", + "properties": { + "part": { + "type": "string" + }, + "serial": { + "type": "string" + } + }, + "required": [ + "part", + "serial" + ] + }, + "UninitializedSledResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/UninitializedSled" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "Units": { + "description": "Measurement units for timeseries samples.", + "oneOf": [ + { + "type": "string", + "enum": [ + "count", + "bytes", + "seconds", + "nanoseconds", + "volts", + "amps", + "watts", + "degrees_celsius" + ] + }, + { + "description": "No meaningful units, e.g. a dimensionless quanity.", + "type": "string", + "enum": [ + "none" + ] + }, + { + "description": "Rotations per minute.", + "type": "string", + "enum": [ + "rpm" + ] + } + ] + }, + "UpdateStatus": { + "type": "object", + "properties": { + "components_by_release_version": { + "description": "Count of components running each release version\n\nKeys will be either:\n\n* Semver-like release version strings * \"install dataset\", representing the initial rack software before any updates * \"unknown\", which means there is no TUF repo uploaded that matches the software running on the component)", + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "uint", + "minimum": 0 + } + }, + "suspended": { + "description": "Whether automatic update is suspended due to manual update activity\n\nAfter a manual support procedure that changes the system software, automatic update activity is suspended to avoid undoing the change. To resume automatic update, first upload the TUF repository matching the manually applied update, then set that as the target release.", + "type": "boolean" + }, + "target_release": { + "nullable": true, + "description": "Current target release of the system software\n\nThis may not correspond to the actual system software running at the time of request; it is instead the release that the system should be moving towards as a goal state. The system asynchronously updates software to match this target release.\n\nWill only be null if a target release has never been set. In that case, the system is not automatically attempting to manage software versions.", + "allOf": [ + { + "$ref": "#/components/schemas/TargetRelease" + } + ] + }, + "time_last_step_planned": { + "description": "Time of most recent update planning activity\n\nThis is intended as a rough indicator of the last time something happened in the update planner.", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "components_by_release_version", + "suspended", + "target_release", + "time_last_step_planned" + ] + }, + "UpdatesTrustRoot": { + "description": "Trusted root role used by the update system to verify update repositories.", + "type": "object", + "properties": { + "id": { + "description": "The UUID of this trusted root role.", + "type": "string", + "format": "uuid" + }, + "root_role": { + "description": "The trusted root role itself, a JSON document as described by The Update Framework." + }, + "time_created": { + "description": "Time the trusted root role was added.", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "root_role", + "time_created" + ] + }, + "UpdatesTrustRootResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/UpdatesTrustRoot" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "User": { + "description": "View of a User", + "type": "object", + "properties": { + "display_name": { + "description": "Human-readable name that can identify the user", + "type": "string" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "silo_id": { + "description": "Uuid of the silo to which this user belongs", + "type": "string", + "format": "uuid" + } + }, + "required": [ + "display_name", + "id", + "silo_id" + ] + }, + "UserBuiltin": { + "description": "View of a Built-in User\n\nBuilt-in users are identities internal to the system, used when the control plane performs actions autonomously", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "name", + "time_created", + "time_modified" + ] + }, + "UserBuiltinResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/UserBuiltin" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "UserCreate": { + "description": "Create-time parameters for a `User`", + "type": "object", + "properties": { + "external_id": { + "description": "username used to log in", + "allOf": [ + { + "$ref": "#/components/schemas/UserId" + } + ] + }, + "password": { + "description": "how to set the user's login password", + "allOf": [ + { + "$ref": "#/components/schemas/UserPassword" + } + ] + } + }, + "required": [ + "external_id", + "password" + ] + }, + "UserId": { + "title": "A username for a local-only user", + "description": "Usernames must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Usernames cannot be a UUID, but they may contain a UUID. They can be at most 63 characters long.", + "type": "string", + "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$", + "minLength": 1, + "maxLength": 63 + }, + "UserPassword": { + "description": "Parameters for setting a user's password", + "oneOf": [ + { + "description": "Sets the user's password to the provided value", + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": [ + "password" + ] + }, + "value": { + "$ref": "#/components/schemas/Password" + } + }, + "required": [ + "mode", + "value" + ] + }, + { + "description": "Invalidates any current password (disabling password authentication)", + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": [ + "login_disallowed" + ] + } + }, + "required": [ + "mode" + ] + } + ] + }, + "UserResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "UsernamePasswordCredentials": { + "description": "Credentials for local user login", + "type": "object", + "properties": { + "password": { + "$ref": "#/components/schemas/Password" + }, + "username": { + "$ref": "#/components/schemas/UserId" + } + }, + "required": [ + "password", + "username" + ] + }, + "Utilization": { + "description": "View of the current silo's resource utilization and capacity", + "type": "object", + "properties": { + "capacity": { + "description": "The total amount of resources that can be provisioned in this silo Actions that would exceed this limit will fail", + "allOf": [ + { + "$ref": "#/components/schemas/VirtualResourceCounts" + } + ] + }, + "provisioned": { + "description": "Accounts for resources allocated to running instances or storage allocated via disks or snapshots Note that CPU and memory resources associated with a stopped instances are not counted here whereas associated disks will still be counted", + "allOf": [ + { + "$ref": "#/components/schemas/VirtualResourceCounts" + } + ] + } + }, + "required": [ + "capacity", + "provisioned" + ] + }, + "ValueArray": { + "description": "List of data values for one timeseries.\n\nEach element is an option, where `None` represents a missing sample.", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "integer" + ] + }, + "values": { + "type": "array", + "items": { + "nullable": true, + "type": "integer", + "format": "int64" + } + } + }, + "required": [ + "type", + "values" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "double" + ] + }, + "values": { + "type": "array", + "items": { + "nullable": true, + "type": "number", + "format": "double" + } + } + }, + "required": [ + "type", + "values" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "boolean" + ] + }, + "values": { + "type": "array", + "items": { + "nullable": true, + "type": "boolean" + } + } + }, + "required": [ + "type", + "values" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "string" + ] + }, + "values": { + "type": "array", + "items": { + "nullable": true, + "type": "string" + } + } + }, + "required": [ + "type", + "values" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "integer_distribution" + ] + }, + "values": { + "type": "array", + "items": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Distributionint64" + } + ] + } + } + }, + "required": [ + "type", + "values" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "double_distribution" + ] + }, + "values": { + "type": "array", + "items": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Distributiondouble" + } + ] + } + } + }, + "required": [ + "type", + "values" + ] + } + ] + }, + "Values": { + "description": "A single list of values, for one dimension of a timeseries.", + "type": "object", + "properties": { + "metric_type": { + "description": "The type of this metric.", + "allOf": [ + { + "$ref": "#/components/schemas/MetricType" + } + ] + }, + "values": { + "description": "The data values.", + "allOf": [ + { + "$ref": "#/components/schemas/ValueArray" + } + ] + } + }, + "required": [ + "metric_type", + "values" + ] + }, + "VirtualResourceCounts": { + "description": "A collection of resource counts used to describe capacity and utilization", + "type": "object", + "properties": { + "cpus": { + "description": "Number of virtual CPUs", + "type": "integer", + "format": "int64" + }, + "memory": { + "description": "Amount of memory in bytes", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + }, + "storage": { + "description": "Amount of disk storage in bytes", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + } + }, + "required": [ + "cpus", + "memory", + "storage" + ] + }, + "Vni": { + "description": "A Geneve Virtual Network Identifier", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "Vpc": { + "description": "View of a VPC", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "dns_name": { + "description": "The name used for the VPC in DNS.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "ipv6_prefix": { + "description": "The unique local IPv6 address range for subnets in this VPC", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Net" + } + ] + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "project_id": { + "description": "id for the project containing this VPC", + "type": "string", + "format": "uuid" + }, + "system_router_id": { + "description": "id for the system router where subnet default routes are registered", + "type": "string", + "format": "uuid" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "dns_name", + "id", + "ipv6_prefix", + "name", + "project_id", + "system_router_id", + "time_created", + "time_modified" + ] + }, + "VpcCreate": { + "description": "Create-time parameters for a `Vpc`", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "dns_name": { + "$ref": "#/components/schemas/Name" + }, + "ipv6_prefix": { + "nullable": true, + "description": "The IPv6 prefix for this VPC\n\nAll IPv6 subnets created from this VPC must be taken from this range, which should be a Unique Local Address in the range `fd00::/48`. The default VPC Subnet will have the first `/64` range from this prefix.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Net" + } + ] + }, + "name": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "description", + "dns_name", + "name" + ] + }, + "VpcFirewallIcmpFilter": { + "type": "object", + "properties": { + "code": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/IcmpParamRange" + } + ] + }, + "icmp_type": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "icmp_type" + ] + }, + "VpcFirewallRule": { + "description": "A single rule in a VPC firewall", + "type": "object", + "properties": { + "action": { + "description": "Whether traffic matching the rule should be allowed or dropped", + "allOf": [ + { + "$ref": "#/components/schemas/VpcFirewallRuleAction" + } + ] + }, + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "direction": { + "description": "Whether this rule is for incoming or outgoing traffic", + "allOf": [ + { + "$ref": "#/components/schemas/VpcFirewallRuleDirection" + } + ] + }, + "filters": { + "description": "Reductions on the scope of the rule", + "allOf": [ + { + "$ref": "#/components/schemas/VpcFirewallRuleFilter" + } + ] + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "priority": { + "description": "The relative priority of this rule", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "status": { + "description": "Whether this rule is in effect", + "allOf": [ + { + "$ref": "#/components/schemas/VpcFirewallRuleStatus" + } + ] + }, + "targets": { + "description": "Determine the set of instances that the rule applies to", + "type": "array", + "items": { + "$ref": "#/components/schemas/VpcFirewallRuleTarget" + } + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + }, + "vpc_id": { + "description": "The VPC to which this rule belongs", + "type": "string", + "format": "uuid" + } + }, + "required": [ + "action", + "description", + "direction", + "filters", + "id", + "name", + "priority", + "status", + "targets", + "time_created", + "time_modified", + "vpc_id" + ] + }, + "VpcFirewallRuleAction": { + "type": "string", + "enum": [ + "allow", + "deny" + ] + }, + "VpcFirewallRuleDirection": { + "type": "string", + "enum": [ + "inbound", + "outbound" + ] + }, + "VpcFirewallRuleFilter": { + "description": "Filters reduce the scope of a firewall rule. Without filters, the rule applies to all packets to the targets (or from the targets, if it's an outbound rule). With multiple filters, the rule applies only to packets matching ALL filters. The maximum number of each type of filter is 256.", + "type": "object", + "properties": { + "hosts": { + "nullable": true, + "description": "If present, host filters match the \"other end\" of traffic from the target’s perspective: for an inbound rule, they match the source of traffic. For an outbound rule, they match the destination.", + "type": "array", + "items": { + "$ref": "#/components/schemas/VpcFirewallRuleHostFilter" + }, + "maxItems": 256 + }, + "ports": { + "nullable": true, + "description": "If present, the destination ports or port ranges this rule applies to.", + "type": "array", + "items": { + "$ref": "#/components/schemas/L4PortRange" + }, + "maxItems": 256 + }, + "protocols": { + "nullable": true, + "description": "If present, the networking protocols this rule applies to.", + "type": "array", + "items": { + "$ref": "#/components/schemas/VpcFirewallRuleProtocol" + }, + "maxItems": 256 + } + } + }, + "VpcFirewallRuleHostFilter": { + "description": "The `VpcFirewallRuleHostFilter` is used to filter traffic on the basis of its source or destination host.", + "oneOf": [ + { + "description": "The rule applies to traffic from/to all instances in the VPC", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "vpc" + ] + }, + "value": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The rule applies to traffic from/to all instances in the VPC Subnet", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "subnet" + ] + }, + "value": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The rule applies to traffic from/to this specific instance", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "instance" + ] + }, + "value": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The rule applies to traffic from/to a specific IP address", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ip" + ] + }, + "value": { + "type": "string", + "format": "ip" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The rule applies to traffic from/to a specific IP subnet", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ip_net" + ] + }, + "value": { + "$ref": "#/components/schemas/IpNet" + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "VpcFirewallRuleProtocol": { + "description": "The protocols that may be specified in a firewall rule's filter", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "tcp" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "udp" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "icmp" + ] + }, + "value": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/VpcFirewallIcmpFilter" + } + ] + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "VpcFirewallRuleStatus": { + "type": "string", + "enum": [ + "disabled", + "enabled" + ] + }, + "VpcFirewallRuleTarget": { + "description": "A `VpcFirewallRuleTarget` is used to specify the set of instances to which a firewall rule applies. You can target instances directly by name, or specify a VPC, VPC subnet, IP, or IP subnet, which will apply the rule to traffic going to all matching instances. Targets are additive: the rule applies to instances matching ANY target.", + "oneOf": [ + { + "description": "The rule applies to all instances in the VPC", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "vpc" + ] + }, + "value": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The rule applies to all instances in the VPC Subnet", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "subnet" + ] + }, + "value": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The rule applies to this specific instance", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "instance" + ] + }, + "value": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The rule applies to a specific IP address", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ip" + ] + }, + "value": { + "type": "string", + "format": "ip" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The rule applies to a specific IP subnet", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ip_net" + ] + }, + "value": { + "$ref": "#/components/schemas/IpNet" + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "VpcFirewallRuleUpdate": { + "description": "A single rule in a VPC firewall", + "type": "object", + "properties": { + "action": { + "description": "Whether traffic matching the rule should be allowed or dropped", + "allOf": [ + { + "$ref": "#/components/schemas/VpcFirewallRuleAction" + } + ] + }, + "description": { + "description": "Human-readable free-form text about a resource", + "type": "string" + }, + "direction": { + "description": "Whether this rule is for incoming or outgoing traffic", + "allOf": [ + { + "$ref": "#/components/schemas/VpcFirewallRuleDirection" + } + ] + }, + "filters": { + "description": "Reductions on the scope of the rule", + "allOf": [ + { + "$ref": "#/components/schemas/VpcFirewallRuleFilter" + } + ] + }, + "name": { + "description": "Name of the rule, unique to this VPC", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "priority": { + "description": "The relative priority of this rule", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "status": { + "description": "Whether this rule is in effect", + "allOf": [ + { + "$ref": "#/components/schemas/VpcFirewallRuleStatus" + } + ] + }, + "targets": { + "description": "Determine the set of instances that the rule applies to", + "type": "array", + "items": { + "$ref": "#/components/schemas/VpcFirewallRuleTarget" + }, + "maxItems": 256 + } + }, + "required": [ + "action", + "description", + "direction", + "filters", + "name", + "priority", + "status", + "targets" + ] + }, + "VpcFirewallRuleUpdateParams": { + "description": "Updated list of firewall rules. Will replace all existing rules.", + "type": "object", + "properties": { + "rules": { + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/VpcFirewallRuleUpdate" + }, + "maxItems": 1024 + } + } + }, + "VpcFirewallRules": { + "description": "Collection of a Vpc's firewall rules", + "type": "object", + "properties": { + "rules": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VpcFirewallRule" + } + } + }, + "required": [ + "rules" + ] + }, + "VpcResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Vpc" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "VpcRouter": { + "description": "A VPC router defines a series of rules that indicate where traffic should be sent depending on its destination.", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "kind": { + "$ref": "#/components/schemas/VpcRouterKind" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + }, + "vpc_id": { + "description": "The VPC to which the router belongs.", + "type": "string", + "format": "uuid" + } + }, + "required": [ + "description", + "id", + "kind", + "name", + "time_created", + "time_modified", + "vpc_id" + ] + }, + "VpcRouterCreate": { + "description": "Create-time parameters for a `VpcRouter`", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "description", + "name" + ] + }, + "VpcRouterKind": { + "type": "string", + "enum": [ + "system", + "custom" + ] + }, + "VpcRouterResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/VpcRouter" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "VpcRouterUpdate": { + "description": "Updateable properties of a `VpcRouter`", + "type": "object", + "properties": { + "description": { + "nullable": true, + "type": "string" + }, + "name": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + } + }, + "VpcSubnet": { + "description": "A VPC subnet represents a logical grouping for instances that allows network traffic between them, within a IPv4 subnetwork or optionally an IPv6 subnetwork.", + "type": "object", + "properties": { + "custom_router_id": { + "nullable": true, + "description": "ID for an attached custom router.", + "type": "string", + "format": "uuid" + }, + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "ipv4_block": { + "description": "The IPv4 subnet CIDR block.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4Net" + } + ] + }, + "ipv6_block": { + "description": "The IPv6 subnet CIDR block.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Net" + } + ] + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + }, + "vpc_id": { + "description": "The VPC to which the subnet belongs.", + "type": "string", + "format": "uuid" + } + }, + "required": [ + "description", + "id", + "ipv4_block", + "ipv6_block", + "name", + "time_created", + "time_modified", + "vpc_id" + ] + }, + "VpcSubnetCreate": { + "description": "Create-time parameters for a `VpcSubnet`", + "type": "object", + "properties": { + "custom_router": { + "nullable": true, + "description": "An optional router, used to direct packets sent from hosts in this subnet to any destination address.\n\nCustom routers apply in addition to the VPC-wide *system* router, and have higher priority than the system router for an otherwise equal-prefix-length match.", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + }, + "description": { + "type": "string" + }, + "ipv4_block": { + "description": "The IPv4 address range for this subnet.\n\nIt must be allocated from an RFC 1918 private address range, and must not overlap with any other existing subnet in the VPC.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4Net" + } + ] + }, + "ipv6_block": { + "nullable": true, + "description": "The IPv6 address range for this subnet.\n\nIt must be allocated from the RFC 4193 Unique Local Address range, with the prefix equal to the parent VPC's prefix. A random `/64` block will be assigned if one is not provided. It must not overlap with any existing subnet in the VPC.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Net" + } + ] + }, + "name": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "description", + "ipv4_block", + "name" + ] + }, + "VpcSubnetResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/VpcSubnet" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "VpcSubnetUpdate": { + "description": "Updateable properties of a `VpcSubnet`", + "type": "object", + "properties": { + "custom_router": { + "nullable": true, + "description": "An optional router, used to direct packets sent from hosts in this subnet to any destination address.", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + }, + "description": { + "nullable": true, + "type": "string" + }, + "name": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + } + }, + "VpcUpdate": { + "description": "Updateable properties of a `Vpc`", + "type": "object", + "properties": { + "description": { + "nullable": true, + "type": "string" + }, + "dns_name": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "name": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + } + }, + "WebhookCreate": { + "description": "Create-time identity-related parameters", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "endpoint": { + "description": "The URL that webhook notification requests should be sent to", + "type": "string", + "format": "uri" + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "secrets": { + "description": "A non-empty list of secret keys used to sign webhook payloads.", + "type": "array", + "items": { + "type": "string" + } + }, + "subscriptions": { + "description": "A list of webhook event class subscriptions.\n\nIf this list is empty or is not included in the request body, the webhook will not be subscribed to any events.", + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/AlertSubscription" + } + } + }, + "required": [ + "description", + "endpoint", + "name", + "secrets" + ] + }, + "WebhookDeliveryAttempt": { + "description": "An individual delivery attempt for a webhook event.\n\nThis represents a single HTTP request that was sent to the receiver, and its outcome.", + "type": "object", + "properties": { + "attempt": { + "description": "The attempt number.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "response": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/WebhookDeliveryResponse" + } + ] + }, + "result": { + "description": "The outcome of this delivery attempt: either the event was delivered successfully, or the request failed for one of several reasons.", + "allOf": [ + { + "$ref": "#/components/schemas/WebhookDeliveryAttemptResult" + } + ] + }, + "time_sent": { + "description": "The time at which the webhook delivery was attempted.", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "attempt", + "result", + "time_sent" + ] + }, + "WebhookDeliveryAttemptResult": { + "oneOf": [ + { + "description": "The webhook event has been delivered successfully.", + "type": "string", + "enum": [ + "succeeded" + ] + }, + { + "description": "A webhook request was sent to the endpoint, and it returned a HTTP error status code indicating an error.", + "type": "string", + "enum": [ + "failed_http_error" + ] + }, + { + "description": "The webhook request could not be sent to the receiver endpoint.", + "type": "string", + "enum": [ + "failed_unreachable" + ] + }, + { + "description": "A connection to the receiver endpoint was successfully established, but no response was received within the delivery timeout.", + "type": "string", + "enum": [ + "failed_timeout" + ] + } + ] + }, + "WebhookDeliveryResponse": { + "description": "The response received from a webhook receiver endpoint.", + "type": "object", + "properties": { + "duration_ms": { + "description": "The response time of the webhook endpoint, in milliseconds.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "status": { + "description": "The HTTP status code returned from the webhook endpoint.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "duration_ms", + "status" + ] + }, + "WebhookReceiver": { + "description": "The configuration for a webhook alert receiver.", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "endpoint": { + "description": "The URL that webhook notification requests are sent to.", + "type": "string", + "format": "uri" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "secrets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebhookSecret" + } + }, + "subscriptions": { + "description": "The list of alert classes to which this receiver is subscribed.", + "type": "array", + "items": { + "$ref": "#/components/schemas/AlertSubscription" + } + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "endpoint", + "id", + "name", + "secrets", + "subscriptions", + "time_created", + "time_modified" + ] + }, + "WebhookReceiverUpdate": { + "description": "Parameters to update a webhook configuration.", + "type": "object", + "properties": { + "description": { + "nullable": true, + "type": "string" + }, + "endpoint": { + "nullable": true, + "description": "The URL that webhook notification requests should be sent to", + "type": "string", + "format": "uri" + }, + "name": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + } + }, + "WebhookSecret": { + "description": "A view of a shared secret key assigned to a webhook receiver.\n\nOnce a secret is created, the value of the secret is not available in the API, as it must remain secret. Instead, secrets are referenced by their unique IDs assigned when they are created.", + "type": "object", + "properties": { + "id": { + "description": "The public unique ID of the secret.", + "type": "string", + "format": "uuid" + }, + "time_created": { + "description": "The UTC timestamp at which this secret was created.", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "time_created" + ] + }, + "WebhookSecretCreate": { + "type": "object", + "properties": { + "secret": { + "description": "The value of the shared secret key.", + "type": "string" + } + }, + "required": [ + "secret" + ] + }, + "WebhookSecrets": { + "description": "A list of the IDs of secrets associated with a webhook receiver.", + "type": "object", + "properties": { + "secrets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebhookSecret" + } + } + }, + "required": [ + "secrets" + ] + }, + "NameOrIdSortMode": { + "description": "Supported set of sort modes for scanning by name or id", + "oneOf": [ + { + "description": "sort in increasing order of \"name\"", + "type": "string", + "enum": [ + "name_ascending" + ] + }, + { + "description": "sort in decreasing order of \"name\"", + "type": "string", + "enum": [ + "name_descending" + ] + }, + { + "description": "sort in increasing order of \"id\"", + "type": "string", + "enum": [ + "id_ascending" + ] + } + ] + }, + "TimeAndIdSortMode": { + "description": "Supported set of sort modes for scanning by timestamp and ID", + "oneOf": [ + { + "description": "sort in increasing order of timestamp and ID, i.e., earliest first", + "type": "string", + "enum": [ + "time_and_id_ascending" + ] + }, + { + "description": "sort in increasing order of timestamp and ID, i.e., most recent first", + "type": "string", + "enum": [ + "time_and_id_descending" + ] + } + ] + }, + "IdSortMode": { + "description": "Supported set of sort modes for scanning by id only.\n\nCurrently, we only support scanning in ascending order.", + "oneOf": [ + { + "description": "sort in increasing order of \"id\"", + "type": "string", + "enum": [ + "id_ascending" + ] + } + ] + }, + "SystemMetricName": { + "type": "string", + "enum": [ + "virtual_disk_space_provisioned", + "cpus_provisioned", + "ram_provisioned" + ] + }, + "PaginationOrder": { + "description": "The order in which the client wants to page through the requested collection", + "type": "string", + "enum": [ + "ascending", + "descending" + ] + }, + "VersionSortMode": { + "description": "Supported sort modes when scanning by semantic version", + "oneOf": [ + { + "description": "Sort in increasing semantic version order (oldest first)", + "type": "string", + "enum": [ + "version_ascending" + ] + }, + { + "description": "Sort in decreasing semantic version order (newest first)", + "type": "string", + "enum": [ + "version_descending" + ] + } + ] + }, + "NameSortMode": { + "description": "Supported set of sort modes for scanning by name only\n\nCurrently, we only support scanning in ascending order.", + "oneOf": [ + { + "description": "sort in increasing order of \"name\"", + "type": "string", + "enum": [ + "name_ascending" + ] + } + ] + } + }, + "responses": { + "Error": { + "description": "Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "tags": [ + { + "name": "affinity", + "description": "Anti-affinity groups give control over instance placement.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/affinity" + } + }, + { + "name": "console-auth", + "description": "API for console authentication", + "externalDocs": { + "url": "http://docs.oxide.computer/api/console-auth" + } + }, + { + "name": "current-user", + "description": "Information pertaining to the current user.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/current-user" + } + }, + { + "name": "disks", + "description": "Virtual disks are used to store instance-local data which includes the operating system.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/disks" + } + }, + { + "name": "experimental", + "description": "Experimental, unstable interfaces, primarily for use by Oxide personnel", + "externalDocs": { + "url": "http://docs.oxide.computer/api/experimental" + } + }, + { + "name": "floating-ips", + "description": "Floating IPs allow a project to allocate well-known IPs to instances.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/floating-ips" + } + }, + { + "name": "images", + "description": "Images are read-only virtual disks that may be used to boot virtual machines.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/images" + } + }, + { + "name": "instances", + "description": "Virtual machine instances are the basic unit of computation. These operations are used for provisioning, controlling, and destroying instances.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/instances" + } + }, + { + "name": "login", + "description": "Authentication endpoints", + "externalDocs": { + "url": "http://docs.oxide.computer/api/login" + } + }, + { + "name": "metrics", + "description": "Silo-scoped metrics", + "externalDocs": { + "url": "http://docs.oxide.computer/api/metrics" + } + }, + { + "name": "multicast-groups", + "description": "Multicast groups provide efficient one-to-many network communication.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/multicast-groups" + } + }, + { + "name": "policy", + "description": "System-wide IAM policy", + "externalDocs": { + "url": "http://docs.oxide.computer/api/policy" + } + }, + { + "name": "projects", + "description": "Projects are a grouping of associated resources such as instances and disks within a silo for purposes of billing and access control.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/projects" + } + }, + { + "name": "silos", + "description": "Silos represent a logical partition of users and resources.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/silos" + } + }, + { + "name": "snapshots", + "description": "Snapshots of virtual disks at a particular point in time.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/snapshots" + } + }, + { + "name": "system/alerts", + "description": "Alerts deliver notifications for events that occur on the Oxide rack", + "externalDocs": { + "url": "http://docs.oxide.computer/api/alerts" + } + }, + { + "name": "system/audit-log", + "description": "These endpoints relate to audit logs.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/system-audit-log" + } + }, + { + "name": "system/hardware", + "description": "These operations pertain to hardware inventory and management. Racks are the unit of expansion of an Oxide deployment. Racks are in turn composed of sleds, switches, power supplies, and a cabled backplane.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/system-hardware" + } + }, + { + "name": "system/ip-pools", + "description": "IP pools are collections of external IPs that can be assigned to silos. When a pool is linked to a silo, users in that silo can allocate IPs from the pool for their instances.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/system-ip-pools" + } + }, + { + "name": "system/metrics", + "description": "Metrics provide insight into the operation of the Oxide deployment. These include telemetry on hardware and software components that can be used to understand the current state as well as to diagnose issues.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/system-metrics" + } + }, + { + "name": "system/networking", + "description": "This provides rack-level network configuration.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/system-networking" + } + }, + { + "name": "system/probes", + "description": "Probes for testing network connectivity", + "externalDocs": { + "url": "http://docs.oxide.computer/api/probes" + } + }, + { + "name": "system/silos", + "description": "Silos represent a logical partition of users and resources.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/system-silos" + } + }, + { + "name": "system/status", + "description": "Endpoints related to system health", + "externalDocs": { + "url": "http://docs.oxide.computer/api/system-status" + } + }, + { + "name": "system/update", + "description": "Upload and manage system updates", + "externalDocs": { + "url": "http://docs.oxide.computer/api/system-update" + } + }, + { + "name": "tokens", + "description": "API clients use device access tokens for authentication.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/tokens" + } + }, + { + "name": "vpcs", + "description": "Virtual Private Clouds (VPCs) provide isolated network environments for managing and deploying services.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/vpcs" + } + } + ] +} diff --git a/openapi/nexus/nexus-latest.json b/openapi/nexus/nexus-latest.json index 58fe0b52fa8..e03d35d7d91 120000 --- a/openapi/nexus/nexus-latest.json +++ b/openapi/nexus/nexus-latest.json @@ -1 +1 @@ -nexus-2025121200.0.0-ad3628.json \ No newline at end of file +nexus-2025121800.0.0-c18c45.json \ No newline at end of file diff --git a/sled-agent/src/services.rs b/sled-agent/src/services.rs index a240558317c..2c532701f69 100644 --- a/sled-agent/src/services.rs +++ b/sled-agent/src/services.rs @@ -61,6 +61,7 @@ use internal_dns_types::names::BOUNDARY_NTP_DNS_NAME; use internal_dns_types::names::DNS_ZONE; use nexus_config::{ConfigDropshotWithTls, DeploymentConfig}; use omicron_common::address::AZ_PREFIX; +use omicron_common::address::ConcreteIp; use omicron_common::address::DENDRITE_PORT; use omicron_common::address::LLDP_PORT; use omicron_common::address::MAX_PORT; @@ -78,7 +79,6 @@ use omicron_common::address::{ }; use omicron_common::address::{Ipv6Subnet, NEXUS_TECHPORT_EXTERNAL_PORT}; use omicron_common::api::external::Generation; -use omicron_common::api::internal::shared::external_ip::ConcreteIp; use omicron_common::api::internal::shared::{ ExternalIpConfig, ExternalIpConfigBuilder, ExternalIps, HostPortConfig, PrivateIpConfig, RackNetworkConfig, SledIdentifiers, From bc3f12cb75c7a3c095d9d686545943462c0d9ff6 Mon Sep 17 00:00:00 2001 From: Benjamin Naecker Date: Thu, 18 Dec 2025 18:02:31 +0000 Subject: [PATCH 2/2] WIP: implementing NIC update --- nexus/tests/integration_tests/instances.rs | 134 +++++++++++++-------- nexus/types/src/external_api/params.rs | 47 +++++++- 2 files changed, 129 insertions(+), 52 deletions(-) diff --git a/nexus/tests/integration_tests/instances.rs b/nexus/tests/integration_tests/instances.rs index 88444697958..59902bac183 100644 --- a/nexus/tests/integration_tests/instances.rs +++ b/nexus/tests/integration_tests/instances.rs @@ -44,6 +44,7 @@ use nexus_test_utils::wait_for_producer; use nexus_types::external_api::params::IpAssignment; use nexus_types::external_api::params::PrivateIpStackCreate; use nexus_types::external_api::params::PrivateIpv4StackCreate; +use nexus_types::external_api::params::PrivateIpv4StackUpdate; use nexus_types::external_api::params::PrivateIpv6StackCreate; use nexus_types::external_api::params::SshKeyCreate; use nexus_types::external_api::shared::IpKind; @@ -3568,7 +3569,7 @@ async fn test_instance_update_network_interfaces( description: Some(new_description.clone()), }, primary: false, - transit_ips: vec![], + ..Default::default() }; // Verify we fail to update the NIC when the instance is running @@ -3655,7 +3656,7 @@ async fn test_instance_update_network_interfaces( description: None, }, primary: true, - transit_ips: vec![], + ..Default::default() }; let updated_primary_iface1 = NexusRequest::object_put( client, @@ -3753,7 +3754,7 @@ async fn test_instance_update_network_interfaces( description: None, }, primary: true, - transit_ips: vec![], + ..Default::default() }; let new_primary_iface = NexusRequest::object_put( client, @@ -3849,6 +3850,21 @@ async fn test_instance_update_network_interfaces( assert_eq!(iface.identity.name, if_params[0].identity.name); } +#[nexus_test] +async fn cannot_make_new_primary_nic_lacking_ip_stack_for_external_addresses( + _cptestctx: &ControlPlaneTestContext, +) { + todo!() +} + + +#[nexus_test] +async fn cannot_remove_ip_stack_with_outstanding_external_ips_of_the_same_version( + _cptestctx: &ControlPlaneTestContext, +) { + todo!() +} + #[nexus_test] async fn can_add_instance_network_interface_ip_stack( _cptestctx: &ControlPlaneTestContext, @@ -3906,11 +3922,13 @@ async fn test_instance_update_network_interface_transit_ips( description: None, }, primary: false, - transit_ips: vec![ - "10.0.0.0/9".parse().unwrap(), - "10.128.0.0/9".parse().unwrap(), - "1.1.1.1/32".parse().unwrap(), - ], + ipv4: PrivateIpv4StackUpdate::Modify { + transit_ips: vec![ + "10.0.0.0/9".parse().unwrap(), + "10.128.0.0/9".parse().unwrap(), + "1.1.1.1/32".parse().unwrap(), + ], + }, }; // Verify that a selection of transit IPs (mixture of private and global @@ -3926,12 +3944,14 @@ async fn test_instance_update_network_interface_transit_ips( // Non-canonical form (e.g., host identifier is nonzero) subnets should // be rejected. let with_extra_bits = params::InstanceNetworkInterfaceUpdate { - transit_ips: vec![ - "10.0.0.0/9".parse().unwrap(), - "10.128.0.0/9".parse().unwrap(), - // Invalid vvv - "172.30.255.255/24".parse().unwrap(), - ], + ipv4: PrivateIpv4StackUpdate::Modify { + transit_ips: vec![ + "10.0.0.0/9".parse().unwrap(), + "10.128.0.0/9".parse().unwrap(), + // Invalid vvv + "172.30.255.255/24".parse().unwrap(), + ], + }, ..base_update.clone() }; let err = object_put_error( @@ -3948,11 +3968,13 @@ async fn test_instance_update_network_interface_transit_ips( // Multicast IP blocks should be rejected. let with_mc1 = params::InstanceNetworkInterfaceUpdate { - transit_ips: vec![ - "10.0.0.0/9".parse().unwrap(), - "10.128.0.0/9".parse().unwrap(), - "224.0.0.0/4".parse().unwrap(), - ], + ipv4: PrivateIpv4StackUpdate::Modify { + transit_ips: vec![ + "10.0.0.0/9".parse().unwrap(), + "10.128.0.0/9".parse().unwrap(), + "224.0.0.0/4".parse().unwrap(), + ], + }, ..base_update.clone() }; let err = object_put_error( @@ -3968,11 +3990,13 @@ async fn test_instance_update_network_interface_transit_ips( ); let with_mc2 = params::InstanceNetworkInterfaceUpdate { - transit_ips: vec![ - "10.0.0.0/9".parse().unwrap(), - "10.128.0.0/9".parse().unwrap(), - "230.20.20.128/32".parse().unwrap(), - ], + ipv4: PrivateIpv4StackUpdate::Modify { + transit_ips: vec![ + "10.0.0.0/9".parse().unwrap(), + "10.128.0.0/9".parse().unwrap(), + "230.20.20.128/32".parse().unwrap(), + ], + }, ..base_update.clone() }; let err = object_put_error( @@ -3989,11 +4013,13 @@ async fn test_instance_update_network_interface_transit_ips( // Loopback ranges. let with_lo1 = params::InstanceNetworkInterfaceUpdate { - transit_ips: vec![ - "10.0.0.0/9".parse().unwrap(), - "10.128.0.0/9".parse().unwrap(), - "127.42.77.0/24".parse().unwrap(), - ], + ipv4: PrivateIpv4StackUpdate::Modify { + transit_ips: vec![ + "10.0.0.0/9".parse().unwrap(), + "10.128.0.0/9".parse().unwrap(), + "127.42.77.0/24".parse().unwrap(), + ], + }, ..base_update.clone() }; let err = object_put_error( @@ -4009,11 +4035,13 @@ async fn test_instance_update_network_interface_transit_ips( ); let with_lo2 = params::InstanceNetworkInterfaceUpdate { - transit_ips: vec![ - "10.0.0.0/9".parse().unwrap(), - "10.128.0.0/9".parse().unwrap(), - "127.0.0.1/32".parse().unwrap(), - ], + ipv4: PrivateIpv4StackUpdate::Modify { + transit_ips: vec![ + "10.0.0.0/9".parse().unwrap(), + "10.128.0.0/9".parse().unwrap(), + "127.0.0.1/32".parse().unwrap(), + ], + }, ..base_update.clone() }; let err = object_put_error( @@ -4027,11 +4055,13 @@ async fn test_instance_update_network_interface_transit_ips( // Overlapping IP ranges should be rejected, as should identical ranges. let with_dup1 = params::InstanceNetworkInterfaceUpdate { - transit_ips: vec![ - "10.0.0.0/9".parse().unwrap(), - "10.128.0.0/9".parse().unwrap(), - "10.0.0.0/9".parse().unwrap(), - ], + ipv4: PrivateIpv4StackUpdate::Modify { + transit_ips: vec![ + "10.0.0.0/9".parse().unwrap(), + "10.128.0.0/9".parse().unwrap(), + "10.0.0.0/9".parse().unwrap(), + ], + }, ..base_update.clone() }; let err = object_put_error( @@ -4047,11 +4077,13 @@ async fn test_instance_update_network_interface_transit_ips( ); let with_dup2 = params::InstanceNetworkInterfaceUpdate { - transit_ips: vec![ - "10.0.0.0/9".parse().unwrap(), - "10.128.0.0/9".parse().unwrap(), - "10.128.32.0/24".parse().unwrap(), - ], + ipv4: PrivateIpv4StackUpdate::Modify { + transit_ips: vec![ + "10.0.0.0/9".parse().unwrap(), + "10.128.0.0/9".parse().unwrap(), + "10.128.32.0/24".parse().unwrap(), + ], + }, ..base_update.clone() }; let err = object_put_error( @@ -4068,10 +4100,12 @@ async fn test_instance_update_network_interface_transit_ips( // Verify that we also catch more specific CIDRs appearing sooner in the list. let with_dup3 = params::InstanceNetworkInterfaceUpdate { - transit_ips: vec![ - "10.20.20.0/30".parse().unwrap(), - "10.0.0.0/8".parse().unwrap(), - ], + ipv4: PrivateIpv4StackUpdate::Modify { + transit_ips: vec![ + "10.20.20.0/30".parse().unwrap(), + "10.0.0.0/8".parse().unwrap(), + ], + }, ..base_update.clone() }; let err = object_put_error( @@ -4097,7 +4131,9 @@ async fn test_instance_update_network_interface_transit_ips( // As a final sanity test, we can still effectively remove spoof checking // using the unspecified network address. let allow_all = params::InstanceNetworkInterfaceUpdate { - transit_ips: vec!["0.0.0.0/0".parse().unwrap()], + ipv4: PrivateIpv4StackUpdate::Modify { + transit_ips: vec!["0.0.0.0/0".parse().unwrap()], + }, ..base_update.clone() }; diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 08ab2e3a1be..ea441ee0bb7 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -957,6 +957,44 @@ pub struct InstanceNetworkInterfaceCreate { pub ip_config: PrivateIpStackCreate, } +#[derive(Clone, Debug, Default, Deserialize, JsonSchema, Serialize)] +#[serde(rename_all = "snake_case", tag = "type", content = "value")] +pub enum PrivateIpv4StackUpdate { + #[default] + NoChange, + Remove, + Add(PrivateIpv4StackCreate), + Modify { transit_ips: Vec }, +} + +#[derive(Clone, Debug, Default, Deserialize, JsonSchema, Serialize)] +#[serde(rename_all = "snake_case", tag = "type", content = "value")] +pub enum PrivateIpv6StackUpdate { + #[default] + NoChange, + Remove, + Add(PrivateIpv6StackCreate), + Modify { transit_ips: Vec }, +} + +// Do nothing to a specific IP stack +// Remove a specific IP stack +// Add a specific IP stack +// Modify the transit IPs of a stack +// +// Independently for IPv6 / IPv4, though we need to do a bunch of validation in +// the database queries for that: +// +// - If adding, don't already have a stack of the same version (UPDATE ... SET +// .. WHERE ip IS NULL, basically) +// - If we're removing, we have a stack of the _other_ version so that the NIC +// is still valid, although the CHECK constraint we have should take care of +// that. +// +// TODO(ben): We need to beef up the check for making a new primary NIC. That +// has to ensure that we have private IP stacks for all the external addresses +// the instance might have. + /// Parameters for updating an `InstanceNetworkInterface` /// /// Note that modifying IP addresses for an interface is not yet supported, a @@ -982,10 +1020,13 @@ pub struct InstanceNetworkInterfaceUpdate { #[serde(default)] pub primary: bool, - /// A set of additional networks that this interface may send and - /// receive traffic on. + /// Update the interface's VPC-private IPv4 stack. + #[serde(default)] + pub ipv4: PrivateIpv4StackUpdate, + + /// Update the interface's VPC-private IPv6 stack. #[serde(default)] - pub transit_ips: Vec, + pub ipv6: PrivateIpv6StackUpdate, } /// How a VPC-private IP address is assigned to a network interface.