Skip to content

Commit bd75858

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 3a6ea71 commit bd75858

7 files changed

Lines changed: 329 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: 4 additions & 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
}
@@ -1552,6 +1553,7 @@ mod tests {
15521553
de,
15531554
ereports,
15541555
alerts_requested,
1556+
support_bundles_requested: _,
15551557
} = case;
15561558
let case_id = id;
15571559
let Some(expected) = this.cases.get(&case_id) else {
@@ -1770,6 +1772,7 @@ mod tests {
17701772
de: fm::DiagnosisEngineKind::PowerShelf,
17711773
ereports,
17721774
alerts_requested,
1775+
support_bundles_requested: iddqd::IdOrdMap::new(),
17731776
comment: "my cool case".to_string(),
17741777
}
17751778
};
@@ -1802,6 +1805,7 @@ mod tests {
18021805
de: fm::DiagnosisEngineKind::PowerShelf,
18031806
ereports,
18041807
alerts_requested,
1808+
support_bundles_requested: iddqd::IdOrdMap::new(),
18051809
comment: "break in case of emergency".to_string(),
18061810
}
18071811
};

nexus/fm/src/builder/case.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ impl AllCases {
6767
comment: String::new(),
6868
ereports: Default::default(),
6969
alerts_requested: Default::default(),
70+
support_bundles_requested: Default::default(),
7071
};
7172
entry.insert(CaseBuilder::new(
7273
&self.log, sitrep_id, case, case_rng,

nexus/src/app/background/tasks/fm_rendezvous.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ mod tests {
230230
de: fm::DiagnosisEngineKind::PowerShelf,
231231
alerts_requested: iddqd::IdOrdMap::new(),
232232
ereports: iddqd::IdOrdMap::new(),
233+
support_bundles_requested: iddqd::IdOrdMap::new(),
233234
comment: "my great case".to_string(),
234235
};
235236
case1
@@ -305,6 +306,7 @@ mod tests {
305306
de: fm::DiagnosisEngineKind::PowerShelf,
306307
alerts_requested: iddqd::IdOrdMap::new(),
307308
ereports: iddqd::IdOrdMap::new(),
309+
support_bundles_requested: iddqd::IdOrdMap::new(),
308310
comment: "my other great case".to_string(),
309311
};
310312
case2

nexus/types/src/fm/case.rs

Lines changed: 99 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,44 @@ 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!(f, "{:>indent$}{DATA}", "")?;
290+
writeln!(f, "{}\n", selection.display(indent + 2))?;
291+
}
292+
}
293+
}
294+
}
295+
237296
writeln!(f)?;
238297

