Skip to content

Commit 935c547

Browse files
authored
[omdb] fix a variety of omdb db alert badness (#10753)
Regrettably, I have recently come to discover that the `omdb db alert` subcommands that deal with alerts (`omdb db alert list` and `omdb db alert info`) are all broken in a variety of ways. This is largely due to the fact that, until very recently, there was no actual way to cause a real Oxide system to *have* any alerts to tell you about, so these commands had sadly never really been tested.[^1] In particular, it turns out that both `omdb db alert list` *and* `omdb db alert show` will currently run queries which attempt to perform full table scans and therefore don't work at all. The list command does this pretty much any time you try and list alerts without filtering on `time_dispatched`, which is the only thing the `alert` table has indices for (see #10718). Meanwhile, `alert show` will happily go and fetch the alert record itself, and show it to you successfully, but then it will try and list deliveries of that alert using a query that...doesn't actually filter the deliveries by the alert's ID and would instead just dump the whole table, so that also obviously doesn't work. This PR fixes both of those commands, and also does a bit of refactoring that made sense to do while making the changes necessary to fix them. In particular, I moved all the queries that these commands perform into the `nexus-db-queries` crate so that we could actually have tests that *ensure* they don't do full scans, and then actually fixed the queries. The query for listing webhook deliveries for an alert ID was pretty easily fixed by just making it actually filter on the alert ID like it was supposed to, but the query that lists alert records (used by the `omdb db alert list` command) required creating some new indices for filtering the alert table on various properties. I also added support for filtering on alert _classes_ in `omdb db alert list`, refactored some of the alert lookup stuff to use `authz::Alert`, added an alert schema version column to `omdb db alert list`, and fixed some `clap` messiness. [^1]: Note that the `omdb db alert webhook receiver` subcommands, which tell you about webhook receivers, are much less busted, because I was actually able to test them meaningfully way back when I added them. Unlike alerts, you actually *can* create webhook receivers using the API...
1 parent 2231164 commit 935c547

18 files changed

Lines changed: 919 additions & 115 deletions

File tree

dev-tools/omdb/src/bin/omdb/db/alert.rs

Lines changed: 140 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use diesel::ExpressionMethods;
2222
use diesel::OptionalExtension;
2323
use diesel::expression::SelectableHelper;
2424
use diesel::query_dsl::QueryDsl;
25+
use nexus_db_lookup::LookupPath;
2526
use nexus_db_model::Alert;
2627
use nexus_db_model::AlertClass;
2728
use nexus_db_model::AlertReceiver;
@@ -30,8 +31,6 @@ use nexus_db_model::fm::RendezvousAlertCreated;
3031
use nexus_db_queries::context::OpContext;
3132
use nexus_db_queries::db;
3233
use nexus_db_queries::db::DataStore;
33-
use nexus_db_schema::schema::alert::dsl as alert_dsl;
34-
use nexus_db_schema::schema::rendezvous_alert_created::dsl as rendezvous_created_dsl;
3534
use nexus_db_schema::schema::webhook_delivery::dsl as delivery_dsl;
3635
use nexus_db_schema::schema::webhook_delivery_attempt::dsl as attempt_dsl;
3736
use nexus_types::identity::Resource;
@@ -40,6 +39,7 @@ use omicron_common::api::external::Generation;
4039
use omicron_common::api::external::NameOrId;
4140
use omicron_common::api::external::http_pagination::PaginatedBy;
4241
use omicron_uuid_kinds::AlertUuid;
42+
use omicron_uuid_kinds::CaseUuid;
4343
use omicron_uuid_kinds::GenericUuid;
4444
use tabled::Tabled;
4545
use uuid::Uuid;
@@ -95,9 +95,20 @@ struct AlertListArgs {
9595
/// cases will be included in the output.
9696
///
9797
/// Note that not all alerts are requested by fault management cases.
98-
#[clap(long = "case")]
98+
#[clap(long, num_args(1..))]
9999
cases: Vec<Uuid>,
100100

101+
/// Include only alerts with the specified alert classes.
102+
///
103+
/// If multiple values are provided, alerts with any of those classes will
104+
/// be included in the output.
105+
#[clap(
106+
long,
107+
num_args(1..),
108+
value_enum,
109+
)]
110+
classes: Vec<ClapAlertClass>,
111+
101112
/// If `true`, include only alerts that have been fully dispatched.
102113
/// If `false`, include only alerts that have not been fully dispatched.
103114
///
@@ -107,6 +118,39 @@ struct AlertListArgs {
107118
dispatched: Option<bool>,
108119
}
109120

