From 3346a33b262334f9bccfffe3aed5202972c34711 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 4 Dec 2023 11:29:52 -0500 Subject: [PATCH] Create quotas during silo creation --- nexus/db-model/src/quota.rs | 32 +++++++++++++- nexus/db-queries/src/db/datastore/quota.rs | 49 +++++++++++++--------- nexus/db-queries/src/db/datastore/silo.rs | 14 +++++++ nexus/db-queries/src/db/fixed_data/silo.rs | 12 ++++++ nexus/src/app/quota.rs | 2 +- nexus/src/app/rack.rs | 2 + nexus/test-utils/src/resource_helpers.rs | 5 +++ nexus/types/src/external_api/params.rs | 12 +++++- 8 files changed, 104 insertions(+), 24 deletions(-) diff --git a/nexus/db-model/src/quota.rs b/nexus/db-model/src/quota.rs index da0d2fdc64..277ebb7888 100644 --- a/nexus/db-model/src/quota.rs +++ b/nexus/db-model/src/quota.rs @@ -1,11 +1,18 @@ use crate::schema::silo_quotas; use chrono::{DateTime, Utc}; -use nexus_types::external_api::views; +use nexus_types::external_api::{params, views}; use serde::{Deserialize, Serialize}; use uuid::Uuid; #[derive( - Queryable, Insertable, Debug, Clone, Selectable, Serialize, Deserialize, + Queryable, + Insertable, + Debug, + Clone, + Selectable, + Serialize, + Deserialize, + AsChangeset, )] #[diesel(table_name = silo_quotas)] pub struct SiloQuotas { @@ -54,3 +61,24 @@ impl From for SiloQuotas { } } } + +// Describes a set of updates for the [`SiloQuotas`] model. +#[derive(AsChangeset)] +#[diesel(table_name = silo_quotas)] +pub struct SiloQuotasUpdate { + pub cpus: Option, + pub memory: Option, + pub storage: Option, + pub time_modified: DateTime, +} + +impl From for SiloQuotasUpdate { + fn from(params: params::SiloQuotasUpdate) -> Self { + Self { + cpus: params.cpus, + memory: params.memory, + storage: params.storage, + time_modified: Utc::now(), + } + } +} diff --git a/nexus/db-queries/src/db/datastore/quota.rs b/nexus/db-queries/src/db/datastore/quota.rs index 07cd4c1f86..894fd94ff3 100644 --- a/nexus/db-queries/src/db/datastore/quota.rs +++ b/nexus/db-queries/src/db/datastore/quota.rs @@ -5,12 +5,13 @@ use crate::db; use crate::db::error::public_error_from_diesel; use crate::db::error::ErrorHandler; use crate::db::pagination::paginated; +use crate::db::pool::DbConnection; +use crate::db::TransactionError; +use async_bb8_diesel::AsyncConnection; use async_bb8_diesel::AsyncRunQueryDsl; -use chrono::Utc; use diesel::prelude::*; use nexus_db_model::SiloQuotas; -use nexus_types::external_api::params; -use omicron_common::api::external::CreateResult; +use nexus_db_model::SiloQuotasUpdate; use omicron_common::api::external::DataPageParams; use omicron_common::api::external::Error; use omicron_common::api::external::ListResultVec; @@ -24,46 +25,54 @@ impl DataStore { pub async fn silo_quotas_create( &self, opctx: &OpContext, + conn: &async_bb8_diesel::Connection, authz_silo: &authz::Silo, quotas: SiloQuotas, - ) -> CreateResult { + ) -> Result<(), Error> { opctx.authorize(authz::Action::Modify, authz_silo).await?; let silo_id = authz_silo.id(); use db::schema::silo_quotas::dsl; - diesel::insert_into(dsl::silo_quotas) - .values(quotas) - .returning(SiloQuotas::as_returning()) - .get_result_async(&*self.pool_connection_authorized(opctx).await?) - .await - .map_err(|e| { - public_error_from_diesel( + let result = conn + .transaction_async(|c| async move { + diesel::insert_into(dsl::silo_quotas) + .values(quotas) + .execute_async(&c) + .await + .map_err(TransactionError::CustomError) + }) + .await; + + match result { + Ok(_) => Ok(()), + Err(TransactionError::CustomError(e)) => { + // TODO: Is this the right error handler? + Err(public_error_from_diesel(e, ErrorHandler::Server)) + } + Err(TransactionError::Database(e)) => { + Err(public_error_from_diesel( e, ErrorHandler::Conflict( ResourceType::SiloQuotas, &silo_id.to_string(), ), - ) - }) + )) + } + } } pub async fn silo_update_quota( &self, opctx: &OpContext, authz_silo: &authz::Silo, - updates: params::SiloQuotasUpdate, + updates: SiloQuotasUpdate, ) -> UpdateResult { opctx.authorize(authz::Action::Modify, authz_silo).await?; use db::schema::silo_quotas::dsl; let silo_id = authz_silo.id(); diesel::update(dsl::silo_quotas) .filter(dsl::silo_id.eq(silo_id)) - .set(( - dsl::time_modified.eq(Utc::now()), - dsl::cpus.eq(updates.cpus), - dsl::memory.eq(updates.memory), - dsl::storage.eq(updates.storage), - )) + .set(updates) .returning(SiloQuotas::as_returning()) .get_result_async(&*self.pool_connection_authorized(opctx).await?) .await diff --git a/nexus/db-queries/src/db/datastore/silo.rs b/nexus/db-queries/src/db/datastore/silo.rs index ec3658c067..9d53b18496 100644 --- a/nexus/db-queries/src/db/datastore/silo.rs +++ b/nexus/db-queries/src/db/datastore/silo.rs @@ -27,6 +27,7 @@ use chrono::Utc; use diesel::prelude::*; use nexus_db_model::Certificate; use nexus_db_model::ServiceKind; +use nexus_db_model::SiloQuotas; use nexus_types::external_api::params; use nexus_types::external_api::shared; use nexus_types::external_api::shared::SiloRole; @@ -255,6 +256,19 @@ impl DataStore { self.dns_update(nexus_opctx, &conn, dns_update).await?; + self.silo_quotas_create( + opctx, + &conn, + &authz_silo, + SiloQuotas::new( + authz_silo.id(), + new_silo_params.quotas.cpus, + new_silo_params.quotas.memory, + new_silo_params.quotas.storage, + ), + ) + .await?; + Ok(silo) }) .await diff --git a/nexus/db-queries/src/db/fixed_data/silo.rs b/nexus/db-queries/src/db/fixed_data/silo.rs index d32c4211e9..d63c719567 100644 --- a/nexus/db-queries/src/db/fixed_data/silo.rs +++ b/nexus/db-queries/src/db/fixed_data/silo.rs @@ -24,6 +24,12 @@ lazy_static! { name: "default-silo".parse().unwrap(), description: "default silo".to_string(), }, + // TODO: Should the default silo have a quota? If so, what should the defaults be? + quotas: params::SiloQuotasCreate { + cpus: 0, + memory: 0, + storage: 0, + }, discoverable: false, identity_mode: shared::SiloIdentityMode::LocalOnly, admin_group_name: None, @@ -49,6 +55,12 @@ lazy_static! { name: "oxide-internal".parse().unwrap(), description: "Built-in internal Silo.".to_string(), }, + // TODO: Should the internal silo have a quota? If so, what should the defaults be? + quotas: params::SiloQuotasCreate { + cpus: 0, + memory: 0, + storage: 0, + }, discoverable: false, identity_mode: shared::SiloIdentityMode::LocalOnly, admin_group_name: None, diff --git a/nexus/src/app/quota.rs b/nexus/src/app/quota.rs index fa76af9f24..f59069a9ab 100644 --- a/nexus/src/app/quota.rs +++ b/nexus/src/app/quota.rs @@ -43,7 +43,7 @@ impl super::Nexus { let (.., authz_silo) = silo_lookup.lookup_for(authz::Action::Modify).await?; self.db_datastore - .silo_update_quota(opctx, &authz_silo, updates.clone()) + .silo_update_quota(opctx, &authz_silo, updates.clone().into()) .await } } diff --git a/nexus/src/app/rack.rs b/nexus/src/app/rack.rs index 1c2e49e260..9e452aaeb1 100644 --- a/nexus/src/app/rack.rs +++ b/nexus/src/app/rack.rs @@ -193,6 +193,8 @@ impl super::Nexus { name: request.recovery_silo.silo_name, description: "built-in recovery Silo".to_string(), }, + // TODO: Should the recovery silo have a quota? If so, what should the defaults be? + quotas: params::SiloQuotasCreate { cpus: 0, memory: 0, storage: 0 }, discoverable: false, identity_mode: SiloIdentityMode::LocalOnly, admin_group_name: None, diff --git a/nexus/test-utils/src/resource_helpers.rs b/nexus/test-utils/src/resource_helpers.rs index 2368c3f568..5687e19c69 100644 --- a/nexus/test-utils/src/resource_helpers.rs +++ b/nexus/test-utils/src/resource_helpers.rs @@ -262,6 +262,11 @@ pub async fn create_silo( name: silo_name.parse().unwrap(), description: "a silo".to_string(), }, + quotas: params::SiloQuotasCreate { + cpus: 36, + memory: 1000, + storage: 100000, + }, discoverable, identity_mode, admin_group_name: None, diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index c4feace386..bfc9b318c1 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -262,6 +262,9 @@ pub struct SiloCreate { /// endpoints. These should be valid for the Silo's DNS name(s). pub tls_certificates: Vec, + /// Initial quotas for the new Silo + pub quotas: SiloQuotasCreate, + /// Mapping of which Fleet roles are conferred by each Silo role /// /// The default is that no Fleet roles are conferred by any Silo roles @@ -272,12 +275,19 @@ pub struct SiloCreate { } #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -pub struct SiloQuotasUpdate { +pub struct SiloQuotasCreate { pub cpus: i64, pub memory: i64, pub storage: i64, } +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct SiloQuotasUpdate { + pub cpus: Option, + pub memory: Option, + pub storage: Option, +} + /// Create-time parameters for a `User` #[derive(Clone, Deserialize, Serialize, JsonSchema)] pub struct UserCreate {