Skip to content

Commit 72b5433

Browse files
nexus-types: add SupportBundleRequest type and Case field (#10090)
The core thing this PR does is: * Introduce a new `SupportBundleRequest` type to `fm::case` (analogous to @hawkw's `AlertRequest`), and * Introduce a new `support_bundles_requested` field to `Case` (analogous to `alerts_requested`). There's a lot of ✨ceremony✨ around this small change, though: * `DisplayCase` has a sophisticated `impl Display` that formats the stuff it contains. So we need `Display` impls for all the things `SupportBundleRequest` transitively contains (`BundleData`, etc.). Here's what this comes out looking like: ``` support bundles requested: ------------------------- * bundle a9b8c7d6-e5f4-3210-fedc-ba0987654321 requested in: bba63ba7-bf6b-45f4-b241-d13ccd07fe1c <-- this sitrep data: all (default) * bundle d1a2b3c4-e5f6-7890-abcd-ef1234567890 requested in: ea7affb0-36eb-4a9a-b9bd-22e00f1bcc04 data: - sp_dumps - ereports(classes: hw.pwr.*) - host_info(all) - reconfigurator ``` * Since `Case` is `serde::Serialize/Deserialize`, we also need to add derives to `SupportBundleRequest` and all the things it transitively contains. * Lastly, since we want to have some sort of reasonable round-trip test for the serialization / deserialization, we use proptest to generate arbitrary inputs. This requires `Arbitrary` impls (or functions that return `Strategy`) for all the things too. Context: #10062.
1 parent 14712b1 commit 72b5433

7 files changed

Lines changed: 319 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: 91 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,27 @@ impl iddqd::IdOrdItem for AlertRequest {
8892
iddqd::id_upcast!();
8993
}
9094

95+
/// A request to create a support bundle, associated with a [`Case`].
96+
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
97+
pub struct SupportBundleRequest {
98+
/// Unique identifier for this support bundle request.
99+
pub id: SupportBundleUuid,
100+
/// The sitrep in which this support bundle was requested.
101+
pub requested_sitrep_id: SitrepUuid,
102+
/// Which data to include in the support bundle. Use
103+
/// [`BundleDataSelection::default()`] to request all default data.
104+
pub data_selection: BundleDataSelection,
105+
}
106+
107+
impl iddqd::IdOrdItem for SupportBundleRequest {
108+
type Key<'a> = &'a SupportBundleUuid;
109+
fn key(&self) -> Self::Key<'_> {
110+
&self.id
111+
}
112+
113+
iddqd::id_upcast!();
114+
}
115+
91116
struct DisplayCase<'a> {
92117
case: &'a Case,
93118
indent: usize,
@@ -120,6 +145,7 @@ impl fmt::Display for DisplayCase<'_> {
120145
ereports,
121146
comment,
122147
alerts_requested,
148+
support_bundles_requested,
123149
},
124150
indent,
125151
sitrep_id,
@@ -234,6 +260,33 @@ impl fmt::Display for DisplayCase<'_> {
234260
}
235261
}
236262

263+
if !support_bundles_requested.is_empty() {
264+
writeln!(f, "\n{:>indent$}support bundles requested:", "")?;
265+
writeln!(f, "{:>indent$}-------------------------", "")?;
266+
267+
let indent = indent + 2;
268+
for SupportBundleRequest {
269+
id,
270+
requested_sitrep_id,
271+
data_selection,
272+
} in support_bundles_requested.iter()
273+
{
274+
const REQUESTED_IN: &str = "requested in:";
275+
const DATA: &str = "data:";
276+
const WIDTH: usize = const_max_len(&[REQUESTED_IN, DATA]);
277+
278+
writeln!(f, "{BULLET:>indent$}bundle {id}",)?;
279+
writeln!(
280+
f,
281+
"{:>indent$}{REQUESTED_IN:<WIDTH$} {requested_sitrep_id}{}",
282+
"",
283+
this_sitrep(*requested_sitrep_id)
284+
)?;
285+
writeln!(f, "{:>indent$}{DATA}", "")?;
286+
writeln!(f, "{}\n", data_selection.display(indent + 2))?;
287+
}
288+
}
289+
237290
writeln!(f)?;
238291

239292
Ok(())
@@ -244,10 +297,15 @@ impl fmt::Display for DisplayCase<'_> {
244297
mod tests {
245298
use super::*;
246299
use crate::fm::DiagnosisEngineKind;
300+
use crate::fm::ereport::EreportFilters;
247301
use crate::inventory::SpType;
302+
use crate::support_bundle::{
303+
BundleData, BundleDataSelection, SledSelection,
304+
};
248305
use ereport_types::{Ena, EreportId};
249306
use omicron_uuid_kinds::{
250307
AlertUuid, CaseUuid, EreporterRestartUuid, OmicronZoneUuid, SitrepUuid,
308+
SupportBundleUuid,
251309
};
252310
use std::str::FromStr;
253311
use std::sync::Arc;
@@ -276,6 +334,12 @@ mod tests {
276334
let alert2_id =
277335
AlertUuid::from_str("8a6f88ef-c436-44a9-b4cb-cae91d7306c9")
278336
.unwrap();
337+
let bundle1_id =
338+
SupportBundleUuid::from_str("d1a2b3c4-e5f6-7890-abcd-ef1234567890")
339+
.unwrap();
340+
let bundle2_id =
341+
SupportBundleUuid::from_str("a9b8c7d6-e5f4-3210-fedc-ba0987654321")
342+
.unwrap();
279343

280344
// Create some ereports
281345
let mut ereports = IdOrdMap::new();
@@ -349,6 +413,31 @@ mod tests {
349413
})
350414
.unwrap();
351415

416+
let mut bundle1_data = BundleDataSelection::new();
417+
bundle1_data.insert(BundleData::Reconfigurator);
418+
bundle1_data.insert(BundleData::SpDumps);
419+
bundle1_data.insert(BundleData::HostInfo(SledSelection::All));
420+
bundle1_data.insert(BundleData::Ereports(EreportFilters {
421+
only_classes: vec!["hw.pwr.*".to_string()],
422+
..Default::default()
423+
}));
424+
425+
let mut support_bundles_requested = IdOrdMap::new();
426+
support_bundles_requested
427+
.insert_unique(SupportBundleRequest {
428+
id: bundle1_id,
429+
requested_sitrep_id: created_sitrep_id,
430+
data_selection: bundle1_data,
431+
})
432+
.unwrap();
433+
support_bundles_requested
434+
.insert_unique(SupportBundleRequest {
435+
id: bundle2_id,
436+
requested_sitrep_id: closed_sitrep_id,
437+
data_selection: BundleDataSelection::default(),
438+
})
439+
.unwrap();
440+
352441
// Create the case
353442
let case = Case {
354443
id: case_id,
@@ -357,6 +446,7 @@ mod tests {
357446
de: DiagnosisEngineKind::PowerShelf,
358447
ereports,
359448
alerts_requested,
449+
support_bundles_requested,
360450
comment: "Power shelf rectifier added and removed here :-)"
361451
.to_string(),
362452
};

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)