Skip to content

Commit beef2af

Browse files
committed
nexus-types: add SupportBundleRequest type, Display impls, and Case field
Add the SupportBundleRequest type to fm::case and the support_bundles_requested field to Case. Add Display impls for BundleData, BundleDataSelection, SledSelection, and EreportFilters so that case formatting can show the full data selection for each requested support bundle. Add Serialize/Deserialize derives to the support bundle selection types (BundleDataSelection, BundleData, BundleDataCategory, SledSelection, EreportFilters) since SupportBundleRequest contains Option<BundleDataSelection>. Add proptest Arbitrary impls for BundleDataSelection and EreportFilters (in test_utils modules) and a proptest-based serde round-trip test for BundleDataSelection. Add test_case_display coverage for support bundle requests, exercising both data_selection: None and Some with parameterized categories.
1 parent 7a5224c commit beef2af

5 files changed

Lines changed: 263 additions & 6 deletions

File tree

nexus/db-model/src/fm/case.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ impl CaseMetadata {
5858
de,
5959
comment,
6060
alerts_requested: _,
61+
support_bundles_requested: _,
6162
ereports: _,
6263
} = case;
6364
Self {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@ impl DataStore {
385385
comment,
386386
ereports,
387387
alerts_requested,
388+
support_bundles_requested: iddqd::IdOrdMap::new(),
388389
}
389390
}));
390391
}

nexus/types/src/fm/case.rs

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
use crate::alert::AlertClass;
66
use crate::fm::DiagnosisEngineKind;
77
use crate::fm::Ereport;
8+
use crate::support_bundle::BundleDataSelection;
89
use iddqd::{IdOrdItem, IdOrdMap};
9-
use omicron_uuid_kinds::{AlertUuid, CaseEreportUuid, CaseUuid, SitrepUuid};
10+
use omicron_uuid_kinds::{
11+
AlertUuid, CaseEreportUuid, CaseUuid, SitrepUuid, SupportBundleUuid,
12+
};
1013
use serde::{Deserialize, Serialize};
1114
use std::fmt;
1215
use std::sync::Arc;
@@ -21,6 +24,7 @@ pub struct Case {
2124

2225
pub ereports: IdOrdMap<CaseEreport>,
2326
pub alerts_requested: IdOrdMap<AlertRequest>,
27+
pub support_bundles_requested: IdOrdMap<SupportBundleRequest>,
2428

2529
pub comment: String,
2630
}
@@ -88,6 +92,22 @@ impl iddqd::IdOrdItem for AlertRequest {
8892
iddqd::id_upcast!();
8993
}
9094

95+
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
96+
pub struct SupportBundleRequest {
97+
pub id: SupportBundleUuid,
98+
pub requested_sitrep_id: SitrepUuid,
99+
pub data_selection: Option<BundleDataSelection>,
100+
}
101+
102+
impl iddqd::IdOrdItem for SupportBundleRequest {
103+
type Key<'a> = &'a SupportBundleUuid;
104+
fn key(&self) -> Self::Key<'_> {
105+
&self.id
106+
}
107+
108+
iddqd::id_upcast!();
109+
}
110+
91111
struct DisplayCase<'a> {
92112
case: &'a Case,
93113
indent: usize,
@@ -120,6 +140,7 @@ impl fmt::Display for DisplayCase<'_> {
120140
ereports,
121141
comment,
122142
alerts_requested,
143+
support_bundles_requested,
123144
},
124145
indent,
125146
sitrep_id,
@@ -234,6 +255,47 @@ impl fmt::Display for DisplayCase<'_> {
234255
}
235256
}
236257

258+
if !support_bundles_requested.is_empty() {
259+
writeln!(f, "\n{:>indent$}support bundles requested:", "")?;
260+
writeln!(f, "{:>indent$}-------------------------", "")?;
261+
262+
let indent = indent + 2;
263+
for SupportBundleRequest {
264+
id,
265+
requested_sitrep_id,
266+
data_selection,
267+
} in support_bundles_requested.iter()
268+
{
269+
const REQUESTED_IN: &str = "requested in:";
270+
const DATA: &str = "data:";
271+
const WIDTH: usize = const_max_len(&[REQUESTED_IN, DATA]);
272+
273+
writeln!(f, "{BULLET:>indent$}bundle {id}",)?;
274+
writeln!(
275+
f,
276+
"{:>indent$}{REQUESTED_IN:<WIDTH$} {requested_sitrep_id}{}",
277+
"",
278+
this_sitrep(*requested_sitrep_id)
279+
)?;
280+
match data_selection {
281+
None => {
282+
writeln!(
283+
f,
284+
"{:>indent$}{DATA:<WIDTH$} all (default)\n",
285+
"",
286+
)?;
287+
}
288+
Some(selection) => {
289+
writeln!(
290+
f,
291+
"{:>indent$}{DATA:<WIDTH$} {selection}\n",
292+
"",
293+
)?;
294+
}
295+
}
296+
}
297+
}
298+
237299
writeln!(f)?;
238300

