Skip to content

(1/5) [nexus] Add Affinity/Anti-Affinity Groups to API (unimplemented) #7443

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions common/src/api/external/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,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;
Expand Down Expand Up @@ -994,6 +996,10 @@ impl JsonSchema for Hostname {
pub enum ResourceType {
AddressLot,
AddressLotBlock,
AffinityGroup,
AffinityGroupMember,
AntiAffinityGroup,
AntiAffinityGroupMember,
AllowList,
BackgroundTask,
BgpConfig,
Expand Down Expand Up @@ -1324,6 +1330,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
Expand Down
21 changes: 21 additions & 0 deletions nexus/external-api/output/nexus_tags.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
225 changes: 225 additions & 0 deletions nexus/external-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -1257,6 +1264,224 @@ pub trait NexusExternalApi {
disk_to_detach: TypedBody<params::DiskPath>,
) -> Result<HttpResponseAccepted<Disk>, HttpError>;

// Affinity Groups

/// List affinity groups
#[endpoint {
method = GET,
path = "/v1/affinity-groups",
tags = ["affinity"],
}]
async fn affinity_group_list(
rqctx: RequestContext<Self::Context>,
query_params: Query<PaginatedByNameOrId<params::ProjectSelector>>,
) -> Result<HttpResponseOk<ResultsPage<views::AffinityGroup>>, HttpError>;

/// Fetch an affinity group
#[endpoint {
method = GET,
path = "/v1/affinity-groups/{affinity_group}",
tags = ["affinity"],
}]
async fn affinity_group_view(
rqctx: RequestContext<Self::Context>,
query_params: Query<params::OptionalProjectSelector>,
path_params: Path<params::AffinityGroupPath>,
) -> Result<HttpResponseOk<views::AffinityGroup>, 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<Self::Context>,
query_params: Query<PaginatedById<params::OptionalProjectSelector>>,
path_params: Path<params::AffinityGroupPath>,
) -> Result<HttpResponseOk<ResultsPage<AffinityGroupMember>>, 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<Self::Context>,
query_params: Query<params::OptionalProjectSelector>,
path_params: Path<params::AffinityInstanceGroupMemberPath>,
) -> Result<HttpResponseOk<AffinityGroupMember>, 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<Self::Context>,
query_params: Query<params::OptionalProjectSelector>,
path_params: Path<params::AffinityInstanceGroupMemberPath>,
) -> Result<HttpResponseCreated<AffinityGroupMember>, 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<Self::Context>,
query_params: Query<params::OptionalProjectSelector>,
path_params: Path<params::AffinityInstanceGroupMemberPath>,
) -> Result<HttpResponseDeleted, HttpError>;

/// Create an affinity group
#[endpoint {
method = POST,
path = "/v1/affinity-groups",
tags = ["affinity"],
}]
async fn affinity_group_create(
rqctx: RequestContext<Self::Context>,
query_params: Query<params::ProjectSelector>,
new_affinity_group_params: TypedBody<params::AffinityGroupCreate>,
) -> Result<HttpResponseCreated<views::AffinityGroup>, HttpError>;

/// Update an affinity group
#[endpoint {
method = PUT,
path = "/v1/affinity-groups/{affinity_group}",
tags = ["affinity"],
}]
async fn affinity_group_update(
rqctx: RequestContext<Self::Context>,
query_params: Query<params::OptionalProjectSelector>,
path_params: Path<params::AffinityGroupPath>,
updated_group: TypedBody<params::AffinityGroupUpdate>,
) -> Result<HttpResponseOk<views::AffinityGroup>, HttpError>;

/// Delete an affinity group
#[endpoint {
method = DELETE,
path = "/v1/affinity-groups/{affinity_group}",
tags = ["affinity"],
}]
async fn affinity_group_delete(
rqctx: RequestContext<Self::Context>,
query_params: Query<params::OptionalProjectSelector>,
path_params: Path<params::AffinityGroupPath>,
) -> Result<HttpResponseDeleted, HttpError>;

/// List anti-affinity groups
#[endpoint {
method = GET,
path = "/v1/anti-affinity-groups",
tags = ["affinity"],
}]
async fn anti_affinity_group_list(
rqctx: RequestContext<Self::Context>,
query_params: Query<PaginatedByNameOrId<params::ProjectSelector>>,
) -> Result<HttpResponseOk<ResultsPage<views::AntiAffinityGroup>>, 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<Self::Context>,
query_params: Query<params::OptionalProjectSelector>,
path_params: Path<params::AntiAffinityGroupPath>,
) -> Result<HttpResponseOk<views::AntiAffinityGroup>, 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<Self::Context>,
query_params: Query<PaginatedById<params::OptionalProjectSelector>>,
path_params: Path<params::AntiAffinityGroupPath>,
) -> Result<HttpResponseOk<ResultsPage<AntiAffinityGroupMember>>, 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<Self::Context>,
query_params: Query<params::OptionalProjectSelector>,
path_params: Path<params::AntiAffinityInstanceGroupMemberPath>,
) -> Result<HttpResponseOk<AntiAffinityGroupMember>, 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<Self::Context>,
query_params: Query<params::OptionalProjectSelector>,
path_params: Path<params::AntiAffinityInstanceGroupMemberPath>,
) -> Result<HttpResponseCreated<AntiAffinityGroupMember>, 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<Self::Context>,
query_params: Query<params::OptionalProjectSelector>,
path_params: Path<params::AntiAffinityInstanceGroupMemberPath>,
) -> Result<HttpResponseDeleted, HttpError>;

/// Create an anti-affinity group
#[endpoint {
method = POST,
path = "/v1/anti-affinity-groups",
tags = ["affinity"],
}]
async fn anti_affinity_group_create(
rqctx: RequestContext<Self::Context>,
query_params: Query<params::ProjectSelector>,
new_affinity_group_params: TypedBody<params::AntiAffinityGroupCreate>,
) -> Result<HttpResponseCreated<views::AntiAffinityGroup>, 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<Self::Context>,
query_params: Query<params::OptionalProjectSelector>,
path_params: Path<params::AntiAffinityGroupPath>,
updated_group: TypedBody<params::AntiAffinityGroupUpdate>,
) -> Result<HttpResponseOk<views::AntiAffinityGroup>, 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<Self::Context>,
query_params: Query<params::OptionalProjectSelector>,
path_params: Path<params::AntiAffinityGroupPath>,
) -> Result<HttpResponseDeleted, HttpError>;

// Certificates

/// List certificates for external endpoints
Expand Down
Loading
Loading