Skip to content

Commit 81868f1

Browse files
committed
single view for ereports to make fetching planning input easier
1 parent 9a48605 commit 81868f1

File tree

5 files changed

+319
-29
lines changed

5 files changed

+319
-29
lines changed

nexus/db-model/src/ereport.rs

Lines changed: 138 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// License, v. 2.0. If a copy of the MPL was not distributed with this
33
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
44

5+
use super::impl_enum_type;
56
use crate::SpMgsSlot;
67
use crate::SpType;
78
use crate::typed_uuid::DbTypedUuid;
@@ -12,9 +13,9 @@ use diesel::pg::Pg;
1213
use diesel::prelude::*;
1314
use diesel::serialize::{self, ToSql};
1415
use diesel::sql_types;
15-
use nexus_db_schema::schema::{host_ereport, sp_ereport};
16+
use nexus_db_schema::schema::{ereport, host_ereport, sp_ereport};
1617
use nexus_types::fm::ereport::{
17-
Ena, Ereport, EreportId, EreportMetadata, Reporter,
18+
self as fm, Ena, EreportId, EreportMetadata, Reporter,
1819
};
1920
use omicron_uuid_kinds::{EreporterRestartKind, OmicronZoneKind, SledKind};
2021
use serde::{Deserialize, Serialize};
@@ -62,6 +63,133 @@ where
6263
}
6364
}
6465

