-
Notifications
You must be signed in to change notification settings - Fork 70
[nexus] Affinity and Anti-Affinity Groups #7076
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
Changes from all commits
9e736b2
472668a
f0caff8
7ed38e4
a500448
8cc9d0a
afe4175
f677d0b
729e67d
0d3162e
ddbd9d4
9df029f
80da442
e44c210
ff7054a
ee03721
6ec80b1
c3b1596
5c9252a
7e89c5c
75d106f
f4a846f
5777058
6d88ea0
12e4fd2
5e74942
0b644e9
92f1d4c
f48743c
40c9371
b261d33
1f0adbd
e05f1a7
19cd706
a6f4845
1a71813
f079d78
2a12c13
45eb03f
fc62869
ef45e2d
27fa1b2
cdc033d
e295db6
e21fac8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -982,6 +982,10 @@ impl JsonSchema for Hostname { | |
| pub enum ResourceType { | ||
| AddressLot, | ||
| AddressLotBlock, | ||
| AffinityGroup, | ||
| AffinityGroupMember, | ||
| AntiAffinityGroup, | ||
| AntiAffinityGroupMember, | ||
| AllowList, | ||
| BackgroundTask, | ||
| BgpConfig, | ||
|
|
@@ -1312,6 +1316,56 @@ pub enum InstanceAutoRestartPolicy { | |
| BestEffort, | ||
| } | ||
|
|
||
| // 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, | ||
smklein marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /// 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, | ||
| } | ||
|
|
||
| #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] | ||
| #[serde(tag = "type", content = "value", rename_all = "snake_case")] | ||
| pub enum AffinityGroupMember { | ||
| Instance(Uuid), | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know this is an "enum of one", but RFD 522 discusses having anti-affinity groups which contain "either instances or affinity groups". I figured I'd just define these as "members" to be flexible for future work
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We respond with a member ID here, which isn't the most readable way for the user to see the group members. They'd need to then fetch each to get the name – and whilst we could string those queries on the console, the CLI wouldn't do that. Any thoughts? Perhaps responding with: ID, name and project?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
|
|
||
| impl SimpleIdentity for AffinityGroupMember { | ||
| fn id(&self) -> Uuid { | ||
| match self { | ||
| AffinityGroupMember::Instance(id) => *id, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] | ||
| #[serde(tag = "type", content = "value", rename_all = "snake_case")] | ||
| pub enum AntiAffinityGroupMember { | ||
| Instance(Uuid), | ||
| } | ||
|
|
||
| impl SimpleIdentity for AntiAffinityGroupMember { | ||
| fn id(&self) -> Uuid { | ||
| match self { | ||
| AntiAffinityGroupMember::Instance(id) => *id, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // DISKS | ||
|
|
||
| /// View of a Disk | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,260 @@ | ||
| // 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/5.0/. | ||
|
|
||
| // Copyright 2024 Oxide Computer Company | ||
|
|
||
| //! Database representation of affinity and anti-affinity groups | ||
|
|
||
| use super::impl_enum_type; | ||
| use super::Name; | ||
| use crate::schema::affinity_group; | ||
| use crate::schema::affinity_group_instance_membership; | ||
| use crate::schema::anti_affinity_group; | ||
| use crate::schema::anti_affinity_group_instance_membership; | ||
| use crate::typed_uuid::DbTypedUuid; | ||
| use chrono::{DateTime, Utc}; | ||
| use db_macros::Resource; | ||
| use nexus_types::external_api::params; | ||
| use nexus_types::external_api::views; | ||
| use omicron_common::api::external; | ||
| use omicron_common::api::external::IdentityMetadata; | ||
| use omicron_uuid_kinds::AffinityGroupKind; | ||
| use omicron_uuid_kinds::AffinityGroupUuid; | ||
| use omicron_uuid_kinds::AntiAffinityGroupKind; | ||
| use omicron_uuid_kinds::AntiAffinityGroupUuid; | ||
| use omicron_uuid_kinds::GenericUuid; | ||
| use omicron_uuid_kinds::InstanceKind; | ||
| use omicron_uuid_kinds::InstanceUuid; | ||
| use uuid::Uuid; | ||
|
|
||
| impl_enum_type!( | ||
| #[derive(SqlType, Debug, QueryId)] | ||
| #[diesel(postgres_type(name = "affinity_policy", schema = "public"))] | ||
| pub struct AffinityPolicyEnum; | ||
|
|
||
| #[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, PartialEq, Eq, Ord, PartialOrd)] | ||
| #[diesel(sql_type = AffinityPolicyEnum)] | ||
| pub enum AffinityPolicy; | ||
|
|
||
| // Enum values | ||
| Fail => b"fail" | ||
| Allow => b"allow" | ||
| ); | ||
|
|
||
| impl From<AffinityPolicy> for external::AffinityPolicy { | ||
| fn from(policy: AffinityPolicy) -> Self { | ||
| match policy { | ||
| AffinityPolicy::Fail => Self::Fail, | ||
| AffinityPolicy::Allow => Self::Allow, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl From<external::AffinityPolicy> for AffinityPolicy { | ||
| fn from(policy: external::AffinityPolicy) -> Self { | ||
| match policy { | ||
| external::AffinityPolicy::Fail => Self::Fail, | ||
| external::AffinityPolicy::Allow => Self::Allow, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl_enum_type!( | ||
| #[derive(SqlType, Debug)] | ||
| #[diesel(postgres_type(name = "failure_domain", schema = "public"))] | ||
| pub struct FailureDomainEnum; | ||
|
|
||
| #[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, PartialEq)] | ||
| #[diesel(sql_type = FailureDomainEnum)] | ||
| pub enum FailureDomain; | ||
|
|
||
| // Enum values | ||
| Sled => b"sled" | ||
| ); | ||
|
|
||
| impl From<FailureDomain> for external::FailureDomain { | ||
| fn from(domain: FailureDomain) -> Self { | ||
| match domain { | ||
| FailureDomain::Sled => Self::Sled, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl From<external::FailureDomain> for FailureDomain { | ||
| fn from(domain: external::FailureDomain) -> Self { | ||
| match domain { | ||
| external::FailureDomain::Sled => Self::Sled, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[derive( | ||
| Queryable, Insertable, Clone, Debug, Resource, Selectable, PartialEq, | ||
| )] | ||
| #[diesel(table_name = affinity_group)] | ||
| pub struct AffinityGroup { | ||
| #[diesel(embed)] | ||
| pub identity: AffinityGroupIdentity, | ||
| pub project_id: Uuid, | ||
| pub policy: AffinityPolicy, | ||
| pub failure_domain: FailureDomain, | ||
| } | ||
|
|
||
| impl AffinityGroup { | ||
| pub fn new(project_id: Uuid, params: params::AffinityGroupCreate) -> Self { | ||
| Self { | ||
| identity: AffinityGroupIdentity::new( | ||
| Uuid::new_v4(), | ||
| params.identity, | ||
| ), | ||
| project_id, | ||
| policy: params.policy.into(), | ||
| failure_domain: params.failure_domain.into(), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl From<AffinityGroup> for views::AffinityGroup { | ||
| fn from(group: AffinityGroup) -> Self { | ||
| let identity = IdentityMetadata { | ||
| id: group.identity.id, | ||
| name: group.identity.name.into(), | ||
| description: group.identity.description, | ||
| time_created: group.identity.time_created, | ||
| time_modified: group.identity.time_modified, | ||
| }; | ||
| Self { | ||
| identity, | ||
| policy: group.policy.into(), | ||
| failure_domain: group.failure_domain.into(), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Describes a set of updates for the [`AffinityGroup`] model. | ||
| #[derive(AsChangeset)] | ||
| #[diesel(table_name = affinity_group)] | ||
| pub struct AffinityGroupUpdate { | ||
| pub name: Option<Name>, | ||
| pub description: Option<String>, | ||
| pub time_modified: DateTime<Utc>, | ||
| } | ||
|
|
||
| impl From<params::AffinityGroupUpdate> for AffinityGroupUpdate { | ||
| fn from(params: params::AffinityGroupUpdate) -> Self { | ||
| Self { | ||
| name: params.identity.name.map(Name), | ||
| description: params.identity.description, | ||
| time_modified: Utc::now(), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[derive( | ||
| Queryable, Insertable, Clone, Debug, Resource, Selectable, PartialEq, | ||
| )] | ||
| #[diesel(table_name = anti_affinity_group)] | ||
| pub struct AntiAffinityGroup { | ||
| #[diesel(embed)] | ||
| identity: AntiAffinityGroupIdentity, | ||
| pub project_id: Uuid, | ||
| pub policy: AffinityPolicy, | ||
| pub failure_domain: FailureDomain, | ||
| } | ||
|
|
||
| impl AntiAffinityGroup { | ||
| pub fn new( | ||
| project_id: Uuid, | ||
| params: params::AntiAffinityGroupCreate, | ||
| ) -> Self { | ||
| Self { | ||
| identity: AntiAffinityGroupIdentity::new( | ||
| Uuid::new_v4(), | ||
| params.identity, | ||
| ), | ||
| project_id, | ||
| policy: params.policy.into(), | ||
| failure_domain: params.failure_domain.into(), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl From<AntiAffinityGroup> for views::AntiAffinityGroup { | ||
| fn from(group: AntiAffinityGroup) -> Self { | ||
| let identity = IdentityMetadata { | ||
| id: group.identity.id, | ||
| name: group.identity.name.into(), | ||
| description: group.identity.description, | ||
| time_created: group.identity.time_created, | ||
| time_modified: group.identity.time_modified, | ||
| }; | ||
| Self { | ||
| identity, | ||
| policy: group.policy.into(), | ||
| failure_domain: group.failure_domain.into(), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Describes a set of updates for the [`AntiAffinityGroup`] model. | ||
| #[derive(AsChangeset)] | ||
| #[diesel(table_name = anti_affinity_group)] | ||
| pub struct AntiAffinityGroupUpdate { | ||
| pub name: Option<Name>, | ||
| pub description: Option<String>, | ||
| pub time_modified: DateTime<Utc>, | ||
| } | ||
|
|
||
| impl From<params::AntiAffinityGroupUpdate> for AntiAffinityGroupUpdate { | ||
| fn from(params: params::AntiAffinityGroupUpdate) -> Self { | ||
| Self { | ||
| name: params.identity.name.map(Name), | ||
| description: params.identity.description, | ||
| time_modified: Utc::now(), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[derive(Queryable, Insertable, Clone, Debug, Selectable)] | ||
| #[diesel(table_name = affinity_group_instance_membership)] | ||
| pub struct AffinityGroupInstanceMembership { | ||
| pub group_id: DbTypedUuid<AffinityGroupKind>, | ||
| pub instance_id: DbTypedUuid<InstanceKind>, | ||
| } | ||
|
|
||
| impl AffinityGroupInstanceMembership { | ||
| pub fn new(group_id: AffinityGroupUuid, instance_id: InstanceUuid) -> Self { | ||
| Self { group_id: group_id.into(), instance_id: instance_id.into() } | ||
| } | ||
| } | ||
|
|
||
| impl From<AffinityGroupInstanceMembership> for external::AffinityGroupMember { | ||
| fn from(member: AffinityGroupInstanceMembership) -> Self { | ||
| Self::Instance(member.instance_id.into_untyped_uuid()) | ||
| } | ||
| } | ||
|
|
||
| #[derive(Queryable, Insertable, Clone, Debug, Selectable)] | ||
| #[diesel(table_name = anti_affinity_group_instance_membership)] | ||
| pub struct AntiAffinityGroupInstanceMembership { | ||
| pub group_id: DbTypedUuid<AntiAffinityGroupKind>, | ||
| pub instance_id: DbTypedUuid<InstanceKind>, | ||
| } | ||
|
|
||
| impl AntiAffinityGroupInstanceMembership { | ||
| pub fn new( | ||
| group_id: AntiAffinityGroupUuid, | ||
| instance_id: InstanceUuid, | ||
| ) -> Self { | ||
| Self { group_id: group_id.into(), instance_id: instance_id.into() } | ||
| } | ||
| } | ||
|
|
||
| impl From<AntiAffinityGroupInstanceMembership> | ||
| for external::AntiAffinityGroupMember | ||
| { | ||
| fn from(member: AntiAffinityGroupInstanceMembership) -> Self { | ||
| Self::Instance(member.instance_id.into_untyped_uuid()) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm using this in
http_entrypoints.rsto paginate over "group members", which only have UUIDs, not names.