239301
Ok(())
@@ -248,6 +310,7 @@ mod tests {
248310
use ereport_types::{Ena, EreportId};
249311
use omicron_uuid_kinds::{
250312
AlertUuid, CaseUuid, EreporterRestartUuid, OmicronZoneUuid, SitrepUuid,
313+
SupportBundleUuid,
251314
};
252315
use std::str::FromStr;
253316
use std::sync::Arc;
@@ -276,6 +339,12 @@ mod tests {
276339
let alert2_id =
277340
AlertUuid::from_str("8a6f88ef-c436-44a9-b4cb-cae91d7306c9")
278341
.unwrap();
342+
let bundle1_id =
343+
SupportBundleUuid::from_str("d1a2b3c4-e5f6-7890-abcd-ef1234567890")
344+
.unwrap();
345+
let bundle2_id =
346+
SupportBundleUuid::from_str("a9b8c7d6-e5f4-3210-fedc-ba0987654321")
347+
.unwrap();
279348

280349
// Create some ereports
281350
let mut ereports = IdOrdMap::new();
@@ -349,6 +418,38 @@ mod tests {
349418
})
350419
.unwrap();
351420

421+
use crate::support_bundle::{BundleData, BundleDataSelection};
422+
let mut bundle1_data = BundleDataSelection::new();
423+
bundle1_data.insert(BundleData::Reconfigurator);
424+
bundle1_data.insert(BundleData::SpDumps);
425+
bundle1_data.insert(BundleData::HostInfo(
426+
std::collections::HashSet::from([
427+
crate::support_bundle::SledSelection::All,
428+
]),
429+
));
430+
bundle1_data.insert(BundleData::Ereports(
431+
crate::fm::ereport::EreportFilters {
432+
only_classes: vec!["hw.pwr.*".to_string()],
433+
..Default::default()
434+
},
435+
));
436+
437+
let mut support_bundles_requested = IdOrdMap::new();
438+
support_bundles_requested
439+
.insert_unique(SupportBundleRequest {
440+
id: bundle1_id,
441+
requested_sitrep_id: created_sitrep_id,
442+
data_selection: Some(bundle1_data),
443+
})
444+
.unwrap();
445+
support_bundles_requested
446+
.insert_unique(SupportBundleRequest {
447+
id: bundle2_id,
448+
requested_sitrep_id: closed_sitrep_id,
449+
data_selection: None,
450+
})
451+
.unwrap();
452+
352453
// Create the case
353454
let case = Case {
354455
id: case_id,
@@ -357,6 +458,7 @@ mod tests {
357458
de: DiagnosisEngineKind::PowerShelf,
358459
ereports,
359460
alerts_requested,
461+
support_bundles_requested,
360462
comment: "Power shelf rectifier added and removed here :-)"
361463
.to_string(),
362464
};

nexus/types/src/fm/ereport.rs

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ fn get_sp_metadata_string(
217217
}
218218

219219
/// A set of filters for fetching ereports.
220-
#[derive(Clone, Debug, Default, Eq, PartialEq)]
220+
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
221221
pub struct EreportFilters {
222222
/// If present, include only ereports that were collected at the specified
223223
/// timestamp or later.
@@ -240,6 +240,55 @@ pub struct EreportFilters {
240240
pub only_classes: Vec<String>,
241241
}
242242

243+
impl fmt::Display for EreportFilters {
244+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245+
use itertools::Itertools;
246+
247+
// Writes a semicolon-separated part to the formatter, tracking whether
248+
// we've written anything yet.
249+
let mut empty = true;
250+
let mut fmt_part =
251+
|f: &mut fmt::Formatter, args: fmt::Arguments| -> fmt::Result {
252+
if !empty {
253+
write!(f, "; ")?;
254+
}
255+
empty = false;
256+
f.write_fmt(args)
257+
};
258+
259+
if let Some(start) = &self.start_time {
260+
fmt_part(f, format_args!("start: {start}"))?;
261+
}
262+
if let Some(end) = &self.end_time {
263+
fmt_part(f, format_args!("end: {end}"))?;
264+
}
265+
if !self.only_serials.is_empty() {
266+
fmt_part(
267+
f,
268+
format_args!(
269+
"serials: {}",
270+
self.only_serials.iter().format(", ")
271+
),
272+
)?;
273+
}
274+
if !self.only_classes.is_empty() {
275+
fmt_part(
276+
f,
277+
format_args!(
278+
"classes: {}",
279+
self.only_classes.iter().format(", ")
280+
),
281+
)?;
282+
}
283+
284+
// If no filters are set, display "none" rather than empty output.
285+
if empty {
286+
write!(f, "none")?;
287+
}
288+
Ok(())
289+
}
290+
}
291+
243292
impl EreportFilters {
244293
pub fn check_time_range(&self) -> Result<(), Error> {
245294
if let (Some(start), Some(end)) = (self.start_time, self.end_time) {
@@ -253,3 +302,40 @@ impl EreportFilters {
253302
Ok(())
254303
}
255304
}
305+
306+
#[cfg(test)]
307+
pub(crate) mod test_utils {
308+
use super::*;
309+
use proptest::prelude::*;
310+
311+
fn arb_datetime() -> impl Strategy<Value = DateTime<Utc>> {
312+
// Generate timestamps in a reasonable range (2020-2030).
313+
(1577836800i64..1893456000i64)
314+
.prop_map(|secs| DateTime::from_timestamp(secs, 0).unwrap())
315+
}
316+
317+
impl Arbitrary for EreportFilters {
318+
type Parameters = ();
319+
type Strategy = BoxedStrategy<Self>;
320+
321+
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
322+
(
323+
prop::option::of(arb_datetime()),
324+
prop::option::of(arb_datetime()),
325+
prop::collection::vec(".*", 0..=3),
326+
prop::collection::vec(".*", 0..=3),
327+
)
328+
.prop_map(
329+
|(start_time, end_time, only_serials, only_classes)| {
330+
EreportFilters {
331+
start_time,
332+
end_time,
333+
only_serials,
334+
only_classes,
335+
}
336+
},
337+
)
338+
.boxed()
339+
}
340+
}
341+
}

0 commit comments

Comments
 (0)