239298
Ok(())
@@ -244,11 +303,17 @@ impl fmt::Display for DisplayCase<'_> {
244303
mod tests {
245304
use super::*;
246305
use crate::fm::DiagnosisEngineKind;
306+
use crate::fm::ereport::EreportFilters;
247307
use crate::inventory::SpType;
308+
use crate::support_bundle::{
309+
BundleData, BundleDataSelection, SledSelection,
310+
};
248311
use ereport_types::{Ena, EreportId};
249312
use omicron_uuid_kinds::{
250313
AlertUuid, CaseUuid, EreporterRestartUuid, OmicronZoneUuid, SitrepUuid,
314+
SupportBundleUuid,
251315
};
316+
use std::collections::HashSet;
252317
use std::str::FromStr;
253318
use std::sync::Arc;
254319

@@ -276,6 +341,12 @@ mod tests {
276341
let alert2_id =
277342
AlertUuid::from_str("8a6f88ef-c436-44a9-b4cb-cae91d7306c9")
278343
.unwrap();
344+
let bundle1_id =
345+
SupportBundleUuid::from_str("d1a2b3c4-e5f6-7890-abcd-ef1234567890")
346+
.unwrap();
347+
let bundle2_id =
348+
SupportBundleUuid::from_str("a9b8c7d6-e5f4-3210-fedc-ba0987654321")
349+
.unwrap();
279350

280351
// Create some ereports
281352
let mut ereports = IdOrdMap::new();
@@ -349,6 +420,32 @@ mod tests {
349420
})
350421
.unwrap();
351422

423+
let mut bundle1_data = BundleDataSelection::new();
424+
bundle1_data.insert(BundleData::Reconfigurator);
425+
bundle1_data.insert(BundleData::SpDumps);
426+
bundle1_data
427+
.insert(BundleData::HostInfo(HashSet::from([SledSelection::All])));
428+
bundle1_data.insert(BundleData::Ereports(EreportFilters {
429+
only_classes: vec!["hw.pwr.*".to_string()],
430+
..Default::default()
431+
}));
432+
433+
let mut support_bundles_requested = IdOrdMap::new();
434+
support_bundles_requested
435+
.insert_unique(SupportBundleRequest {
436+
id: bundle1_id,
437+
requested_sitrep_id: created_sitrep_id,
438+
data_selection: Some(bundle1_data),
439+
})
440+
.unwrap();
441+
support_bundles_requested
442+
.insert_unique(SupportBundleRequest {
443+
id: bundle2_id,
444+
requested_sitrep_id: closed_sitrep_id,
445+
data_selection: None,
446+
})
447+
.unwrap();
448+
352449
// Create the case
353450
let case = Case {
354451
id: case_id,
@@ -357,6 +454,7 @@ mod tests {
357454
de: DiagnosisEngineKind::PowerShelf,
358455
ereports,
359456
alerts_requested,
457+
support_bundles_requested,
360458
comment: "Power shelf rectifier added and removed here :-)"
361459
.to_string(),
362460
};

nexus/types/src/fm/ereport.rs

Lines changed: 101 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,69 @@ pub struct EreportFilters {
240240
pub only_classes: Vec<String>,
241241
}
242242

243+
/// Displayer for pretty-printing [`EreportFilters`].
244+
#[must_use = "this struct does nothing unless displayed"]
245+
pub struct DisplayEreportFilters<'a> {
246+
filters: &'a EreportFilters,
247+
}
248+
249+
impl EreportFilters {
250+
pub fn display(&self) -> DisplayEreportFilters<'_> {
251+
DisplayEreportFilters { filters: self }
252+
}
253+
}
254+
255+
impl fmt::Display for DisplayEreportFilters<'_> {
256+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257+
use itertools::Itertools;
258+
259+
let filters = self.filters;
260+
261+
// Writes a semicolon-separated part to the formatter, tracking whether
262+
// we've written anything yet.
263+
let mut empty = true;
264+
let mut fmt_part =
265+
|f: &mut fmt::Formatter, args: fmt::Arguments| -> fmt::Result {
266+
if !empty {
267+
write!(f, "; ")?;
268+
}
269+
empty = false;
270+
f.write_fmt(args)
271+
};
272+
273+
if let Some(start) = &filters.start_time {
274+
fmt_part(f, format_args!("start: {start}"))?;
275+
}
276+
if let Some(end) = &filters.end_time {
277+
fmt_part(f, format_args!("end: {end}"))?;
278+
}
279+
if !filters.only_serials.is_empty() {
280+
fmt_part(
281+
f,
282+
format_args!(
283+
"serials: {}",
284+
filters.only_serials.iter().format(", ")
285+
),
286+
)?;
287+
}
288+
if !filters.only_classes.is_empty() {
289+
fmt_part(
290+
f,
291+
format_args!(
292+
"classes: {}",
293+
filters.only_classes.iter().format(", ")
294+
),
295+
)?;
296+
}
297+
298+
// If no filters are set, display "none" rather than empty output.
299+
if empty {
300+
write!(f, "none")?;
301+
}
302+
Ok(())
303+
}
304+
}
305+
243306
impl EreportFilters {
244307
pub fn check_time_range(&self) -> Result<(), Error> {
245308
if let (Some(start), Some(end)) = (self.start_time, self.end_time) {
@@ -253,3 +316,40 @@ impl EreportFilters {
253316
Ok(())
254317
}
255318
}
319+
320+
#[cfg(test)]
321+
pub(crate) mod test_utils {
322+
use super::*;
323+
use proptest::prelude::*;
324+
325+
fn arb_datetime() -> impl Strategy<Value = DateTime<Utc>> {
326+
// Generate timestamps in a reasonable range (2020-2030).
327+
(1577836800i64..1893456000i64)
328+
.prop_map(|secs| DateTime::from_timestamp(secs, 0).unwrap())
329+
}
330+
331+
impl Arbitrary for EreportFilters {
332+
type Parameters = ();
333+
type Strategy = BoxedStrategy<Self>;
334+
335+
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
336+
(
337+
prop::option::of(arb_datetime()),
338+
prop::option::of(arb_datetime()),
339+
prop::collection::vec(".*", 0..=3),
340+
prop::collection::vec(".*", 0..=3),
341+
)
342+
.prop_map(
343+
|(start_time, end_time, only_serials, only_classes)| {
344+
EreportFilters {
345+
start_time,
346+
end_time,
347+
only_serials,
348+
only_classes,
349+
}
350+
},
351+
)
352+
.boxed()
353+
}
354+
}
355+
}

0 commit comments

Comments
 (0)