Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
319da58
okay i think this might actually work
hawkw Jun 12, 2026
b2fff05
diesel hell
hawkw Jun 12, 2026
5602f03
okay get the latest query in there too
hawkw Jun 12, 2026
972cfc2
move stuff into other stuff
hawkw Jun 12, 2026
675e2ed
okay i can't quite get the latest query inlined into the CTE, that's
hawkw Jun 12, 2026
c98befb
whoa it actually works
hawkw Jun 12, 2026
7aa683a
have a restart list query
hawkw Jun 12, 2026
7cd919c
let's have some tests for that as well
hawkw Jun 13, 2026
3cbade2
clean up `ereports_insert_query`
hawkw Jun 13, 2026
7e29d33
migration, index, constraints
hawkw Jun 15, 2026
92ebeb5
actually nothing is using the slot index
hawkw Jun 15, 2026
68a2f39
wip add to loader
hawkw Jun 15, 2026
82c5cb4
finish loader
hawkw Jun 15, 2026
3732a29
gotta also add it here
hawkw Jun 15, 2026
9f1437b
data migration test
hawkw Jun 15, 2026
91df26b
Merge branch 'main' into eliza/ereport-restart-order-v2-final
hawkw Jun 16, 2026
dd76be4
update omdb command to use restart table as authortiative source
hawkw Jun 16, 2026
97a5487
Merge branch 'main' into eliza/ereport-restart-order-v2-final
hawkw Jun 16, 2026
30d72f2
issue warnings for ereports with unknown restart IDs
hawkw Jun 16, 2026
6b157b2
post merge test fixup
hawkw Jun 16, 2026
c063bd2
nexus-fm now depends on a db type sigh
hawkw Jun 16, 2026
6269baa
restart query will also do a full scan
hawkw Jun 16, 2026
efe895f
you have to actually get the rpaths thing correct lol
hawkw Jun 16, 2026
8b01405
fix typo in migration test
hawkw Jun 16, 2026
0b96394
some commentary improvements
hawkw Jun 16, 2026
4de2c50
remove some dead code
hawkw Jun 16, 2026
875fc5b
BLERGH THERES EVEN MORE COPY PASTE MISTAKES IN HERE
hawkw Jun 16, 2026
59cc9fb
UGH I TYPED THE WRONG ONE KMS KMS KMS KMS
hawkw Jun 16, 2026
f9f2e34
fixup omdb db ereport reporters arguments, some expectorates
hawkw Jun 16, 2026
22417d8
Merge branch 'main' into eliza/ereport-restart-order-v2-final
hawkw Jun 18, 2026
8376b52
post merge expectoration
hawkw Jun 18, 2026
3c289b5
comment explaining ereports insert return value
hawkw Jun 18, 2026
30370df
Merge branch 'main' into eliza/ereport-restart-order-v2-final
hawkw Jun 18, 2026
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
3 changes: 3 additions & 0 deletions Cargo.lock

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