66+
impl_enum_type!(
67+
EreporterKindEnum:
68+
69+
#[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, Serialize, Deserialize, PartialEq)]
70+
pub enum EreporterKind;
71+
72+
// Enum values
73+
Sp => b"sp"
74+
Host => b"host"
75+
);
76+
77+
#[derive(Clone, Debug, Queryable, Selectable)]
78+
#[diesel(table_name = ereport)]
79+
pub struct Ereport {
80+
pub restart_id: DbTypedUuid<EreporterRestartKind>,
81+
pub ena: DbEna,
82+
pub time_collected: DateTime<Utc>,
83+
pub collector_id: DbTypedUuid<OmicronZoneKind>,
84+
pub reporter_kind: EreporterKind,
85+
86+
// The physical location of the reporting SP.
87+
//
88+
/// SP location: the type of SP slot (sled, switch, power shelf).
89+
///
90+
/// This is always known, as SPs are indexed by physical location when
91+
/// collecting ereports from MGS.
92+
pub sp_type: Option<SpType>,
93+
/// SP location: the slot number.
94+
///
95+
/// This is always known, as SPs are indexed by physical location when
96+
/// collecting ereports from MGS.
97+
pub sp_slot: Option<SpMgsSlot>,
98+
pub sled_id: Option<DbTypedUuid<SledKind>>,
99+
100+
/// SP VPD identity: the baseboard part number of the reporting SP.
101+
///
102+
/// This is nullable, as the ereport may have been generated in a condition
103+
/// where the SP was unable to determine its own part number. Consider that
104+
/// "I don't know what I am!" is an error condition for which we might want
105+
/// to generate an ereport!
106+
pub part_number: Option<String>,
107+
/// SP VPD identity: the baseboard serial number of the reporting SP.
108+
///
109+
/// This is nullable, as the ereport may have been generated in a condition
110+
/// where the SP was unable to determine its own serial number. Consider that
111+
/// "I don't know who I am!" is an error condition for which we might want
112+
/// to generate an ereport!
113+
pub serial_number: Option<String>,
114+
/// The ereport class, which indicates the category of event reported.
115+
///
116+
/// This is nullable, as it is extracted from the report JSON, and reports
117+
/// missing class information must still be ingested.
118+
pub class: Option<String>,
119+
120+
pub report: serde_json::Value,
121+
}
122+
123+
impl TryFrom<Ereport> for fm::Ereport {
124+
type Error = anyhow::Error;
125+
fn try_from(ereport: Ereport) -> Result<Self, Self::Error> {
126+
let reporter = match &ereport {
127+
Ereport {
128+
reporter_kind: EreporterKind::Sp,
129+
sp_slot: Some(slot),
130+
sp_type: Some(sp_type),
131+
..
132+
} => Reporter::Sp { sp_type: (*sp_type).into(), slot: slot.0 },
133+
Ereport {
134+
reporter_kind: EreporterKind::Sp,
135+
sp_slot,
136+
sp_type,
137+
..
138+
} => {
139+
anyhow::bail!(
140+
"an ereport with reporter_kind='sp' should have a slot \
141+
and sp_type, but found sp_slot={sp_slot:?}, \
142+
sp_type={sp_type:?}",
143+
);
144+
}
145+
Ereport {
146+
reporter_kind: EreporterKind::Host,
147+
sled_id: Some(sled_id),
148+
..
149+
} => Reporter::HostOs { sled: (*sled_id).into() },
150+
Ereport {
151+
reporter_kind: EreporterKind::Host,
152+
sled_id: None,
153+
..
154+
} => {
155+
anyhow::bail!(
156+
"an ereport with reporter_kind='host' should have a \
157+
non-null sled UUID"
158+
);
159+
}
160+
};
161+
162+
let Ereport {
163+
restart_id,
164+
ena,
165+
reporter_kind: _,
166+
time_collected,
167+
collector_id,
168+
part_number,
169+
serial_number,
170+
sp_type: _,
171+
sp_slot: _,
172+
sled_id: _,
173+
class,
174+
report,
175+
} = ereport;
176+
Ok(fm::Ereport {
177+
id: EreportId { restart_id: restart_id.into(), ena: ena.into() },
178+
part_number,
179+
serial_number,
180+
class,
181+
metadata: EreportMetadata {
182+
time_collected,
183+
// The `ereport` view only contains ereports which haven't been deleted
184+
time_deleted: None,
185+
collector_id: collector_id.into(),
186+
},
187+
reporter,
188+
report,
189+
})
190+
}
191+
}
192+
65193
#[derive(Clone, Debug, Insertable, Queryable, Selectable)]
66194
#[diesel(table_name = sp_ereport)]
67195
pub struct SpEreport {
@@ -109,7 +237,7 @@ pub struct SpEreport {
109237
pub report: serde_json::Value,
110238
}
111239

112-
impl From<SpEreport> for Ereport {
240+
impl From<SpEreport> for fm::Ereport {
113241
fn from(sp_report: SpEreport) -> Self {
114242
let SpEreport {
115243
restart_id,
@@ -124,7 +252,7 @@ impl From<SpEreport> for Ereport {
124252
class,
125253
report,
126254
} = sp_report;
127-
Ereport {
255+
fm::Ereport {
128256
id: EreportId { restart_id: restart_id.into(), ena: ena.into() },
129257
part_number,
130258
serial_number,
@@ -167,7 +295,7 @@ pub struct HostEreport {
167295
pub part_number: Option<String>,
168296
}
169297

170-
impl From<HostEreport> for Ereport {
298+
impl From<HostEreport> for fm::Ereport {
171299
fn from(host_report: HostEreport) -> Self {
172300
let HostEreport {
173301
restart_id,
@@ -181,8 +309,11 @@ impl From<HostEreport> for Ereport {
181309
report,
182310
part_number,
183311
} = host_report;
184-
Ereport {
185-
id: EreportId { restart_id: restart_id.into(), ena: ena.into() },
312+
fm::Ereport {
313+
id: fm::EreportId {
314+
restart_id: restart_id.into(),
315+
ena: ena.into(),
316+
},
186317
part_number,
187318
serial_number: Some(sled_serial),
188319
class,

nexus/db-queries/src/db/datastore/ereport.rs

Lines changed: 115 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use super::DataStore;
88
use crate::authz;
99
use crate::context::OpContext;
1010
use crate::db::datastore::RunnableQuery;
11+
use crate::db::model;
1112
use crate::db::model::DbEna;
1213
use crate::db::model::HostEreport;
1314
use crate::db::model::SpEreport;
@@ -24,6 +25,7 @@ use diesel::prelude::*;
2425
use nexus_db_errors::ErrorHandler;
2526
use nexus_db_errors::public_error_from_diesel;
2627
use nexus_db_lookup::DbConnection;
28+
use nexus_db_schema::schema::ereport::dsl;
2729
use nexus_db_schema::schema::host_ereport::dsl as host_dsl;
2830
use nexus_db_schema::schema::sp_ereport::dsl as sp_dsl;
2931
use nexus_types::fm::ereport::{Ereport, Reporter};
@@ -101,33 +103,21 @@ impl DataStore {
101103
let restart_id = id.restart_id.into_untyped_uuid();
102104
let ena = DbEna::from(id.ena);
103105

104-
if let Some(report) = sp_dsl::sp_ereport
105-
.filter(sp_dsl::restart_id.eq(restart_id))
106-
.filter(sp_dsl::ena.eq(ena))
107-
.filter(sp_dsl::time_deleted.is_null())
108-
.select(SpEreport::as_select())
109-
.first_async(&*conn)
110-
.await
111-
.optional()
112-
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?
113-
{
114-
return Ok(report.into());
115-
}
116-
117-
if let Some(report) = host_dsl::host_ereport
118-
.filter(host_dsl::restart_id.eq(restart_id))
119-
.filter(host_dsl::ena.eq(ena))
120-
.filter(host_dsl::time_deleted.is_null())
121-
.select(HostEreport::as_select())
106+
let ereport = dsl::ereport
107+
.filter(dsl::restart_id.eq(restart_id))
108+
.filter(dsl::ena.eq(ena))
109+
.select(model::Ereport::as_select())
122110
.first_async(&*conn)
123111
.await
124112
.optional()
125113
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?
126-
{
127-
return Ok(report.into());
128-
}
114+
.ok_or_else(|| {
115+
Error::non_resourcetype_not_found(format!("ereport {id}"))
116+
})?;
129117

130-
Err(Error::non_resourcetype_not_found(format!("ereport {id}")))
118+
ereport.try_into().map_err(|e: anyhow::Error| Error::InternalError {
119+
internal_message: e.to_string(),
120+
})
131121
}
132122

133123
pub async fn host_ereports_fetch_matching(
@@ -492,7 +482,9 @@ mod tests {
492482
use super::*;
493483
use crate::db::explain::ExplainableAsync;
494484
use crate::db::pub_test_utils::TestDatabase;
485+
use nexus_types::fm::ereport::Ena;
495486
use omicron_test_utils::dev;
487+
use omicron_uuid_kinds::OmicronZoneUuid;
496488

497489
#[tokio::test]
498490
async fn explain_sp_latest_ereport_id() {
@@ -596,4 +588,105 @@ mod tests {
596588
db.terminate().await;
597589
logctx.cleanup_successful();
598590
}
591+
592+
#[tokio::test]
593+
async fn test_fetch_ereports_by_view() {
594+
let logctx = dev::test_setup_log("test_fetch_ereports_by_view");
595+
let db = TestDatabase::new_with_datastore(&logctx.log).await;
596+
let (opctx, datastore) = (db.opctx(), db.datastore());
597+
598+
let host_restart_id = EreporterRestartUuid::new_v4();
599+
let sled_id = SledUuid::new_v4();
600+
let host_ena = Ena(2);
601+
let host_ereport = HostEreport {
602+
restart_id: host_restart_id.into(),
603+
ena: host_ena.into(),
604+
time_deleted: None,
605+
606+
time_collected: Utc::now(),
607+
collector_id: OmicronZoneUuid::new_v4().into(),
608+
609+
sled_id: sled_id.into(),
610+
sled_serial: "BRM6900420".to_string(),
611+
part_number: Some("fakegimlet".to_string()),
612+
class: Some("my.cool.ereport".to_string()),
613+
614+
report: serde_json::json!({
615+
"computer": "very gone",
616+
"badness": 10000,
617+
}),
618+
};
619+
620+
let (created, _) = datastore
621+
.host_ereports_insert(&opctx, sled_id, vec![host_ereport])
622+
.await
623+
.expect("host ereport should be inserted");
624+
assert_eq!(created, 1);
625+
626+
let sp_restart_id = EreporterRestartUuid::new_v4();
627+
let sp_ena = Ena(2); // same ENA!
628+
let sp_type = SpType::Sled;
629+
let slot = 19u16;
630+
let sp_ereport = SpEreport {
631+
restart_id: sp_restart_id.into(),
632+
ena: sp_ena.into(),
633+
time_deleted: None,
634+
635+
time_collected: Utc::now(),
636+
collector_id: OmicronZoneUuid::new_v4().into(),
637+
638+
sp_type,
639+
sp_slot: slot.into(),
640+
// pretend to be the same sled
641+
serial_number: Some("BRM6900420".to_string()),
642+
part_number: Some("fakegimlet".to_string()),
643+
class: Some("my.cool.sp.ereport".to_string()),
644+
645+
report: serde_json::json!({
646+
"hello": "world",
647+
"im_a_sp": true,
648+
}),
649+
};
650+
651+
let (created, _) = datastore
652+
.sp_ereports_insert(&opctx, sp_type, slot, vec![sp_ereport])
653+
.await
654+
.expect("SP ereport should be inserted");
655+
assert_eq!(created, 1);
656+
657+
let fetched_host_ereport = datastore
658+
.ereport_fetch(
659+
&opctx,
660+
ereport_types::EreportId {
661+
restart_id: host_restart_id,
662+
ena: host_ena,
663+
},
664+
)
665+
.await
666+
.expect("host ereport fetch should succeed");
667+
dbg!(&fetched_host_ereport);
668+
assert_eq!(
669+
fetched_host_ereport.reporter,
670+
Reporter::HostOs { sled: sled_id }
671+
);
672+
673+
let fetched_sp_ereport = datastore
674+
.ereport_fetch(
675+
&opctx,
676+
ereport_types::EreportId {
677+
restart_id: sp_restart_id,
678+
ena: sp_ena,
679+
},
680+
)
681+
.await
682+
.expect("SP ereport fetch should succeed");
683+
dbg!(&fetched_sp_ereport);
684+
assert_eq!(
685+
fetched_sp_ereport.reporter,
686+
Reporter::Sp { sp_type: sp_type.into(), slot }
687+
);
688+
689+
db.terminate().await;
690+
logctx.cleanup_successful();
691+
}
599692
}

nexus/db-schema/src/enums.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ define_enums! {
4242
DnsGroupEnum => "dns_group",
4343
DownstairsClientStopRequestReasonEnum => "downstairs_client_stop_request_reason_type",
4444
DownstairsClientStoppedReasonEnum => "downstairs_client_stopped_reason_type",
45+
EreporterKindEnum => "ereporter_kind",
4546
FailureDomainEnum => "failure_domain",
4647
HwM2SlotEnum => "hw_m2_slot",
4748
HwPowerStateEnum => "hw_power_state",
@@ -109,3 +110,4 @@ define_enums! {
109110
WebhookDeliveryAttemptResultEnum => "webhook_delivery_attempt_result",
110111
ZoneTypeEnum => "zone_type",
111112
}
113+

0 commit comments

Comments
 (0)