Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
124 changes: 72 additions & 52 deletions nexus/db-queries/src/db/datastore/db_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ use anyhow::{Context, bail, ensure};
use async_bb8_diesel::{AsyncRunQueryDsl, AsyncSimpleConnection};
use chrono::Utc;
use diesel::prelude::*;
use diesel::upsert::excluded;
use nexus_db_errors::ErrorHandler;
use nexus_db_errors::OptionalError;
use nexus_db_errors::public_error_from_diesel;
Expand Down Expand Up @@ -152,9 +151,9 @@ fn skippable_version(

/// Describes the state of the database access with respect this Nexus
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum NexusAccess {
enum NexusAccess {
/// Nexus does not yet have access to the database.
DoesNotHaveAccessYet,
DoesNotHaveAccessYet { nexus_id: OmicronZoneUuid },

/// Nexus has been explicitly locked out of the database.
LockedOut,
Expand Down Expand Up @@ -194,16 +193,19 @@ enum SchemaStatus {
OlderThanDesiredSkipAccessCheck,
}

/// Describes what should be done with a schema
/// Describes what setup is necessary for DataStore creation
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum SchemaAction {
pub enum DatastoreSetupAction {
/// Normal operation: The database is ready for usage
Ready,

/// Not ready for usage yet
///
/// The database may be ready for usage once handoff has completed.
NeedsHandoff,
/// The `nexus_id` here may attempt to takeover the database if it has
/// a `db_metadata_nexus` record of "not_yet", and all other records
/// are either "not_yet" or "quiesced".
NeedsHandoff { nexus_id: OmicronZoneUuid },

/// Start a schema update
Update,
Expand All @@ -212,18 +214,18 @@ pub enum SchemaAction {
Refuse,
}

/// Committment that the database is willing to perform a [SchemaAction]
/// Committment that the database is willing to perform a [DatastoreSetupAction]
/// to a desired schema [Version].
///
/// Can be created through [DataStore::check_schema_and_access]
#[derive(Clone)]
pub struct ValidatedSchemaAction {
action: SchemaAction,
pub struct ValidatedDatastoreSetupAction {
action: DatastoreSetupAction,
desired: Version,
}

impl ValidatedSchemaAction {
pub fn action(&self) -> &SchemaAction {
impl ValidatedDatastoreSetupAction {
pub fn action(&self) -> &DatastoreSetupAction {
&self.action
}

Expand All @@ -232,7 +234,7 @@ impl ValidatedSchemaAction {
}
}

impl SchemaAction {
impl DatastoreSetupAction {
// Interprets the combination of access and status to decide what action
// should be taken.
fn new(access: NexusAccess, status: SchemaStatus) -> Self {
Expand All @@ -249,9 +251,9 @@ impl SchemaAction {
// If we don't have access yet, but could do something once handoff
// occurs, then handoff is needed
(
DoesNotHaveAccessYet,
DoesNotHaveAccessYet { nexus_id },
UpToDate | OlderThanDesired | OlderThanDesiredSkipAccessCheck,
) => Self::NeedsHandoff,
) => Self::NeedsHandoff { nexus_id },

// This is the most "normal" case: Nexus should have access to the
// database, and the schema matches what it wants.
Expand Down Expand Up @@ -304,7 +306,7 @@ impl DataStore {
let msg = "Nexus does not have access to the database (no \
db_metadata_nexus record)";
warn!(&self.log, "{msg}"; "nexus_id" => ?nexus_id);
return Ok(NexusAccess::DoesNotHaveAccessYet);
return Ok(NexusAccess::DoesNotHaveAccessYet { nexus_id });
};

let status = match state {
Expand All @@ -322,7 +324,7 @@ impl DataStore {
"Nexus does not yet have access to the database";
"nexus_id" => ?nexus_id
);
NexusAccess::DoesNotHaveAccessYet
NexusAccess::DoesNotHaveAccessYet { nexus_id }
}
DbMetadataNexusState::Quiesced => {
let msg = "Nexus locked out of database access (quiesced)";
Expand Down Expand Up @@ -380,7 +382,7 @@ impl DataStore {
&self,
identity_check: IdentityCheckPolicy,
desired_version: Version,
) -> Result<ValidatedSchemaAction, anyhow::Error> {
) -> Result<ValidatedDatastoreSetupAction, anyhow::Error> {
let schema_status = self.check_schema(desired_version.clone()).await?;

let nexus_access = match identity_check {
Expand All @@ -399,38 +401,40 @@ impl DataStore {
}
}
IdentityCheckPolicy::DontCare => {
// If a "nexus_id" was not supplied, skip the check, and treat it
// as having access.
// If a "nexus_id" was not supplied, skip the check, and treat
// it as having access.
//
// This is necessary for tools which access the schema without a
// running Nexus, such as the schema-updater binary.
NexusAccess::HasImplicitAccess
}
};

Ok(ValidatedSchemaAction {
action: SchemaAction::new(nexus_access, schema_status),
Ok(ValidatedDatastoreSetupAction {
action: DatastoreSetupAction::new(nexus_access, schema_status),
desired: desired_version,
})
}

/// Ensures that the database schema matches `desired_version`.
///
/// - `validated_action`: A [ValidatedSchemaAction], indicating that
/// - `validated_action`: A [ValidatedDatastoreSetupAction], indicating that
/// [Self::check_schema_and_access] has already been called.
/// - `all_versions`: A description of all schema versions between
/// "whatever is in the DB" and `desired_version`, instructing
/// how to perform an update.
pub async fn update_schema(
&self,
validated_action: ValidatedSchemaAction,
validated_action: ValidatedDatastoreSetupAction,
all_versions: Option<&AllSchemaVersions>,
) -> Result<(), anyhow::Error> {
let action = validated_action.action();

match action {
SchemaAction::Ready => bail!("No schema update is necessary"),
SchemaAction::Update => (),
DatastoreSetupAction::Ready => {
bail!("No schema update is necessary")
}
DatastoreSetupAction::Update => (),
_ => bail!("Not ready for schema update"),
}

Expand Down Expand Up @@ -655,8 +659,8 @@ impl DataStore {
Ok(())
}

/// Returns the access this Nexus has to the database
pub async fn database_nexus_access(
// Returns the access this Nexus has to the database
async fn database_nexus_access(
&self,
nexus_id: OmicronZoneUuid,
) -> Result<Option<DbMetadataNexus>, Error> {
Expand All @@ -674,14 +678,15 @@ impl DataStore {
Ok(nexus_access)
}

/// Checks if any db_metadata_nexus records exist in the database
pub async fn database_nexus_access_any_exist(&self) -> Result<bool, Error> {
// Checks if any db_metadata_nexus records exist in the database
async fn database_nexus_access_any_exist(&self) -> Result<bool, Error> {
let conn = self.pool_connection_unauthorized().await?;
Self::database_nexus_access_any_exist_on_connection(&conn).await
}

/// Checks if any db_metadata_nexus records exist in the database using an existing connection
pub async fn database_nexus_access_any_exist_on_connection(
// Checks if any db_metadata_nexus records exist in the database using an
// existing connection
async fn database_nexus_access_any_exist_on_connection(
conn: &async_bb8_diesel::Connection<DbConnection>,
) -> Result<bool, Error> {
use nexus_db_schema::schema::db_metadata_nexus::dsl;
Expand All @@ -696,8 +701,9 @@ impl DataStore {
Ok(exists)
}

/// Registers a Nexus instance as having active access to the database
pub async fn database_nexus_access_insert(
// Registers a Nexus instance as having active access to the database
#[cfg(test)]
async fn database_nexus_access_insert(
&self,
nexus_id: OmicronZoneUuid,
state: DbMetadataNexusState,
Expand All @@ -710,7 +716,7 @@ impl DataStore {
.values(new_nexus)
.on_conflict(dsl::nexus_id)
.do_update()
.set(dsl::state.eq(excluded(dsl::state)))
.set(dsl::state.eq(diesel::upsert::excluded(dsl::state)))
.execute_async(&*self.pool_connection_unauthorized().await?)
.await
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?;
Expand Down Expand Up @@ -1049,7 +1055,7 @@ mod test {
.expect("Failed to check schema and access");

assert!(
matches!(checked_action.action(), SchemaAction::Ready),
matches!(checked_action.action(), DatastoreSetupAction::Ready),
"Unexpected action: {:?}",
checked_action.action(),
);
Expand Down Expand Up @@ -1508,7 +1514,8 @@ mod test {
let datastore =
DataStore::new_unchecked(logctx.log.clone(), db.pool().clone());

// Set up test data: create multiple nexus records in not_yet and quiesced states
// Set up test data: create multiple nexus records in not_yet and
// quiesced states
let nexus1_id = OmicronZoneUuid::new_v4();
let nexus2_id = OmicronZoneUuid::new_v4();
let nexus3_id = OmicronZoneUuid::new_v4();
Expand Down Expand Up @@ -1563,7 +1570,8 @@ mod test {
}
assert!(result.is_ok());

// Verify final state: all not_yet records should now be active, quiesced should remain quiesced
// Verify final state: all not_yet records should now be active,
// quiesced should remain quiesced
let nexus1_after = datastore
.database_nexus_access(nexus1_id)
.await
Expand All @@ -1582,20 +1590,22 @@ mod test {

assert_eq!(nexus1_after.state(), DbMetadataNexusState::Active);
assert_eq!(nexus2_after.state(), DbMetadataNexusState::Active);
assert_eq!(nexus3_after.state(), DbMetadataNexusState::Quiesced); // Should remain unchanged
// Should remain unchanged
assert_eq!(nexus3_after.state(), DbMetadataNexusState::Quiesced);

db.terminate().await;
logctx.cleanup_successful();
}

// This test covers two cases:
//
// 1. New systems: We use RSS to initialize Nexus, but no db_metadata_nexus entries
// exist.
// 1. New systems: We use RSS to initialize Nexus, but no db_metadata_nexus
// entries exist.
// 2. Deployed systems: We have a deployed system which updates to have this
// "db_metadata_nexus"-handling code, but has no rows in that table.
//
// Both of these cases must be granted database access to self-populate later.
// Both of these cases must be granted database access to self-populate
// later.
#[tokio::test]
async fn test_check_schema_and_access_empty_table_permits_access() {
let logctx = dev::test_setup_log(
Expand All @@ -1615,9 +1625,10 @@ mod test {
)
.await
.expect("Failed to check schema and access");
assert_eq!(action.action(), &SchemaAction::Ready);
assert_eq!(action.action(), &DatastoreSetupAction::Ready);

// Add a record to the table, now explicit nexus ID should NOT get access
// Add a record to the table, now explicit nexus ID should NOT get
// access
datastore
.database_nexus_access_insert(
OmicronZoneUuid::new_v4(), // Different nexus
Expand All @@ -1633,7 +1644,10 @@ mod test {
)
.await
.expect("Failed to check schema and access");
assert_eq!(action.action(), &SchemaAction::NeedsHandoff);
assert_eq!(
action.action(),
&DatastoreSetupAction::NeedsHandoff { nexus_id }
);

db.terminate().await;
logctx.cleanup_successful();
Expand Down Expand Up @@ -1667,7 +1681,7 @@ mod test {
)
.await
.expect("Failed to check schema and access");
assert_eq!(action.action(), &SchemaAction::Ready);
assert_eq!(action.action(), &DatastoreSetupAction::Ready);

// Explicit CheckAndTakeover with a Nexus ID that doesn't exist should
// not get access
Expand All @@ -1679,7 +1693,10 @@ mod test {
)
.await
.expect("Failed to check schema and access");
assert_eq!(action.action(), &SchemaAction::NeedsHandoff);
assert_eq!(
action.action(),
&DatastoreSetupAction::NeedsHandoff { nexus_id },
);

db.terminate().await;
logctx.cleanup_successful();
Expand Down Expand Up @@ -1715,7 +1732,7 @@ mod test {
)
.await
.expect("Failed to check schema and access");
assert_eq!(action.action(), &SchemaAction::Refuse);
assert_eq!(action.action(), &DatastoreSetupAction::Refuse);

db.terminate().await;
logctx.cleanup_successful();
Expand Down Expand Up @@ -1759,7 +1776,7 @@ mod test {
)
.await
.expect("Failed to check schema and access");
assert_eq!(action.action(), &SchemaAction::Refuse);
assert_eq!(action.action(), &DatastoreSetupAction::Refuse);

// Implicit Access: Rejected
let action = datastore
Expand All @@ -1769,7 +1786,7 @@ mod test {
)
.await
.expect("Failed to check schema and access");
assert_eq!(action.action(), &SchemaAction::Refuse);
assert_eq!(action.action(), &DatastoreSetupAction::Refuse);

db.terminate().await;
logctx.cleanup_successful();
Expand Down Expand Up @@ -1812,7 +1829,10 @@ mod test {
)
.await
.expect("Failed to check schema and access");
assert_eq!(action.action(), &SchemaAction::NeedsHandoff);
assert_eq!(
action.action(),
&DatastoreSetupAction::NeedsHandoff { nexus_id },
);
}

db.terminate().await;
Expand Down Expand Up @@ -1848,7 +1868,7 @@ mod test {
.await
.expect("Failed to check schema and access");

assert_eq!(action.action(), &SchemaAction::Ready);
assert_eq!(action.action(), &DatastoreSetupAction::Ready);
assert_eq!(action.desired_version(), &SCHEMA_VERSION);

db.terminate().await;
Expand Down Expand Up @@ -1887,7 +1907,7 @@ mod test {
.await
.expect("Failed to check schema and access");

assert_eq!(action.action(), &SchemaAction::Update);
assert_eq!(action.action(), &DatastoreSetupAction::Update);
assert_eq!(action.desired_version(), &newer_version);

db.terminate().await;
Expand Down
Loading