diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index fd5cbe3804..f3234d74b9 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -23,6 +23,8 @@ use dropshot::HttpError; pub use dropshot::PaginationOrder; pub use error::*; use futures::stream::BoxStream; +use omicron_uuid_kinds::GenericUuid; +use omicron_uuid_kinds::InstanceUuid; use oxnet::IpNet; use oxnet::Ipv4Net; use parse_display::Display; @@ -982,6 +984,10 @@ impl JsonSchema for Hostname { pub enum ResourceType { AddressLot, AddressLotBlock, + AffinityGroup, + AffinityGroupMember, + AntiAffinityGroup, + AntiAffinityGroupMember, AllowList, BackgroundTask, BgpConfig, @@ -1312,6 +1318,69 @@ pub enum InstanceAutoRestartPolicy { BestEffort, } +// AFFINITY GROUPS + +/// Affinity policy used to describe "what to do when a request cannot be satisfied" +/// +/// Used for both Affinity and Anti-Affinity Groups +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum AffinityPolicy { + /// If the affinity request cannot be satisfied, allow it anyway. + /// + /// This enables a "best-effort" attempt to satisfy the affinity policy. + Allow, + + /// If the affinity request cannot be satisfied, fail explicitly. + Fail, +} + +/// Describes the scope of affinity for the purposes of co-location. +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum FailureDomain { + /// Instances are considered co-located if they are on the same sled + Sled, +} + +/// A member of an Affinity Group +/// +/// Membership in a group is not exclusive - members may belong to multiple +/// affinity / anti-affinity groups. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +#[serde(tag = "type", content = "value", rename_all = "snake_case")] +pub enum AffinityGroupMember { + /// An instance belonging to this group, identified by UUID. + Instance(InstanceUuid), +} + +impl SimpleIdentity for AffinityGroupMember { + fn id(&self) -> Uuid { + match self { + AffinityGroupMember::Instance(id) => *id.as_untyped_uuid(), + } + } +} + +/// A member of an Anti-Affinity Group +/// +/// Membership in a group is not exclusive - members may belong to multiple +/// affinity / anti-affinity groups. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +#[serde(tag = "type", content = "value", rename_all = "snake_case")] +pub enum AntiAffinityGroupMember { + /// An instance belonging to this group, identified by UUID. + Instance(InstanceUuid), +} + +impl SimpleIdentity for AntiAffinityGroupMember { + fn id(&self) -> Uuid { + match self { + AntiAffinityGroupMember::Instance(id) => *id.as_untyped_uuid(), + } + } +} + // DISKS /// View of a Disk diff --git a/nexus/external-api/output/nexus_tags.txt b/nexus/external-api/output/nexus_tags.txt index 82767e399c..59ba14e5fb 100644 --- a/nexus/external-api/output/nexus_tags.txt +++ b/nexus/external-api/output/nexus_tags.txt @@ -1,3 +1,24 @@ +API operations found with tag "affinity" +OPERATION ID METHOD URL PATH +affinity_group_create POST /v1/affinity-groups +affinity_group_delete DELETE /v1/affinity-groups/{affinity_group} +affinity_group_list GET /v1/affinity-groups +affinity_group_member_instance_add POST /v1/affinity-groups/{affinity_group}/members/instance/{instance} +affinity_group_member_instance_delete DELETE /v1/affinity-groups/{affinity_group}/members/instance/{instance} +affinity_group_member_instance_view GET /v1/affinity-groups/{affinity_group}/members/instance/{instance} +affinity_group_member_list GET /v1/affinity-groups/{affinity_group}/members +affinity_group_update PUT /v1/affinity-groups/{affinity_group} +affinity_group_view GET /v1/affinity-groups/{affinity_group} +anti_affinity_group_create POST /v1/anti-affinity-groups +anti_affinity_group_delete DELETE /v1/anti-affinity-groups/{anti_affinity_group} +anti_affinity_group_list GET /v1/anti-affinity-groups +anti_affinity_group_member_instance_add POST /v1/anti-affinity-groups/{anti_affinity_group}/members/instance/{instance} +anti_affinity_group_member_instance_delete DELETE /v1/anti-affinity-groups/{anti_affinity_group}/members/instance/{instance} +anti_affinity_group_member_instance_view GET /v1/anti-affinity-groups/{anti_affinity_group}/members/instance/{instance} +anti_affinity_group_member_list GET /v1/anti-affinity-groups/{anti_affinity_group}/members +anti_affinity_group_update PUT /v1/anti-affinity-groups/{anti_affinity_group} +anti_affinity_group_view GET /v1/anti-affinity-groups/{anti_affinity_group} + API operations found with tag "disks" OPERATION ID METHOD URL PATH disk_bulk_write_import POST /v1/disks/{disk}/bulk-write diff --git a/nexus/external-api/src/lib.rs b/nexus/external-api/src/lib.rs index a832bde319..ad637c9ca4 100644 --- a/nexus/external-api/src/lib.rs +++ b/nexus/external-api/src/lib.rs @@ -74,6 +74,13 @@ const PUT_UPDATE_REPOSITORY_MAX_BYTES: usize = 4 * GIB; allow_other_tags = false, policy = EndpointTagPolicy::ExactlyOne, tags = { + "affinity" = { + description = "Affinity and anti-affinity groups give control over instance placement.", + external_docs = { + url = "http://docs.oxide.computer/api/affinity" + } + + }, "disks" = { description = "Virtual disks are used to store instance-local data which includes the operating system.", external_docs = { @@ -1257,6 +1264,224 @@ pub trait NexusExternalApi { disk_to_detach: TypedBody, ) -> Result, HttpError>; + // Affinity Groups + + /// List affinity groups + #[endpoint { + method = GET, + path = "/v1/affinity-groups", + tags = ["affinity"], + }] + async fn affinity_group_list( + rqctx: RequestContext, + query_params: Query>, + ) -> Result>, HttpError>; + + /// Fetch an affinity group + #[endpoint { + method = GET, + path = "/v1/affinity-groups/{affinity_group}", + tags = ["affinity"], + }] + async fn affinity_group_view( + rqctx: RequestContext, + query_params: Query, + path_params: Path, + ) -> Result, HttpError>; + + /// List members of an affinity group + #[endpoint { + method = GET, + path = "/v1/affinity-groups/{affinity_group}/members", + tags = ["affinity"], + }] + async fn affinity_group_member_list( + rqctx: RequestContext, + query_params: Query>, + path_params: Path, + ) -> Result>, HttpError>; + + /// Fetch an affinity group member + #[endpoint { + method = GET, + path = "/v1/affinity-groups/{affinity_group}/members/instance/{instance}", + tags = ["affinity"], + }] + async fn affinity_group_member_instance_view( + rqctx: RequestContext, + query_params: Query, + path_params: Path, + ) -> Result, HttpError>; + + /// Add a member to an affinity group + #[endpoint { + method = POST, + path = "/v1/affinity-groups/{affinity_group}/members/instance/{instance}", + tags = ["affinity"], + }] + async fn affinity_group_member_instance_add( + rqctx: RequestContext, + query_params: Query, + path_params: Path, + ) -> Result, HttpError>; + + /// Remove a member from an affinity group + #[endpoint { + method = DELETE, + path = "/v1/affinity-groups/{affinity_group}/members/instance/{instance}", + tags = ["affinity"], + }] + async fn affinity_group_member_instance_delete( + rqctx: RequestContext, + query_params: Query, + path_params: Path, + ) -> Result; + + /// Create an affinity group + #[endpoint { + method = POST, + path = "/v1/affinity-groups", + tags = ["affinity"], + }] + async fn affinity_group_create( + rqctx: RequestContext, + query_params: Query, + new_affinity_group_params: TypedBody, + ) -> Result, HttpError>; + + /// Update an affinity group + #[endpoint { + method = PUT, + path = "/v1/affinity-groups/{affinity_group}", + tags = ["affinity"], + }] + async fn affinity_group_update( + rqctx: RequestContext, + query_params: Query, + path_params: Path, + updated_group: TypedBody, + ) -> Result, HttpError>; + + /// Delete an affinity group + #[endpoint { + method = DELETE, + path = "/v1/affinity-groups/{affinity_group}", + tags = ["affinity"], + }] + async fn affinity_group_delete( + rqctx: RequestContext, + query_params: Query, + path_params: Path, + ) -> Result; + + /// List anti-affinity groups + #[endpoint { + method = GET, + path = "/v1/anti-affinity-groups", + tags = ["affinity"], + }] + async fn anti_affinity_group_list( + rqctx: RequestContext, + query_params: Query>, + ) -> Result>, HttpError>; + + /// Fetch an anti-affinity group + #[endpoint { + method = GET, + path = "/v1/anti-affinity-groups/{anti_affinity_group}", + tags = ["affinity"], + }] + async fn anti_affinity_group_view( + rqctx: RequestContext, + query_params: Query, + path_params: Path, + ) -> Result, HttpError>; + + /// List members of an anti-affinity group + #[endpoint { + method = GET, + path = "/v1/anti-affinity-groups/{anti_affinity_group}/members", + tags = ["affinity"], + }] + async fn anti_affinity_group_member_list( + rqctx: RequestContext, + query_params: Query>, + path_params: Path, + ) -> Result>, HttpError>; + + /// Fetch an anti-affinity group member + #[endpoint { + method = GET, + path = "/v1/anti-affinity-groups/{anti_affinity_group}/members/instance/{instance}", + tags = ["affinity"], + }] + async fn anti_affinity_group_member_instance_view( + rqctx: RequestContext, + query_params: Query, + path_params: Path, + ) -> Result, HttpError>; + + /// Add a member to an anti-affinity group + #[endpoint { + method = POST, + path = "/v1/anti-affinity-groups/{anti_affinity_group}/members/instance/{instance}", + tags = ["affinity"], + }] + async fn anti_affinity_group_member_instance_add( + rqctx: RequestContext, + query_params: Query, + path_params: Path, + ) -> Result, HttpError>; + + /// Remove a member from an anti-affinity group + #[endpoint { + method = DELETE, + path = "/v1/anti-affinity-groups/{anti_affinity_group}/members/instance/{instance}", + tags = ["affinity"], + }] + async fn anti_affinity_group_member_instance_delete( + rqctx: RequestContext, + query_params: Query, + path_params: Path, + ) -> Result; + + /// Create an anti-affinity group + #[endpoint { + method = POST, + path = "/v1/anti-affinity-groups", + tags = ["affinity"], + }] + async fn anti_affinity_group_create( + rqctx: RequestContext, + query_params: Query, + new_affinity_group_params: TypedBody, + ) -> Result, HttpError>; + + /// Update an anti-affinity group + #[endpoint { + method = PUT, + path = "/v1/anti-affinity-groups/{anti_affinity_group}", + tags = ["affinity"], + }] + async fn anti_affinity_group_update( + rqctx: RequestContext, + query_params: Query, + path_params: Path, + updated_group: TypedBody, + ) -> Result, HttpError>; + + /// Delete an anti-affinity group + #[endpoint { + method = DELETE, + path = "/v1/anti-affinity-groups/{anti_affinity_group}", + tags = ["affinity"], + }] + async fn anti_affinity_group_delete( + rqctx: RequestContext, + query_params: Query, + path_params: Path, + ) -> Result; + // Certificates /// List certificates for external endpoints diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 191c304216..887a6d35a6 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -15,6 +15,7 @@ use super::{ }; use crate::app::external_endpoints::authority_for_request; use crate::app::support_bundles::SupportBundleQueryType; +use crate::app::Unimpl; use crate::context::ApiContext; use crate::external_api::shared; use dropshot::Body; @@ -69,7 +70,9 @@ use omicron_common::api::external::http_pagination::ScanParams; use omicron_common::api::external::AddressLot; use omicron_common::api::external::AddressLotBlock; use omicron_common::api::external::AddressLotCreateResponse; +use omicron_common::api::external::AffinityGroupMember; use omicron_common::api::external::AggregateBgpMessageHistory; +use omicron_common::api::external::AntiAffinityGroupMember; use omicron_common::api::external::BgpAnnounceSet; use omicron_common::api::external::BgpAnnouncement; use omicron_common::api::external::BgpConfig; @@ -2506,6 +2509,358 @@ impl NexusExternalApi for NexusExternalApiImpl { .await } + // Affinity Groups + + async fn affinity_group_list( + rqctx: RequestContext, + _query_params: Query>, + ) -> Result>, HttpError> + { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let opctx = + crate::context::op_context_for_external_api(&rqctx).await?; + Err(nexus.unimplemented_todo(&opctx, Unimpl::Public).await.into()) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await + } + + async fn affinity_group_view( + rqctx: RequestContext, + _query_params: Query, + _path_params: Path, + ) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let opctx = + crate::context::op_context_for_external_api(&rqctx).await?; + + Err(nexus.unimplemented_todo(&opctx, Unimpl::Public).await.into()) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await + } + + async fn affinity_group_member_list( + rqctx: RequestContext, + _query_params: Query>, + _path_params: Path, + ) -> Result>, HttpError> + { + let apictx = rqctx.context(); + let handler = async { + let opctx = + crate::context::op_context_for_external_api(&rqctx).await?; + let nexus = &apictx.context.nexus; + Err(nexus.unimplemented_todo(&opctx, Unimpl::Public).await.into()) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await + } + + async fn affinity_group_member_instance_view( + rqctx: RequestContext, + _query_params: Query, + _path_params: Path, + ) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let opctx = + crate::context::op_context_for_external_api(&rqctx).await?; + + Err(nexus.unimplemented_todo(&opctx, Unimpl::Public).await.into()) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await + } + + async fn affinity_group_member_instance_add( + rqctx: RequestContext, + _query_params: Query, + _path_params: Path, + ) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let opctx = + crate::context::op_context_for_external_api(&rqctx).await?; + let nexus = &apictx.context.nexus; + Err(nexus.unimplemented_todo(&opctx, Unimpl::Public).await.into()) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await + } + + async fn affinity_group_member_instance_delete( + rqctx: RequestContext, + _query_params: Query, + _path_params: Path, + ) -> Result { + let apictx = rqctx.context(); + let handler = async { + let opctx = + crate::context::op_context_for_external_api(&rqctx).await?; + let nexus = &apictx.context.nexus; + Err(nexus.unimplemented_todo(&opctx, Unimpl::Public).await.into()) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await + } + + async fn affinity_group_create( + rqctx: RequestContext, + _query_params: Query, + _new_affinity_group_params: TypedBody, + ) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let opctx = + crate::context::op_context_for_external_api(&rqctx).await?; + let nexus = &apictx.context.nexus; + Err(nexus.unimplemented_todo(&opctx, Unimpl::Public).await.into()) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await + } + + async fn affinity_group_update( + rqctx: RequestContext, + _query_params: Query, + _path_params: Path, + _updated_group: TypedBody, + ) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let opctx = + crate::context::op_context_for_external_api(&rqctx).await?; + let nexus = &apictx.context.nexus; + Err(nexus.unimplemented_todo(&opctx, Unimpl::Public).await.into()) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await + } + + async fn affinity_group_delete( + rqctx: RequestContext, + _query_params: Query, + _path_params: Path, + ) -> Result { + let apictx = rqctx.context(); + let handler = async { + let opctx = + crate::context::op_context_for_external_api(&rqctx).await?; + let nexus = &apictx.context.nexus; + Err(nexus.unimplemented_todo(&opctx, Unimpl::Public).await.into()) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await + } + + async fn anti_affinity_group_list( + rqctx: RequestContext, + _query_params: Query>, + ) -> Result>, HttpError> + { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let opctx = + crate::context::op_context_for_external_api(&rqctx).await?; + Err(nexus.unimplemented_todo(&opctx, Unimpl::Public).await.into()) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await + } + + async fn anti_affinity_group_view( + rqctx: RequestContext, + _query_params: Query, + _path_params: Path, + ) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let opctx = + crate::context::op_context_for_external_api(&rqctx).await?; + Err(nexus.unimplemented_todo(&opctx, Unimpl::Public).await.into()) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await + } + + async fn anti_affinity_group_member_list( + rqctx: RequestContext, + _query_params: Query>, + _path_params: Path, + ) -> Result>, HttpError> + { + let apictx = rqctx.context(); + let handler = async { + let opctx = + crate::context::op_context_for_external_api(&rqctx).await?; + let nexus = &apictx.context.nexus; + Err(nexus.unimplemented_todo(&opctx, Unimpl::Public).await.into()) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await + } + + async fn anti_affinity_group_member_instance_view( + rqctx: RequestContext, + _query_params: Query, + _path_params: Path, + ) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let opctx = + crate::context::op_context_for_external_api(&rqctx).await?; + Err(nexus.unimplemented_todo(&opctx, Unimpl::Public).await.into()) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await + } + + async fn anti_affinity_group_member_instance_add( + rqctx: RequestContext, + _query_params: Query, + _path_params: Path, + ) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let opctx = + crate::context::op_context_for_external_api(&rqctx).await?; + let nexus = &apictx.context.nexus; + Err(nexus.unimplemented_todo(&opctx, Unimpl::Public).await.into()) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await + } + + async fn anti_affinity_group_member_instance_delete( + rqctx: RequestContext, + _query_params: Query, + _path_params: Path, + ) -> Result { + let apictx = rqctx.context(); + let handler = async { + let opctx = + crate::context::op_context_for_external_api(&rqctx).await?; + let nexus = &apictx.context.nexus; + Err(nexus.unimplemented_todo(&opctx, Unimpl::Public).await.into()) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await + } + + async fn anti_affinity_group_create( + rqctx: RequestContext, + _query_params: Query, + _new_anti_affinity_group_params: TypedBody< + params::AntiAffinityGroupCreate, + >, + ) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let opctx = + crate::context::op_context_for_external_api(&rqctx).await?; + let nexus = &apictx.context.nexus; + Err(nexus.unimplemented_todo(&opctx, Unimpl::Public).await.into()) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await + } + + async fn anti_affinity_group_update( + rqctx: RequestContext, + _query_params: Query, + _path_params: Path, + _updated_group: TypedBody, + ) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let opctx = + crate::context::op_context_for_external_api(&rqctx).await?; + let nexus = &apictx.context.nexus; + Err(nexus.unimplemented_todo(&opctx, Unimpl::Public).await.into()) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await + } + + async fn anti_affinity_group_delete( + rqctx: RequestContext, + _query_params: Query, + _path_params: Path, + ) -> Result { + let apictx = rqctx.context(); + let handler = async { + let opctx = + crate::context::op_context_for_external_api(&rqctx).await?; + let nexus = &apictx.context.nexus; + Err(nexus.unimplemented_todo(&opctx, Unimpl::Public).await.into()) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await + } + // Certificates async fn certificate_list( diff --git a/nexus/tests/output/uncovered-authz-endpoints.txt b/nexus/tests/output/uncovered-authz-endpoints.txt index 8a639f1224..c943728beb 100644 --- a/nexus/tests/output/uncovered-authz-endpoints.txt +++ b/nexus/tests/output/uncovered-authz-endpoints.txt @@ -1,10 +1,22 @@ API endpoints with no coverage in authz tests: probe_delete (delete "/experimental/v1/probes/{probe}") +affinity_group_delete (delete "/v1/affinity-groups/{affinity_group}") +affinity_group_member_instance_delete (delete "/v1/affinity-groups/{affinity_group}/members/instance/{instance}") +anti_affinity_group_delete (delete "/v1/anti-affinity-groups/{anti_affinity_group}") +anti_affinity_group_member_instance_delete (delete "/v1/anti-affinity-groups/{anti_affinity_group}/members/instance/{instance}") probe_list (get "/experimental/v1/probes") probe_view (get "/experimental/v1/probes/{probe}") support_bundle_download (get "/experimental/v1/system/support-bundles/{support_bundle}/download") support_bundle_download_file (get "/experimental/v1/system/support-bundles/{support_bundle}/download/{file}") support_bundle_index (get "/experimental/v1/system/support-bundles/{support_bundle}/index") +affinity_group_list (get "/v1/affinity-groups") +affinity_group_view (get "/v1/affinity-groups/{affinity_group}") +affinity_group_member_list (get "/v1/affinity-groups/{affinity_group}/members") +affinity_group_member_instance_view (get "/v1/affinity-groups/{affinity_group}/members/instance/{instance}") +anti_affinity_group_list (get "/v1/anti-affinity-groups") +anti_affinity_group_view (get "/v1/anti-affinity-groups/{anti_affinity_group}") +anti_affinity_group_member_list (get "/v1/anti-affinity-groups/{anti_affinity_group}/members") +anti_affinity_group_member_instance_view (get "/v1/anti-affinity-groups/{anti_affinity_group}/members/instance/{instance}") ping (get "/v1/ping") networking_switch_port_lldp_neighbors (get "/v1/system/hardware/rack-switch-port/{rack_id}/{switch_location}/{port}/lldp/neighbors") networking_switch_port_lldp_config_view (get "/v1/system/hardware/switch-port/{port}/lldp/config") @@ -16,6 +28,12 @@ device_auth_confirm (post "/device/confirm") device_access_token (post "/device/token") probe_create (post "/experimental/v1/probes") login_saml (post "/login/{silo_name}/saml/{provider_name}") +affinity_group_create (post "/v1/affinity-groups") +affinity_group_member_instance_add (post "/v1/affinity-groups/{affinity_group}/members/instance/{instance}") +anti_affinity_group_create (post "/v1/anti-affinity-groups") +anti_affinity_group_member_instance_add (post "/v1/anti-affinity-groups/{anti_affinity_group}/members/instance/{instance}") login_local (post "/v1/login/{silo_name}/local") logout (post "/v1/logout") networking_switch_port_lldp_config_update (post "/v1/system/hardware/switch-port/{port}/lldp/config") +affinity_group_update (put "/v1/affinity-groups/{affinity_group}") +anti_affinity_group_update (put "/v1/anti-affinity-groups/{anti_affinity_group}") diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 8294e23e2f..e87acd4e59 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -10,11 +10,11 @@ use base64::Engine; use chrono::{DateTime, Utc}; use http::Uri; use omicron_common::api::external::{ - AddressLotKind, AllowedSourceIps, BfdMode, BgpPeer, ByteCount, Hostname, - IdentityMetadataCreateParams, IdentityMetadataUpdateParams, - InstanceAutoRestartPolicy, InstanceCpuCount, LinkFec, LinkSpeed, Name, - NameOrId, PaginationOrder, RouteDestination, RouteTarget, SemverVersion, - TxEqConfig, UserId, + AddressLotKind, AffinityPolicy, AllowedSourceIps, BfdMode, BgpPeer, + ByteCount, FailureDomain, Hostname, IdentityMetadataCreateParams, + IdentityMetadataUpdateParams, InstanceAutoRestartPolicy, InstanceCpuCount, + LinkFec, LinkSpeed, Name, NameOrId, PaginationOrder, RouteDestination, + RouteTarget, SemverVersion, TxEqConfig, UserId, }; use omicron_common::disk::DiskVariant; use oxnet::{IpNet, Ipv4Net, Ipv6Net}; @@ -69,6 +69,8 @@ pub struct UninitializedSledId { pub part: String, } +path_param!(AffinityGroupPath, affinity_group, "affinity group"); +path_param!(AntiAffinityGroupPath, anti_affinity_group, "anti affinity group"); path_param!(ProjectPath, project, "project"); path_param!(InstancePath, instance, "instance"); path_param!(NetworkInterfacePath, interface, "network interface"); @@ -806,6 +808,70 @@ where Ok(v) } +// AFFINITY GROUPS + +/// Create-time parameters for an `AffinityGroup` +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct AffinityGroupCreate { + #[serde(flatten)] + pub identity: IdentityMetadataCreateParams, + + pub policy: AffinityPolicy, + pub failure_domain: FailureDomain, +} + +/// Updateable properties of an `AffinityGroup` +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct AffinityGroupUpdate { + #[serde(flatten)] + pub identity: IdentityMetadataUpdateParams, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct AffinityInstanceGroupMemberPath { + pub affinity_group: NameOrId, + pub instance: NameOrId, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct AntiAffinityInstanceGroupMemberPath { + pub anti_affinity_group: NameOrId, + pub instance: NameOrId, +} + +/// Create-time parameters for an `AntiAffinityGroup` +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct AntiAffinityGroupCreate { + #[serde(flatten)] + pub identity: IdentityMetadataCreateParams, + + pub policy: AffinityPolicy, + pub failure_domain: FailureDomain, +} + +/// Updateable properties of an `AntiAffinityGroup` +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct AntiAffinityGroupUpdate { + #[serde(flatten)] + pub identity: IdentityMetadataUpdateParams, +} + +#[derive(Deserialize, JsonSchema, Clone)] +pub struct AffinityGroupSelector { + /// Name or ID of the project, only required if `affinity_group` is provided as a `Name` + pub project: Option, + /// Name or ID of the Affinity Group + pub affinity_group: NameOrId, +} + +#[derive(Deserialize, JsonSchema, Clone)] +pub struct AntiAffinityGroupSelector { + /// Name or ID of the project, only required if `anti_affinity_group` is provided as a `Name` + pub project: Option, + /// Name or ID of the Anti Affinity Group + pub anti_affinity_group: NameOrId, +} + // PROJECTS /// Create-time parameters for a `Project` diff --git a/nexus/types/src/external_api/views.rs b/nexus/types/src/external_api/views.rs index 3430d06f72..fe93e4cae6 100644 --- a/nexus/types/src/external_api/views.rs +++ b/nexus/types/src/external_api/views.rs @@ -13,9 +13,9 @@ use chrono::DateTime; use chrono::Utc; use diffus::Diffus; use omicron_common::api::external::{ - AllowedSourceIps as ExternalAllowedSourceIps, ByteCount, Digest, Error, - IdentityMetadata, InstanceState, Name, ObjectIdentity, RoleName, - SimpleIdentityOrName, + AffinityPolicy, AllowedSourceIps as ExternalAllowedSourceIps, ByteCount, + Digest, Error, FailureDomain, IdentityMetadata, InstanceState, Name, + ObjectIdentity, RoleName, SimpleIdentityOrName, }; use oxnet::{Ipv4Net, Ipv6Net}; use schemars::JsonSchema; @@ -112,6 +112,24 @@ impl SimpleIdentityOrName for SiloUtilization { } } +// AFFINITY GROUPS + +#[derive(ObjectIdentity, Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct AffinityGroup { + #[serde(flatten)] + pub identity: IdentityMetadata, + pub policy: AffinityPolicy, + pub failure_domain: FailureDomain, +} + +#[derive(ObjectIdentity, Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct AntiAffinityGroup { + #[serde(flatten)] + pub identity: IdentityMetadata, + pub policy: AffinityPolicy, + pub failure_domain: FailureDomain, +} + // IDENTITY PROVIDER #[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] diff --git a/openapi/nexus.json b/openapi/nexus.json index 45f2d4787c..49b3c68246 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -683,6 +683,958 @@ } } }, + "/v1/affinity-groups": { + "get": { + "tags": [ + "affinity" + ], + "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": [ + "affinity" + ], + "summary": "Create an 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": [ + "affinity" + ], + "summary": "Fetch an 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": [ + "affinity" + ], + "summary": "Update an 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": [ + "affinity" + ], + "summary": "Delete an 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": [ + "affinity" + ], + "summary": "List members of an affinity group", + "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/IdSortMode" + } + }, + { + "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": [ + "affinity" + ], + "summary": "Fetch an 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": [ + "affinity" + ], + "summary": "Add a member to an 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": [ + "affinity" + ], + "summary": "Remove a member from an 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/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 an 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 an 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 an 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 an 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 members of an anti-affinity group", + "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/IdSortMode" + } + }, + { + "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 an 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 a member to an 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 a member from an 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/certificates": { "get": { "tags": [ @@ -11057,11 +12009,226 @@ } }, "required": [ - "first_address", - "last_address" + "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" + ] + }, + "AffinityGroup": { + "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" + }, + "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" + }, + "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", + "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" ] }, - "AddressLotBlockResultsPage": { + "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.", + "oneOf": [ + { + "description": "An instance belonging to this group, identified by UUID.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "instance" + ] + }, + "value": { + "$ref": "#/components/schemas/TypedUuidForInstanceKind" + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "AffinityGroupMemberResultsPage": { "description": "A single page of results", "type": "object", "properties": { @@ -11069,7 +12236,7 @@ "description": "list of items on this page of results", "type": "array", "items": { - "$ref": "#/components/schemas/AddressLotBlock" + "$ref": "#/components/schemas/AffinityGroupMember" } }, "next_page": { @@ -11082,104 +12249,64 @@ "items" ] }, - "AddressLotCreate": { - "description": "Parameters for creating an address lot.", + "AffinityGroupResultsPage": { + "description": "A single page of results", "type": "object", "properties": { - "blocks": { - "description": "The blocks to add along with the new address lot.", + "items": { + "description": "list of items on this page of results", "type": "array", "items": { - "$ref": "#/components/schemas/AddressLotBlockCreate" + "$ref": "#/components/schemas/AffinityGroup" } }, - "description": { + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", "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" + "items" ] }, - "AddressLotCreateResponse": { - "description": "An address lot and associated blocks resulting from creating an address lot.", + "AffinityGroupUpdate": { + "description": "Updateable properties of an `AffinityGroup`", "type": "object", "properties": { - "blocks": { - "description": "The address lot blocks that were created.", - "type": "array", - "items": { - "$ref": "#/components/schemas/AddressLotBlock" - } + "description": { + "nullable": true, + "type": "string" }, - "lot": { - "description": "The address lot that was created.", + "name": { + "nullable": true, "allOf": [ { - "$ref": "#/components/schemas/AddressLot" + "$ref": "#/components/schemas/Name" } ] } - }, - "required": [ - "blocks", - "lot" - ] + } }, - "AddressLotKind": { - "description": "The kind associated with an address lot.", + "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": "Infrastructure address lots are used for network infrastructure like addresses assigned to rack switches.", + "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": [ - "infra" + "allow" ] }, { - "description": "Pool address lots are used by IP pools.", + "description": "If the affinity request cannot be satisfied, fail explicitly.", "type": "string", "enum": [ - "pool" + "fail" ] } ] }, - "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" - ] - }, "AggregateBgpMessageHistory": { "description": "BGP message history for rack switches.", "type": "object", @@ -11284,6 +12411,162 @@ } ] }, + "AntiAffinityGroup": { + "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" + }, + "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" + }, + "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", + "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.", + "oneOf": [ + { + "description": "An instance belonging to this group, identified by UUID.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "instance" + ] + }, + "value": { + "$ref": "#/components/schemas/TypedUuidForInstanceKind" + } + }, + "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" + } + ] + } + } + }, "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": [ @@ -14635,6 +15918,18 @@ "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", @@ -21758,6 +23053,10 @@ } } }, + "TypedUuidForInstanceKind": { + "type": "string", + "format": "uuid" + }, "TypedUuidForSupportBundleKind": { "type": "string", "format": "uuid" @@ -23264,6 +24563,13 @@ } }, "tags": [ + { + "name": "affinity", + "description": "Affinity and anti-affinity groups give control over instance placement.", + "externalDocs": { + "url": "http://docs.oxide.computer/api/affinity" + } + }, { "name": "disks", "description": "Virtual disks are used to store instance-local data which includes the operating system.",