Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
9e736b2
[wip] Sketching out API, DB models for affinity and anti-affinity
smklein Nov 15, 2024
472668a
affinity distance -> failure domain
smklein Nov 18, 2024
f0caff8
Docs for affinity policy
smklein Nov 18, 2024
7ed38e4
failure domain docs
smklein Nov 18, 2024
a500448
Query params
smklein Nov 18, 2024
8cc9d0a
Merge branch 'main' into affinity
smklein Nov 20, 2024
afe4175
Plumbing through lookup, auth
smklein Nov 20, 2024
f677d0b
Wiring up more of the API, db side still unimplemented
smklein Nov 21, 2024
729e67d
CRUD impl for affinity groups in db
smklein Nov 21, 2024
0d3162e
Merge branch 'main' into affinity
smklein Nov 22, 2024
ddbd9d4
schema tweaks
smklein Nov 22, 2024
9df029f
starting tests, adding indexes
smklein Nov 22, 2024
80da442
Add affinity tests, cleanup during db deletion, OSO registration
smklein Nov 23, 2024
e44c210
Merge branch 'main' into affinity
smklein Nov 25, 2024
ff7054a
Merge branch 'main' into affinity
smklein Jan 15, 2025
ee03721
Merge branch 'main' into affinity
smklein Jan 16, 2025
6ec80b1
Integrated affinity groups into instance selection, barely started te…
smklein Jan 18, 2025
c3b1596
Expectorate tests
smklein Jan 18, 2025
5c9252a
Improve queries, testing
smklein Jan 21, 2025
7e89c5c
Fixing bug (difference not symmetric) and improving testing
smklein Jan 22, 2025
75d106f
preference tweaking, more tests
smklein Jan 22, 2025
f4a846f
more tests
smklein Jan 23, 2025
5777058
more testing, better errors internally
smklein Jan 23, 2025
6d88ea0
cleanup
smklein Jan 23, 2025
12e4fd2
Add tag for affinity
smklein Jan 23, 2025
5e74942
Making the nexus API work
smklein Jan 23, 2025
0b644e9
Allow instance selection by name or UUID
smklein Jan 24, 2025
92f1d4c
Group viewing, group member viewing
smklein Jan 24, 2025
f48743c
Updating groups
smklein Jan 24, 2025
40c9371
Expand db test suite for anti-affinity groups
smklein Jan 24, 2025
b261d33
Align schema changes with dbinit.sql updates
smklein Jan 24, 2025
1f0adbd
Patching IAM test
smklein Jan 24, 2025
e05f1a7
Add affinity endpoints for unauthorized tests
smklein Jan 24, 2025
19cd706
Merge branch 'main' into affinity
smklein Jan 24, 2025
a6f4845
starting integration tests, instance UUID vs vmm UUID
smklein Jan 28, 2025
1a71813
db upgrade
smklein Jan 28, 2025
f079d78
New SledResource constructor
smklein Jan 28, 2025
2a12c13
Expanding tests, tweaking responses
smklein Jan 28, 2025
45eb03f
Better selection tests for affinity
smklein Jan 29, 2025
fc62869
Anti-affinity integration tests
smklein Jan 29, 2025
ef45e2d
clippy
smklein Jan 29, 2025
27fa1b2
Merge branch 'main' into affinity
smklein Jan 29, 2025
cdc033d
Patch some idempotency tests
smklein Jan 29, 2025
e295db6
Better UUID typing
smklein Jan 29, 2025
e21fac8
Merge branch 'main' into affinity
smklein Jan 30, 2025
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
13 changes: 13 additions & 0 deletions common/src/api/external/http_pagination.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,19 @@ pub type PaginatedByNameOrId<Selector = ()> = PaginationParams<
pub type PageSelectorByNameOrId<Selector = ()> =
PageSelector<ScanByNameOrId<Selector>, NameOrId>;

pub fn id_pagination<'a, Selector>(
Copy link
Collaborator Author

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.rs to paginate over "group members", which only have UUIDs, not names.

pag_params: &'a DataPageParams<Uuid>,
scan_params: &'a ScanById<Selector>,
) -> Result<PaginatedBy<'a>, HttpError>
where
Selector:
Clone + Debug + DeserializeOwned + JsonSchema + PartialEq + Serialize,
{
match scan_params.sort_by {
IdSortMode::IdAscending => Ok(PaginatedBy::Id(pag_params.clone())),
}
}

pub fn name_or_id_pagination<'a, Selector>(
pag_params: &'a DataPageParams<NameOrId>,
scan_params: &'a ScanByNameOrId<Selector>,
Expand Down
54 changes: 54 additions & 0 deletions common/src/api/external/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,10 @@ impl JsonSchema for Hostname {
pub enum ResourceType {
AddressLot,
AddressLotBlock,
AffinityGroup,
AffinityGroupMember,
AntiAffinityGroup,
AntiAffinityGroupMember,
AllowList,
BackgroundTask,
BgpConfig,
Expand Down Expand Up @@ -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,

/// 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),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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

Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed #7625 to track this.

#7572 will complicate this a little bit, when we do add "group memberships of things that are not just instances".

Since affinity groups are already project-scoped, I don't really want to return the project name for each. But the names of members seems totally reasonable.

}

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
Expand Down
1 change: 1 addition & 0 deletions dev-tools/omdb/src/bin/omdb/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6191,6 +6191,7 @@ async fn cmd_db_vmm_info(
rss_ram: ByteCount(rss),
reservoir_ram: ByteCount(reservoir),
},
instance_id: _,
} = resource;
const SLED_ID: &'static str = "sled ID";
const THREADS: &'static str = "hardware threads";
Expand Down
16 changes: 16 additions & 0 deletions nexus/auth/src/authz/api_resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,22 @@ authz_resource! {
polar_snippet = InProject,
}

authz_resource! {
name = "AffinityGroup",
parent = "Project",
primary_key = Uuid,
roles_allowed = false,
polar_snippet = InProject,
}

authz_resource! {
name = "AntiAffinityGroup",
parent = "Project",
primary_key = Uuid,
roles_allowed = false,
polar_snippet = InProject,
}

authz_resource! {
name = "InstanceNetworkInterface",
parent = "Instance",
Expand Down
2 changes: 2 additions & 0 deletions nexus/auth/src/authz/oso_generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ pub fn make_omicron_oso(log: &slog::Logger) -> Result<OsoInit, anyhow::Error> {
Disk::init(),
Snapshot::init(),
ProjectImage::init(),
AffinityGroup::init(),
AntiAffinityGroup::init(),
Instance::init(),
IpPool::init(),
InstanceNetworkInterface::init(),
Expand Down
260 changes: 260 additions & 0 deletions nexus/db-model/src/affinity.rs
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())
}
}
Loading
Loading