121+
// A workaround for not being able to derive `clap::ValueEnum` on
122+
// `nexus_types::alert::AlertClass`, due to it living in the `nexus_types`
123+
// crate(which doesn't really want a `clap` dependency).
124+
#[derive(Debug, Copy, Clone)]
125+
struct ClapAlertClass(nexus_types::alert::AlertClass);
126+
127+
impl clap::ValueEnum for ClapAlertClass {
128+
fn value_variants<'a>() -> &'a [Self] {
129+
static VALUE_VARIANTS: std::sync::LazyLock<Vec<ClapAlertClass>> =
130+
std::sync::LazyLock::new(|| {
131+
nexus_types::alert::AlertClass::ALL_CLASSES
132+
.iter()
133+
.copied()
134+
.map(ClapAlertClass)
135+
.collect()
136+
});
137+
&VALUE_VARIANTS
138+
}
139+
140+
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
141+
Some(
142+
clap::builder::PossibleValue::new(self.0.as_str())
143+
.help(self.0.description()),
144+
)
145+
}
146+
}
147+
148+
impl From<&'_ ClapAlertClass> for AlertClass {
149+
fn from(&ClapAlertClass(class): &ClapAlertClass) -> AlertClass {
150+
class.into()
151+
}
152+
}
153+
110154
#[derive(Debug, Args, Clone)]
111155
struct AlertInfoArgs {
112156
/// The ID of the alert to show
@@ -215,10 +259,10 @@ pub(super) async fn cmd_db_alert(
215259
) -> anyhow::Result<()> {
216260
match &args.command {
217261
Commands::Info(args) => {
218-
cmd_db_alert_info(datastore, fetch_opts, args).await
262+
cmd_db_alert_info(opctx, datastore, fetch_opts, args).await
219263
}
220264
Commands::List(args) => {
221-
cmd_db_alert_list(datastore, fetch_opts, args).await
265+
cmd_db_alert_list(opctx, datastore, fetch_opts, args).await
222266
}
223267
Commands::Webhook(args) => {
224268
cmd_db_webhook(opctx, datastore, fetch_opts, args).await
@@ -876,6 +920,7 @@ async fn cmd_db_webhook_delivery_info(
876920
}
877921

878922
async fn cmd_db_alert_list(
923+
opctx: &OpContext,
879924
datastore: &DataStore,
880925
fetch_opts: &DbFetchOptions,
881926
args: &AlertListArgs,
@@ -888,80 +933,57 @@ async fn cmd_db_alert_list(
888933
dispatched_before,
889934
dispatched_after,
890935
dispatched,
936+
classes,
891937
} = args;
892938

893-
if let (Some(before), Some(after)) = (before, after) {
894-
anyhow::ensure!(
895-
after < before,
896-
"if both `--after` and `--before` are included, after must be
897-
earlier than before"
898-
);
899-
}
900-
901-
if let (Some(before), Some(after)) = (dispatched_before, dispatched_after) {
902-
anyhow::ensure!(
903-
after < before,
904-
"if both `--dispatched-after` and `--dispatched-before` are
905-
included, after must be earlier than before"
906-
);
907-
}
908-
909-
let conn = datastore.pool_connection_for_tests().await?;
910-
911-
// Fetch all alerts up to our limit, including any corresponding
912-
// `rendezvous_alert_created` markers so we can display the generation at
913-
// which the alert was created. Note, all FM-created alerts can be
914-
// distinguished by their fm_case_id, but won't necessarily have a creation
915-
// marker: GC will clean up markers that are no longer needed.
916-
let mut query = alert_dsl::alert
917-
.left_join(rendezvous_created_dsl::rendezvous_alert_created)
918-
.limit(fetch_opts.fetch_limit.get().into())
919-
.order_by(alert_dsl::time_created.asc())
920-
.select((
921-
Alert::as_select(),
922-
Option::<RendezvousAlertCreated>::as_select(),
923-
))
924-
.into_boxed();
925-
926-
if let Some(before) = before {
927-
query = query.filter(alert_dsl::time_created.lt(*before));
928-
}
929-
930-
if let Some(after) = after {
931-
query = query.filter(alert_dsl::time_created.gt(*after));
932-
}
933-
934-
if let Some(before) = dispatched_before {
935-
query = query.filter(alert_dsl::time_dispatched.lt(*before));
936-
}
937-
938-
if let Some(after) = dispatched_after {
939-
query = query.filter(alert_dsl::time_dispatched.gt(*after));
940-
}
941-
942-
if let Some(dispatched) = dispatched {
943-
if *dispatched {
944-
query = query.filter(alert_dsl::time_dispatched.is_not_null());
945-
} else {
946-
query = query.filter(alert_dsl::time_dispatched.is_null());
939+
let filters = {
940+
let mut filters = nexus_db_queries::db::datastore::AlertFilters::new();
941+
if let &Some(before) = before {
942+
filters = filters.before(before)?;
947943
}
948-
}
944+
if let &Some(after) = after {
945+
filters = filters.after(after)?;
946+
}
947+
if let &Some(dispatched_before) = dispatched_before {
948+
filters = filters.dispatched_before(dispatched_before)?;
949+
}
950+
if let &Some(dispatched_after) = dispatched_after {
951+
filters = filters.dispatched_after(dispatched_after)?;
952+
}
953+
if let &Some(dispatched) = dispatched {
954+
filters = filters.dispatched(dispatched)?;
955+
}
956+
if !cases.is_empty() {
957+
filters = filters.for_fm_cases(
958+
cases.iter().copied().map(CaseUuid::from_untyped_uuid),
959+
);
960+
}
961+
if !classes.is_empty() {
962+
filters = filters.with_classes(classes);
963+
}
964+
filters
965+
};
949966

950-
if !cases.is_empty() {
951-
query = query.filter(alert_dsl::case_id.eq_any(cases.clone()));
952-
}
967+
let pagparams = DataPageParams {
968+
// Descending order shows the newest alerts first
969+
direction: dropshot::PaginationOrder::Descending,
970+
..first_page(fetch_opts.fetch_limit)
971+
};
953972

954973
let ctx = || "loading alerts";
955-
let alerts: Vec<(Alert, Option<RendezvousAlertCreated>)> =
956-
query.load_async(&*conn).await.with_context(ctx)?;
957-
974+
let alerts: Vec<(Alert, Option<RendezvousAlertCreated>)> = datastore
975+
.alert_list_matching(opctx, &filters, &pagparams)
976+
.await
977+
.with_context(ctx)?;
958978
check_limit(&alerts, fetch_opts.fetch_limit, ctx);
959979

960980
#[derive(Tabled)]
961981
#[tabled(rename_all = "SCREAMING_SNAKE_CASE")]
962982
struct AlertRow {
963983
id: Uuid,
964984
class: AlertClass,
985+
#[tabled(rename = "V")]
986+
version: u32,
965987
#[tabled(display_with = "datetime_rfc3339_concise")]
966988
time_created: DateTime<Utc>,
967989
#[tabled(display_with = "datetime_opt_rfc3339_concise")]
@@ -977,6 +999,7 @@ async fn cmd_db_alert_list(
977999
|alert: &Alert, marker: &Option<RendezvousAlertCreated>| AlertRow {
9781000
id: alert.identity.id.into_untyped_uuid(),
9791001
class: alert.class,
1002+
version: u32::from(alert.version),
9801003
time_created: alert.identity.time_created,
9811004
time_dispatched: alert.time_dispatched,
9821005
dispatched: alert.num_dispatched,
@@ -1022,35 +1045,31 @@ async fn cmd_db_alert_list(
10221045
}
10231046

10241047
async fn cmd_db_alert_info(
1048+
opctx: &OpContext,
10251049
datastore: &DataStore,
10261050
fetch_opts: &DbFetchOptions,
10271051
args: &AlertInfoArgs,
10281052
) -> anyhow::Result<()> {
10291053
let AlertInfoArgs { id } = args;
1030-
let conn = datastore.pool_connection_for_tests().await?;
1031-
1032-
// Fetch the requested alert, including any corresponding
1033-
// `rendezvous_alert_created` marker so we can display the generation at
1034-
// which the alert was created. Note, all FM-created alerts can be
1035-
// distinguished by their fm_case_id, but won't necessarily have a creation
1036-
// marker: GC will clean up markers that are no longer needed.
1037-
let (alert, rendezvous_created): (Alert, Option<RendezvousAlertCreated>) =
1038-
alert_dsl::alert
1039-
.left_join(rendezvous_created_dsl::rendezvous_alert_created)
1040-
.filter(alert_dsl::id.eq(id.into_untyped_uuid()))
1041-
.select((
1042-
Alert::as_select(),
1043-
Option::<RendezvousAlertCreated>::as_select(),
1044-
))
1045-
.limit(1)
1046-
.get_result_async(&*conn)
1047-
.await
1048-
.optional()
1049-
.with_context(|| format!("loading alert {id}"))?
1050-
.ok_or_else(|| anyhow::anyhow!("no alert {id} exists"))?;
1054+
let (authz_alert, alert) = LookupPath::new(opctx, datastore)
1055+
.alert_id(*id)
1056+
.fetch()
1057+
.await
1058+
.with_context(|| format!("failed to look up alert {id}"))?;
1059+
1060+
// Fetch any corresponding `rendezvous_alert_created` marker so we can
1061+
// display the generation at which the alert was created. Note, all
1062+
// FM-created alerts can be distinguished by their fm_case_id, but won't
1063+
// necessarily have a creation marker: GC will clean up markers that are no
1064+
// longer needed.
1065+
let rendezvous_created =
1066+
datastore.alert_fetch_fm_rendezvous_gen(opctx, &authz_alert).await;
1067+
if let Err(ref e) = rendezvous_created {
1068+
eprintln!("error: failed to fetch FM rendezvous marker: {e}");
1069+
}
10511070

10521071
let Alert {
1053-
identity: db::model::AlertIdentity { id, time_created, time_modified },
1072+
identity: db::model::AlertIdentity { time_created, time_modified, .. },
10541073
time_dispatched,
10551074
class,
10561075
payload,
@@ -1100,19 +1119,23 @@ async fn cmd_db_alert_info(
11001119
(Some(case_id), marker) => {
11011120
println!(" {PROVENANCE:>WIDTH$}: created by fault management");
11021121
println!(" {CASE_ID:>WIDTH$}: {case_id:?}");
1103-
if let Some(marker) = marker {
1104-
println!(
1105-
" {FM_GENERATION:>WIDTH$}: {}",
1106-
marker.created_at_generation.0
1107-
);
1122+
match marker {
1123+
Ok(Some(marker)) => {
1124+
println!(
1125+
" {FM_GENERATION:>WIDTH$}: {}",
1126+
marker.created_at_generation.0
1127+
);
1128+
}
1129+
Ok(None) => {} // may have been GCed...
1130+
Err(_) => {
1131+
println!(
1132+
" {FM_GENERATION:>WIDTH$}: /!\\ UNKNOWN (failed to \
1133+
fetch marker record)"
1134+
);
1135+
}
11081136
}
11091137
}
1110-
(None, None) => {
1111-
println!(
1112-
" {PROVENANCE:>WIDTH$}: not created by fault management"
1113-
);
1114-
}
1115-
(None, Some(marker)) => {
1138+
(None, Ok(Some(marker))) => {
11161139
println!(
11171140
" {PROVENANCE:>WIDTH$}: /!\\ WEIRD: creation marker present \
11181141
but no FM case (possible bug)"
@@ -1122,6 +1145,16 @@ async fn cmd_db_alert_info(
11221145
marker.created_at_generation.0
11231146
);
11241147
}
1148+
// Note that this includes both cases where we successfully fetched an
1149+
// `Ok(None)` marker record *and* cases where there was a database
1150+
// error. We already printed the db error earlier, so we need not print
1151+
// it again here, and we know the alert is not supposed to have one
1152+
// regardless.
1153+
(None, _) => {
1154+
println!(
1155+
" {PROVENANCE:>WIDTH$}: not created by fault management"
1156+
);
1157+
}
11251158
}
11261159

11271160
println!("\n{:=<80}", "== ALERT PAYLOAD ");
@@ -1130,11 +1163,14 @@ async fn cmd_db_alert_info(
11301163
)?;
11311164

11321165
let ctx = || format!("listing deliveries for alert {id:?}");
1133-
let deliveries = delivery_dsl::webhook_delivery
1134-
.limit(fetch_opts.fetch_limit.get().into())
1135-
.order_by(delivery_dsl::time_created.desc())
1136-
.select(WebhookDelivery::as_select())
1137-
.load_async(&*conn)
1166+
1167+
let pagparams = DataPageParams {
1168+
// Descending order shows the most recent delivery first
1169+
direction: dropshot::PaginationOrder::Descending,
1170+
..first_page(fetch_opts.fetch_limit)
1171+
};
1172+
let deliveries = datastore
1173+
.alert_list_webhook_deliveries(opctx, &authz_alert, &pagparams)
11381174
.await
11391175
.with_context(ctx)?;
11401176

dev-tools/omdb/tests/successes.out

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2318,6 +2318,26 @@ stderr:
23182318
note: using database URL postgresql://root@[::1]:REDACTED_PORT/omicron?sslmode=disable
23192319
note: database schema version matches expected (<redacted database version>)
23202320
=============================================
2321+
EXECUTING COMMAND: omdb ["db", "alert", "list", "--cases", "..........<REDACTED_UUID>...........", "..........<REDACTED_UUID>..........."]
2322+
termination: Exited(0)
2323+
---------------------------------------------
2324+
stdout:
2325+
ID CLASS V TIME_CREATED TIME_DISPATCHED DISPATCHED FM_CASE_ID FM_GEN
2326+
---------------------------------------------
2327+
stderr:
2328+
note: using database URL postgresql://root@[::1]:REDACTED_PORT/omicron?sslmode=disable
2329+
note: database schema version matches expected (<redacted database version>)
2330+
=============================================
2331+
EXECUTING COMMAND: omdb ["db", "alert", "list", "--classes", "test.foo.bar", "test.foo.baz"]
2332+
termination: Exited(0)
2333+
---------------------------------------------
2334+
stdout:
2335+
ID CLASS V TIME_CREATED TIME_DISPATCHED DISPATCHED FM_CASE_ID FM_GEN
2336+
---------------------------------------------
2337+
stderr:
2338+
note: using database URL postgresql://root@[::1]:REDACTED_PORT/omicron?sslmode=disable
2339+
note: database schema version matches expected (<redacted database version>)
2340+
=============================================
23212341
EXECUTING COMMAND: omdb ["--destructive", "db", "db-metadata", "force-mark-nexus-quiesced", "--skip-confirmation", "--skip-blueprint-validation", "..........<REDACTED_UUID>..........."]
23222342
termination: Exited(0)
23232343
---------------------------------------------

0 commit comments

Comments
 (0)