Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ members = [
"nexus/defaults",
"nexus/external-api",
"nexus/fm",
"nexus/fm/state-report"
"nexus/internal-api",
"nexus/inventory",
"nexus/lockstep-api",
Expand Down Expand Up @@ -298,6 +299,7 @@ default-members = [
"nexus/defaults",
"nexus/external-api",
"nexus/fm",
"nexus/fm/state-report",
"nexus/internal-api",
"nexus/inventory",
"nexus/lockstep-api",
Expand Down
2 changes: 1 addition & 1 deletion git-version/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ use std::str::FromStr;
/// See [here][1] for discussion of this limitation.
///
/// [1]: https://github.com/oxidecomputer/omicron/pull/10578#discussion_r3384362440
#[derive(Debug, serde_with::DeserializeFromStr)]
#[derive(Debug, serde_with::DeserializeFromStr, Clone)]
pub struct GitVersion {
// We use a `Cow` here so that we need not allocate when constructing a
// `GitVersion` to represent the current state of the repository, as it can
Expand Down
22 changes: 21 additions & 1 deletion nexus/db-model/src/fm/sitrep_analysis_report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
use crate::DbTypedUuid;
use anyhow::Context;
use nexus_db_schema::schema::fm_sitrep_analysis_report;
use nexus_types::fm::analysis_reports::{AnalysisReport, InputReport};
use nexus_types::fm::analysis_reports::{
AnalysisReport, InputReport, UnparsedSitrepReport,
};
use omicron_uuid_kinds::SitrepKind;

#[derive(Queryable, Insertable, Clone, Debug, Selectable)]
Expand Down Expand Up @@ -42,3 +44,21 @@ impl SitrepAnalysisReport {
Ok(Self { sitrep_id, git_commit, input_report, analysis_report })
}
}

impl From<SitrepAnalysisReport> for UnparsedSitrepReport {
fn from(report: SitrepAnalysisReport) -> Self {
let SitrepAnalysisReport {
sitrep_id: _,
git_commit,
input_report,
analysis_report,
} = report;
Self {
git_commit: git_commit
.parse()
.expect("GitVersion::from_str is infallible"),
input_report,
analysis_report,
}
}
}
118 changes: 118 additions & 0 deletions nexus/db-queries/src/db/datastore/fm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ use nexus_db_schema::schema::fm_sitrep_history::dsl as history_dsl;
use nexus_db_schema::schema::fm_support_bundle_request::dsl as support_bundle_req_dsl;
use nexus_types::fm;
use nexus_types::fm::Sitrep;
use nexus_types::fm::analysis_reports::SitrepSummary;
use nexus_types::support_bundle::{BundleData, BundleDataSelection};
use omicron_common::api::external::DataPageParams;
use omicron_common::api::external::Error;
Expand Down Expand Up @@ -1640,6 +1641,94 @@ impl DataStore {
.select(model::SitrepVersion::as_select())
}

/// Lists summaries of the sitreps in the sitrep history, paginated by
/// version number.
///
/// Unlike [`DataStore::fm_sitrep_version_list`], which returns only the
/// [`fm::SitrepVersion`] records from the history table, this method
/// returns a [`SitrepSummary`] for each version in the history, which
/// includes the sitrep's [`fm::SitrepMetadata`] record, along with
/// [analysis report](fm::analysis_reports::UnparsedSitrepReport) reports
/// describing the analysis that produced it, if one exists.
pub async fn fm_sitrep_history_summary_list(
&self,
opctx: &OpContext,
pagparams: &DataPageParams<'_, SqlU32>,
) -> ListResultVec<SitrepSummary> {
// TODO(eliza): there should probably be an authz object for the fm
// sitrep?
opctx.authorize(authz::Action::ListChildren, &authz::FLEET).await?;
let conn = self.pool_connection_authorized(opctx).await?;

let summaries = Self::sitrep_history_summary_list_query(pagparams)
.load_async(&*conn)
.await
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?
.into_iter()
.filter_map(|(version, metadata, report)| {
let version: fm::SitrepVersion = version.into();
// This *should* never be null, as discussed in the comment in
// `sitrep_history_summary_list_query`. Throwing the whole thing
// out if it is is probably fine, since it should never happen.
let Some(metadata) = metadata else {
slog::warn!(
opctx.log,
"sitrep v{} has ID {}, but no corresponding fm_sitrep \
metadata record exists with that ID! this is a bug!",
version.version,
version.id;
"sitrep_id" => ?version.id,
"sitrep_version" => version.version,
);
return None;
};
Some(SitrepSummary::new(version, metadata.into(), report))
})
.collect::<Vec<_>>();

Ok(summaries)
}

fn sitrep_history_summary_list_query(
pagparams: &DataPageParams<'_, SqlU32>,
) -> impl RunnableQuery<(
model::SitrepVersion,
Option<model::SitrepMetadata>,
Option<model::fm::SitrepAnalysisReport>,
)> + use<> {
paginated(
history_dsl::fm_sitrep_history,
history_dsl::version,
&pagparams,
)
// Here we come to a somewhat sad state of affairs: each row in
// `fm_sitrep_history` should always have a corresponding row in
// `fm_sitrep` for the history record's sitrep ID, so logically, this is
// an INNER JOIN. However! An INNER JOIN prevents CockroachDB's query
// planner from enforcing the LIMIT until after the JOIN is evaluated,
// since an INNER JOIN may discard rows. This means we perform a "full
// scan" of the `fm_sitrep_history` table, which runs afoul of the "no
// full table scans" setting. Using a LEFT JOIN here allows the query
// planner to apply the LIMIT to the scan over the history table, and
// avoids the full scan. Unfortunately, this means that the caller has
// to handle the fact that the "shouldn't happen" case where a history
// row lacks a sitrep with the same UUID.
.left_join(
sitrep_dsl::fm_sitrep.on(sitrep_dsl::id.eq(history_dsl::sitrep_id)),
)
// The analysis report may or may not exist, so this one actually
// *should* be a LEFT JOIN.
.left_join(
analysis_report_dsl::fm_sitrep_analysis_report
.on(analysis_report_dsl::sitrep_id.eq(history_dsl::sitrep_id)),
)
.select((
model::SitrepVersion::as_select(),
Option::<model::SitrepMetadata>::as_select(),
Option::<model::fm::SitrepAnalysisReport>::as_select(),
))
}

/// Check whether the given sitrep limit has been reached.
///
/// This (necessarily) does a full table scan on the sitrep table up to
Expand Down Expand Up @@ -2201,6 +2290,35 @@ mod tests {
logctx.cleanup_successful();
}

#[tokio::test]
async fn explain_sitrep_history_summary_list_query() {
let logctx =
dev::test_setup_log("explain_sitrep_history_summary_list_query");
let db = TestDatabase::new_with_pool(&logctx.log).await;
let pool = db.pool();
let conn = pool.claim().await.unwrap();

let pagparams = DataPageParams {
marker: None,
limit: std::num::NonZeroU32::new(420).unwrap(),
direction: dropshot::PaginationOrder::Descending,
};
let query = DataStore::sitrep_history_summary_list_query(&pagparams);
let explanation = query
.explain_async(&conn)
.await
.expect("Failed to explain query - is it valid SQL?");
eprintln!("{explanation}");
assert!(
!explanation.contains("FULL SCAN"),
"Found an unexpected FULL SCAN: {}",
explanation
);

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

#[tokio::test]
async fn explain_sitrep_read_ereports_query() {
let logctx = dev::test_setup_log("explain_sitrep_read_ereports_query");
Expand Down
4 changes: 4 additions & 0 deletions nexus/db-schema/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3286,6 +3286,10 @@ table! {
}

allow_tables_to_appear_in_same_query!(fm_sitrep_analysis_report, fm_sitrep);
allow_tables_to_appear_in_same_query!(
fm_sitrep_analysis_report,
fm_sitrep_history
);

table! {
disk_type_local_storage (disk_id) {
Expand Down
20 changes: 20 additions & 0 deletions nexus/fm/state-report/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "nexus-fm-state-report"
edition.workspace = true

[lints]
workspace = true

[build-dependencies]
omicron-rpaths.workspace = true

[dependencies]
anyhow.workspace = true
futures.workspace = true
nexus-db-model.workspace = true
nexus-db-queries.workspace = true
nexus-types.workspace = true
# See omicron-rpaths for more about the "pq-sys" dependency.
pq-sys = "*"

omicron-workspace-hack.workspace = true
9 changes: 9 additions & 0 deletions nexus/fm/state-report/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// 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/.

fn main() {
// See omicron-rpaths for documentation. NOTE: This file MUST be kept in
// sync with the other build.rs files in this repository.
omicron_rpaths::configure_default_omicron_rpaths();
}
36 changes: 36 additions & 0 deletions nexus/fm/state-report/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// 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/.

//! A li'l dingus for collecting a snapshot of fault management state.

use anyhow::Context;
use nexus_db_queries::context::OpContext;
use nexus_db_queries::db::DataStore;
use nexus_db_queries::db::datastore::SQL_BATCH_SIZE;
use nexus_db_queries::db::pagination::Paginator;

pub use nexus_types::fm::analysis_reports::*;

pub struct SnapshotParams {
pub requested_sitrep_id: Option<SitrepUuid>,
pub max_historical_sitreps: usize,
}

pub fn snapshot(
opctx: &OpContext,
datastore: &DataStore,
params: &SnapshotParams,
) -> anyhow::Result<FmStateReport> {
// We are about to read A Whole Bunch of Stuff. Make sure that's oaky
opctx.check_complex_operations_allowed()?;

let (current_version, current_sitrep) =
datastore.fm_sitrep_read_current(opctx).await?;
let current_config = datastore
.fm_config_get_latest(opctx)
.await?
.map_or_else(PlannerConfig::default, |c| c.config.planner_config);

todo!("eliza: draw the rest of the owl")
}
1 change: 1 addition & 0 deletions nexus/types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ gateway-client.workspace = true
gateway-types.workspace = true
internal-dns-types.workspace = true
omicron-common.workspace = true
omicron-git-version.workspace = true
omicron-passwords.workspace = true
omicron-workspace-hack.workspace = true
semver.workspace = true
Expand Down
Loading
Loading