diff --git a/nexus/src/app/mod.rs b/nexus/src/app/mod.rs index 5b4a10b60a3..de05a846f0b 100644 --- a/nexus/src/app/mod.rs +++ b/nexus/src/app/mod.rs @@ -10,6 +10,7 @@ use crate::config; use crate::context::OpContext; use crate::db; use crate::populate::populate_start; +use crate::populate::PopulateArgs; use crate::populate::PopulateStatus; use crate::saga_interface::SagaContext; use anyhow::anyhow; @@ -55,15 +56,12 @@ pub struct Nexus { /// uuid for this nexus instance. id: Uuid, - /// uuid for this rack (TODO should also be in persistent storage) + /// uuid for this rack rack_id: Uuid, /// general server log log: Logger, - /// cached rack identity metadata - api_rack_identity: db::model::RackIdentity, - /// persistent storage for resources in the control plane db_datastore: Arc, @@ -140,14 +138,18 @@ impl Nexus { authn::Context::internal_db_init(), Arc::clone(&db_datastore), ); - let populate_status = - populate_start(populate_ctx, Arc::clone(&db_datastore)); + + let populate_args = PopulateArgs::new(rack_id); + let populate_status = populate_start( + populate_ctx, + Arc::clone(&db_datastore), + populate_args, + ); let nexus = Nexus { id: config.deployment.id, rack_id, log: log.new(o!()), - api_rack_identity: db::model::RackIdentity::new(rack_id), db_datastore: Arc::clone(&db_datastore), authz: Arc::clone(&authz), sec_client: Arc::clone(&sec_client), diff --git a/nexus/src/app/rack.rs b/nexus/src/app/rack.rs index a9a10a616aa..dcc7ce92dbc 100644 --- a/nexus/src/app/rack.rs +++ b/nexus/src/app/rack.rs @@ -7,40 +7,21 @@ use crate::authz; use crate::context::OpContext; use crate::db; +use crate::db::lookup::LookupPath; use crate::internal_api::params::ServicePutRequest; -use futures::future::ready; -use futures::StreamExt; use omicron_common::api::external::DataPageParams; use omicron_common::api::external::Error; -use omicron_common::api::external::ListResult; +use omicron_common::api::external::ListResultVec; use omicron_common::api::external::LookupResult; -use omicron_common::api::external::LookupType; -use omicron_common::api::external::ResourceType; use uuid::Uuid; impl super::Nexus { - pub(crate) fn as_rack(&self) -> db::model::Rack { - db::model::Rack { - identity: self.api_rack_identity.clone(), - initialized: true, - tuf_base_url: None, - } - } - pub async fn racks_list( &self, opctx: &OpContext, pagparams: &DataPageParams<'_, Uuid>, - ) -> ListResult { - opctx.authorize(authz::Action::Read, &authz::FLEET).await?; - - if let Some(marker) = pagparams.marker { - if *marker >= self.rack_id { - return Ok(futures::stream::empty().boxed()); - } - } - - Ok(futures::stream::once(ready(Ok(self.as_rack()))).boxed()) + ) -> ListResultVec { + self.db_datastore.rack_list(&opctx, pagparams).await } pub async fn rack_lookup( @@ -48,18 +29,25 @@ impl super::Nexus { opctx: &OpContext, rack_id: &Uuid, ) -> LookupResult { - let authz_rack = authz::Rack::new( - authz::FLEET, - *rack_id, - LookupType::ById(*rack_id), - ); - opctx.authorize(authz::Action::Read, &authz_rack).await?; + let (.., db_rack) = LookupPath::new(opctx, &self.db_datastore) + .rack_id(*rack_id) + .fetch() + .await?; + Ok(db_rack) + } - if *rack_id == self.rack_id { - Ok(self.as_rack()) - } else { - Err(Error::not_found_by_id(ResourceType::Rack, rack_id)) - } + /// Ensures that a rack exists in the DB. + /// + /// If the rack already exists, this function is a no-op. + pub async fn rack_insert( + &self, + opctx: &OpContext, + rack_id: Uuid, + ) -> Result<(), Error> { + self.datastore() + .rack_insert(opctx, &db::model::Rack::new(rack_id)) + .await?; + Ok(()) } /// Marks the rack as initialized with a set of services. diff --git a/nexus/src/app/test_interfaces.rs b/nexus/src/app/test_interfaces.rs index a15f46096a8..40faaae5e1c 100644 --- a/nexus/src/app/test_interfaces.rs +++ b/nexus/src/app/test_interfaces.rs @@ -14,6 +14,9 @@ use uuid::Uuid; /// Exposes additional [`Nexus`] interfaces for use by the test suite #[async_trait] pub trait TestInterfaces { + /// Access the Rack ID of the currently executing Nexus. + fn rack_id(&self) -> Uuid; + /// Returns the SledAgentClient for an Instance from its id. We may also /// want to split this up into instance_lookup_by_id() and instance_sled(), /// but after all it's a test suite special to begin with. @@ -39,6 +42,10 @@ pub trait TestInterfaces { #[async_trait] impl TestInterfaces for super::Nexus { + fn rack_id(&self) -> Uuid { + self.rack_id + } + async fn instance_sled_by_id( &self, id: &Uuid, diff --git a/nexus/src/app/update.rs b/nexus/src/app/update.rs index 0d6721ec439..2d87a44a84f 100644 --- a/nexus/src/app/update.rs +++ b/nexus/src/app/update.rs @@ -24,11 +24,15 @@ use tokio::io::AsyncWriteExt; static BASE_ARTIFACT_DIR: &str = "/var/tmp/oxide_artifacts"; impl super::Nexus { - fn tuf_base_url(&self) -> Option { - self.updates_config.as_ref().map(|c| { - let rack = self.as_rack(); + async fn tuf_base_url( + &self, + opctx: &OpContext, + ) -> Result, Error> { + let rack = self.rack_lookup(opctx, &self.rack_id).await?; + + Ok(self.updates_config.as_ref().map(|c| { rack.tuf_base_url.unwrap_or_else(|| c.default_base_url.clone()) - }) + })) } pub async fn updates_refresh_metadata( @@ -42,10 +46,11 @@ impl super::Nexus { message: "updates system not configured".into(), } })?; - let base_url = - self.tuf_base_url().ok_or_else(|| Error::InvalidRequest { + let base_url = self.tuf_base_url(opctx).await?.ok_or_else(|| { + Error::InvalidRequest { message: "updates system not configured".into(), - })?; + } + })?; let trusted_root = tokio::fs::read(&updates_config.trusted_root) .await .map_err(|e| Error::InternalError { @@ -129,8 +134,10 @@ impl super::Nexus { artifact: UpdateArtifact, ) -> Result, Error> { let mut base_url = - self.tuf_base_url().ok_or_else(|| Error::InvalidRequest { - message: "updates system not configured".into(), + self.tuf_base_url(opctx).await?.ok_or_else(|| { + Error::InvalidRequest { + message: "updates system not configured".into(), + } })?; if !base_url.ends_with('/') { base_url.push('/'); diff --git a/nexus/src/db/datastore.rs b/nexus/src/db/datastore.rs index aa52fbc125f..996eda065f4 100644 --- a/nexus/src/db/datastore.rs +++ b/nexus/src/db/datastore.rs @@ -153,6 +153,20 @@ impl DataStore { Ok(self.pool.pool()) } + pub async fn rack_list( + &self, + opctx: &OpContext, + pagparams: &DataPageParams<'_, Uuid>, + ) -> ListResultVec { + opctx.authorize(authz::Action::Read, &authz::FLEET).await?; + use db::schema::rack::dsl; + paginated(dsl::rack, dsl::id, pagparams) + .select(Rack::as_select()) + .load_async(self.pool_authorized(opctx).await?) + .await + .map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server)) + } + /// Stores a new rack in the database. /// /// This function is a no-op if the rack already exists. diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 7c000bd97a0..a23d3d89dc1 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -3252,13 +3252,15 @@ async fn hardware_racks_get( let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let rack_stream = nexus + let racks = nexus .racks_list(&opctx, &data_page_params_for(&rqctx, &query)?) - .await?; - let view_list = to_list::(rack_stream).await; + .await? + .into_iter() + .map(|r| r.into()) + .collect(); Ok(HttpResponseOk(ScanById::results_page( &query, - view_list, + racks, &|_, rack: &Rack| rack.identity.id, )?)) }; diff --git a/nexus/src/populate.rs b/nexus/src/populate.rs index 9f6bcdcad20..85223aef2b1 100644 --- a/nexus/src/populate.rs +++ b/nexus/src/populate.rs @@ -43,13 +43,14 @@ //! each populator behaves as expected in the above ways. use crate::context::OpContext; -use crate::db::DataStore; +use crate::db::{self, DataStore}; use futures::future::BoxFuture; use futures::FutureExt; use lazy_static::lazy_static; use omicron_common::api::external::Error; use omicron_common::backoff; use std::sync::Arc; +use uuid::Uuid; #[derive(Clone, Debug)] pub enum PopulateStatus { @@ -58,14 +59,26 @@ pub enum PopulateStatus { Failed(String), } +/// Auxiliary data necessary to populate the database. +pub struct PopulateArgs { + rack_id: Uuid, +} + +impl PopulateArgs { + pub fn new(rack_id: Uuid) -> Self { + Self { rack_id } + } +} + pub fn populate_start( opctx: OpContext, datastore: Arc, + args: PopulateArgs, ) -> tokio::sync::watch::Receiver { let (tx, rx) = tokio::sync::watch::channel(PopulateStatus::NotDone); tokio::spawn(async move { - let result = populate(&opctx, &datastore).await; + let result = populate(&opctx, &datastore, &args).await; if let Err(error) = tx.send(match result { Ok(()) => PopulateStatus::Done, Err(message) => PopulateStatus::Failed(message), @@ -80,17 +93,19 @@ pub fn populate_start( async fn populate( opctx: &OpContext, datastore: &DataStore, + args: &PopulateArgs, ) -> Result<(), String> { for p in *ALL_POPULATORS { let db_result = backoff::retry_notify( backoff::internal_service_policy(), || async { - p.populate(opctx, datastore).await.map_err(|error| match &error - { - Error::ServiceUnavailable { .. } => { - backoff::BackoffError::transient(error) + p.populate(opctx, datastore, args).await.map_err(|error| { + match &error { + Error::ServiceUnavailable { .. } => { + backoff::BackoffError::transient(error) + } + _ => backoff::BackoffError::Permanent(error), } - _ => backoff::BackoffError::Permanent(error), }) }, |error, delay| { @@ -130,6 +145,7 @@ trait Populator: std::fmt::Debug + Send + Sync { &self, opctx: &'a OpContext, datastore: &'a DataStore, + args: &'a PopulateArgs, ) -> BoxFuture<'b, Result<(), Error>> where 'a: 'b; @@ -143,6 +159,7 @@ impl Populator for PopulateBuiltinUsers { &self, opctx: &'a OpContext, datastore: &'a DataStore, + _args: &'a PopulateArgs, ) -> BoxFuture<'b, Result<(), Error>> where 'a: 'b, @@ -159,6 +176,7 @@ impl Populator for PopulateBuiltinRoles { &self, opctx: &'a OpContext, datastore: &'a DataStore, + _args: &'a PopulateArgs, ) -> BoxFuture<'b, Result<(), Error>> where 'a: 'b, @@ -175,6 +193,7 @@ impl Populator for PopulateBuiltinRoleAssignments { &self, opctx: &'a OpContext, datastore: &'a DataStore, + _args: &'a PopulateArgs, ) -> BoxFuture<'b, Result<(), Error>> where 'a: 'b, @@ -192,6 +211,7 @@ impl Populator for PopulateBuiltinSilos { &self, opctx: &'a OpContext, datastore: &'a DataStore, + _args: &'a PopulateArgs, ) -> BoxFuture<'b, Result<(), Error>> where 'a: 'b, @@ -214,6 +234,7 @@ impl Populator for PopulateSiloUsers { &self, opctx: &'a OpContext, datastore: &'a DataStore, + _args: &'a PopulateArgs, ) -> BoxFuture<'b, Result<(), Error>> where 'a: 'b, @@ -230,6 +251,7 @@ impl Populator for PopulateSiloUserRoleAssignments { &self, opctx: &'a OpContext, datastore: &'a DataStore, + _args: &'a PopulateArgs, ) -> BoxFuture<'b, Result<(), Error>> where 'a: 'b, @@ -241,19 +263,43 @@ impl Populator for PopulateSiloUserRoleAssignments { } } +#[derive(Debug)] +struct PopulateRack; +impl Populator for PopulateRack { + fn populate<'a, 'b>( + &self, + opctx: &'a OpContext, + datastore: &'a DataStore, + args: &'a PopulateArgs, + ) -> BoxFuture<'b, Result<(), Error>> + where + 'a: 'b, + { + async { + datastore + .rack_insert(opctx, &db::model::Rack::new(args.rack_id)) + .await?; + Ok(()) + } + .boxed() + } +} + lazy_static! { - static ref ALL_POPULATORS: [&'static dyn Populator; 6] = [ + static ref ALL_POPULATORS: [&'static dyn Populator; 7] = [ &PopulateBuiltinUsers, &PopulateBuiltinRoles, &PopulateBuiltinRoleAssignments, &PopulateBuiltinSilos, &PopulateSiloUsers, &PopulateSiloUserRoleAssignments, + &PopulateRack, ]; } #[cfg(test)] mod test { + use super::PopulateArgs; use super::Populator; use super::ALL_POPULATORS; use crate::authn; @@ -265,6 +311,7 @@ mod test { use omicron_common::api::external::Error; use omicron_test_utils::dev; use std::sync::Arc; + use uuid::Uuid; #[tokio::test] async fn test_populators() { @@ -287,16 +334,18 @@ mod test { ); let log = &logctx.log; + let args = PopulateArgs::new(Uuid::new_v4()); + // Running each populator once under normal conditions should work. info!(&log, "populator {:?}, run 1", p); - p.populate(&opctx, &datastore) + p.populate(&opctx, &datastore, &args) .await .with_context(|| format!("populator {:?} (try 1)", p)) .unwrap(); // It should also work fine to run it again. info!(&log, "populator {:?}, run 2 (idempotency check)", p); - p.populate(&opctx, &datastore) + p.populate(&opctx, &datastore, &args) .await .with_context(|| { format!( @@ -331,7 +380,7 @@ mod test { ); info!(&log, "populator {:?}, with database offline", p); - match p.populate(&opctx, &datastore).await { + match p.populate(&opctx, &datastore, &args).await { Err(Error::ServiceUnavailable { .. }) => (), Ok(_) => panic!( "populator {:?}: unexpectedly succeeded with no database", diff --git a/nexus/tests/integration_tests/mod.rs b/nexus/tests/integration_tests/mod.rs index 639c754d84a..1bc551cde72 100644 --- a/nexus/tests/integration_tests/mod.rs +++ b/nexus/tests/integration_tests/mod.rs @@ -15,6 +15,7 @@ mod ip_pools; mod organizations; mod oximeter; mod projects; +mod rack; mod role_assignments; mod roles_builtin; mod router_routes; diff --git a/nexus/tests/integration_tests/rack.rs b/nexus/tests/integration_tests/rack.rs new file mode 100644 index 00000000000..5a6e28ab70a --- /dev/null +++ b/nexus/tests/integration_tests/rack.rs @@ -0,0 +1,42 @@ +// 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/2.0/. + +use nexus_test_utils::http_testing::AuthnMode; +use nexus_test_utils::http_testing::NexusRequest; +use nexus_test_utils::ControlPlaneTestContext; +use nexus_test_utils_macros::nexus_test; +use omicron_nexus::external_api::views::Rack; +use omicron_nexus::TestInterfaces; + +#[nexus_test] +async fn test_list_own_rack(cptestctx: &ControlPlaneTestContext) { + let client = &cptestctx.external_client; + + let racks_url = "/hardware/racks"; + let racks: Vec = + NexusRequest::iter_collection_authn(client, racks_url, "", None) + .await + .expect("failed to list racks") + .all_items; + + assert_eq!(1, racks.len()); + assert_eq!(cptestctx.server.apictx.nexus.rack_id(), racks[0].identity.id); +} + +#[nexus_test] +async fn test_get_own_rack(cptestctx: &ControlPlaneTestContext) { + let client = &cptestctx.external_client; + + let expected_id = cptestctx.server.apictx.nexus.rack_id(); + let rack_url = format!("/hardware/racks/{}", expected_id); + let rack = NexusRequest::object_get(client, &rack_url) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .expect("failed to get rack") + .parsed_body::() + .unwrap(); + + assert_eq!(expected_id, rack.identity.id); +}