16 changes: 15 additions & 1 deletion common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,22 @@ impl slog::KV for FileKv {
/// This exists because the database doesn't store nanosecond-precision, so if
/// we store nanosecond-precision timestamps, then DateTime conversion is lossy
/// when round-tripping through the database. That's rather inconvenient.
///
/// To truncate an arbitrary timestamp to database precision, use
/// [`timestamp_db_precision`].
pub fn now_db_precision() -> chrono::DateTime<chrono::Utc> {
let ts = chrono::Utc::now();
timestamp_db_precision(chrono::Utc::now())
}

/// Truncates a [`chrono::DateTime`]`<`[`chrono::Utc`]`>` to the previous
/// microsecond.
///
/// This exists because the database doesn't store nanosecond-precision, so if
/// we store nanosecond-precision timestamps, then `DateTime`conversion is lossy
/// when round-tripping through the database. That's rather inconvenient.
pub fn timestamp_db_precision(
ts: chrono::DateTime<chrono::Utc>,
) -> chrono::DateTime<chrono::Utc> {
let nanosecs = ts.timestamp_subsec_nanos();
let micros = ts.timestamp_subsec_micros();
let only_nanos = nanosecs - micros * 1000;
Expand Down
143 changes: 85 additions & 58 deletions dev-tools/omdb/src/bin/omdb/db/ereport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ use super::check_limit;
use crate::Omdb;
use crate::helpers::CONNECTION_OPTIONS_HEADING;
use crate::helpers::const_max_len;
use crate::helpers::datetime_opt_rfc3339_concise;
use crate::helpers::datetime_rfc3339_concise;
use crate::helpers::display_option_blank;
use crate::helpers::display_option_invalid;
Expand All @@ -23,20 +22,24 @@ use chrono::Utc;
use clap::Args;
use clap::Subcommand;
use diesel::AggregateExpressionMethods;
use diesel::dsl::{count, min};
use diesel::dsl::{count, max};
use diesel::prelude::*;
use internal_dns_types::names::ServiceName;
use nexus_db_lookup::DbConnection;
use nexus_db_model::EreporterRestart;
use nexus_db_model::ereport as model;
use nexus_db_model::ereport::DbEna;
use nexus_db_queries::db;
use nexus_db_queries::db::DataStore;
use nexus_db_queries::db::queries::ALLOW_FULL_TABLE_SCAN_SQL;
use nexus_db_schema::schema::ereport::dsl;
use nexus_db_schema::schema::ereporter_restart::dsl as restart_dsl;
use nexus_types::fm::ereport::Ena;
use nexus_types::fm::ereport::Reporter;
use omicron_uuid_kinds::EreporterRestartUuid;
use omicron_uuid_kinds::GenericUuid;
use omicron_uuid_kinds::SledUuid;
use std::collections::HashMap;
use tabled::Tabled;
use uuid::Uuid;

Expand Down Expand Up @@ -145,6 +148,7 @@ struct ReportersArgs {
#[clap(long = "slot", short = 's', requires = "slot_type")]
slot: Option<u16>,

#[clap(long = "serial")]
serial: Option<String>,
Comment on lines +151 to 152

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

this was always intended to be --serial rather than a positional argument, but i had forgotten to make it one ages ago. whoops. 😅

}

Expand Down Expand Up @@ -508,69 +512,74 @@ async fn cmd_db_ereporters(
datastore: &DataStore,
args: &ReportersArgs,
) -> anyhow::Result<()> {
let &ReportersArgs { slot, slot_type, ref serial } = args;
let &ReportersArgs { slot_type, slot, serial: ref want_serial } = args;
let slot_type = slot_type.map(nexus_db_model::SpType::from);

let conn = datastore.pool_connection_for_tests().await?;
let reporters = (*conn).transaction_async({
let serial = serial.clone();
async move |conn| {
// Selecting all reporters may require a full table scan, depending
// on filters.
let (restarts, ereport_info) = (*conn)
.transaction_async(async move |conn| {
conn.batch_execute_async(ALLOW_FULL_TABLE_SCAN_SQL).await?;
let mut query = dsl::ereport
.group_by((
dsl::restart_id,
dsl::reporter,
dsl::sled_id,
dsl::slot_type,
dsl::slot,
dsl::serial_number,
dsl::part_number
))
.select((
dsl::restart_id,
model::Reporter::as_select(),
dsl::serial_number,
dsl::part_number,
min(dsl::time_collected),
count(dsl::ena).aggregate_distinct(),
))
.into_boxed();

if let Some(slot) = slot {
if slot_type.is_some() {
query = query
.filter(dsl::slot.eq(db::model::SqlU16::new(slot)));
} else {
anyhow::bail!(
"cannot filter reporters by slot without a value for `--type`"
)
}
}

// The canonical list of reporter restarts comes from the
// `ereporter_restart` table.
let mut query = restart_dsl::ereporter_restart
.select(EreporterRestart::as_select())
.into_boxed();
if let Some(slot_type) = slot_type {
query = query.filter(restart_dsl::slot_type.eq(slot_type));
}
if let Some(slot) = slot {
query = query
.filter(dsl::slot_type.eq(slot_type));
.filter(restart_dsl::slot.eq(db::model::SqlU16::new(slot)));
}
let restarts = query
.load_async::<EreporterRestart>(&conn)
.await
.context("listing ereporter restarts")?;

// The `ereporter_restart` table doesn't record the reporter's sled
// ID, VPD identity, or ereport count, so gather those from the
// `ereport` table, grouped by restart ID. The sled ID is part of
// the `GROUP BY` (rather than aggregated) since it is constant for
// a given restart ID; the VPD identity may start out NULL and be
// filled in later, so we take its `MAX` (ignoring NULLs) as we do
// for the slot number. This may require a full table scan.
let ereport_info = dsl::ereport
.group_by((dsl::restart_id, dsl::sled_id))
.select((
dsl::restart_id,
dsl::sled_id,
max(dsl::serial_number),
max(dsl::part_number),
count(dsl::ena).aggregate_distinct(),
))
.load_async::<(
Uuid,
Option<Uuid>,
Option<String>,
Option<String>,
i64,
)>(&conn)
.await
.context("querying ereport metadata by restart ID")?;

if let Some(serial) = serial {
query = query.filter(dsl::serial_number.eq(serial.clone()));
}
Ok::<_, anyhow::Error>((restarts, ereport_info))
})
.await?;

query
.load_async::<(Uuid, model::Reporter, Option<String>, Option<String>, Option<DateTime<Utc>>, i64)>(
&conn,
)
.await.context("listing reporter entries")
}
}).await?;
// Index the per-restart `ereport` metadata by restart ID.
let mut ereport_info = ereport_info
.into_iter()
.map(|(id, sled_id, serial, part_number, ereports)| {
(id, (sled_id, serial, part_number, ereports))
})
.collect::<HashMap<_, _>>();

#[derive(Tabled)]
#[tabled(rename_all = "SCREAMING_SNAKE_CASE")]
struct ReporterRow {
#[tabled(display_with = "datetime_opt_rfc3339_concise")]
first_seen: Option<DateTime<Utc>>,
#[tabled(display_with = "datetime_rfc3339_concise")]
first_seen: DateTime<Utc>,
id: Uuid,
#[tabled(display_with = "display_option_invalid")]
identity: Option<Reporter>,
Expand All @@ -581,27 +590,45 @@ async fn cmd_db_ereporters(
ereports: i64,
}

let mut rows = reporters
let mut rows = restarts
.into_iter()
.map(|(id, reporter, serial, part_number, first_seen, ereports)| {
let identity = match reporter.try_into() {
.filter_map(|restart| {
let id = restart.id.into_untyped_uuid();
let (sled_id, serial, part_number, ereports) =
ereport_info.remove(&id).unwrap_or((None, None, None, 0));
// If we are filtering by serial number, skip any that don't match.
if want_serial.is_some() && serial.as_ref() != want_serial.as_ref()
{
return None;
}
// The reporter identity combines the restart's location (from
// `ereporter_restart`) with the sled ID (from `ereport`).
let identity = match (model::Reporter {
reporter: restart.reporter,
sled_id: sled_id
.map(|sled| SledUuid::from_untyped_uuid(sled).into()),
slot_type: restart.slot_type,
slot: restart.slot,
})
.try_into()
{
Ok(reporter) => Some(reporter),
Err(err) => {
eprintln!(
"error: encounted an invalid reporter entry for \
"error: encountered an invalid reporter entry for \
reporter ID {id}: {err}",
);
None
}
};
ReporterRow {
first_seen,
Some(ReporterRow {
first_seen: restart.time_first_seen,
id,
identity,
serial,
part_number,
ereports,
}
})
})
.collect::<Vec<_>>();
rows.sort_by_key(|row| row.first_seen);
Expand Down
2 changes: 2 additions & 0 deletions dev-tools/omdb/tests/successes.out
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,7 @@ task: "fm_analysis"
parent sitrep: ..........<REDACTED_UUID>...........
inventory collection: ..........<REDACTED_UUID>...........
<FM_INPUT_INV_COMPARISON_REDACTED>
total known ereport restart IDs: 0
no new ereports since the parent sitrep
no cases copied forward

Expand Down Expand Up @@ -1409,6 +1410,7 @@ task: "fm_analysis"
parent sitrep: ..........<REDACTED_UUID>...........
inventory collection: ..........<REDACTED_UUID>...........
<FM_INPUT_INV_COMPARISON_REDACTED>
total known ereport restart IDs: 0
no new ereports since the parent sitrep
no cases copied forward

Expand Down
6 changes: 2 additions & 4 deletions dev-tools/omdb/tests/usage_errors.out
Original file line number Diff line number Diff line change
Expand Up @@ -650,15 +650,13 @@ termination: Exited(0)
stdout:
List ereport reporters

Usage: omdb db ereport reporters [OPTIONS] [SERIAL]

Arguments:
[SERIAL]
Usage: omdb db ereport reporters [OPTIONS]

Options:
--log-level <LOG_LEVEL> log level filter [env: LOG_LEVEL=] [default: warn]
-t, --type <SLOT_TYPE>
-s, --slot <SLOT>
--serial <SERIAL>
--color <COLOR> Color output [default: auto] [possible values: auto, always, never]
-h, --help Print help

Expand Down
43 changes: 43 additions & 0 deletions nexus/db-model/src/ereporter_restart.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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 crate::DbTypedUuid;
use crate::EreporterType;
use crate::SpMgsSlot;
use crate::SpType;
use chrono::DateTime;
use chrono::Utc;
use nexus_db_schema::schema::ereporter_restart;
use omicron_uuid_kinds::EreporterRestartKind;
use omicron_uuid_kinds::EreporterRestartUuid;

#[derive(Clone, Debug, Insertable, Queryable, Selectable)]
#[diesel(table_name = ereporter_restart)]
pub struct EreporterRestart {
pub id: DbTypedUuid<EreporterRestartKind>,
pub time_first_seen: DateTime<Utc>,
pub reporter: EreporterType,
pub slot_type: SpType,
pub slot: Option<SpMgsSlot>,
}

impl EreporterRestart {
pub fn slot_number(&self) -> Option<u16> {
self.slot.map(|slot| (*slot).0)
}

pub fn id(&self) -> &EreporterRestartUuid {
&self.id.0
}
}

impl iddqd::IdOrdItem for EreporterRestart {
type Key<'a> = &'a EreporterRestartUuid;

fn key(&self) -> Self::Key<'_> {
self.id()
}

iddqd::id_upcast!();
}
1 change: 1 addition & 0 deletions nexus/db-model/src/ereporter_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ impl_enum_type!(
Clone,
Debug,
PartialEq,
Eq,
Serialize,
Deserialize,
AsExpression,
Expand Down
2 changes: 2 additions & 0 deletions nexus/db-model/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ mod disk_type_local_storage;
mod dns;
mod downstairs;
pub mod ereport;
mod ereporter_restart;
mod ereporter_type;
mod external_ip;
mod external_subnet;
Expand Down Expand Up @@ -301,6 +302,7 @@ pub use disk_type_local_storage::*;
pub use dns::*;
pub use downstairs::*;
pub use ereport::Ereport;
pub use ereporter_restart::*;
pub use ereporter_type::*;
pub use external_ip::*;
pub use external_subnet::*;
Expand Down
3 changes: 2 additions & 1 deletion nexus/db-model/src/schema_versions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use std::{collections::BTreeMap, sync::LazyLock};
///
/// This must be updated when you change the database schema. Refer to
/// schema/crdb/README.adoc in the root of this repository for details.
pub const SCHEMA_VERSION: Version = Version::new(271, 0, 0);
pub const SCHEMA_VERSION: Version = Version::new(272, 0, 0);

/// List of all past database schema versions, in *reverse* order
///
Expand All @@ -28,6 +28,7 @@ pub static KNOWN_VERSIONS: LazyLock<Vec<KnownVersion>> = LazyLock::new(|| {
// | leaving the first copy as an example for the next person.
// v
// KnownVersion::new(next_int, "unique-dirname-with-the-sql-files"),
KnownVersion::new(272, "ereporter-restart-order-v2"),
KnownVersion::new(271, "inv-fmd"),
KnownVersion::new(270, "fm-alert-resource-deletion"),
KnownVersion::new(269, "fm-disk-de-and-facts"),
Expand Down
Loading
Loading