Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions nexus/db-model/src/fm/case.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ impl CaseMetadata {
de,
comment,
alerts_requested: _,
support_bundles_requested: _,
ereports: _,
} = case;
Self {
Expand Down
4 changes: 4 additions & 0 deletions nexus/db-queries/src/db/datastore/fm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ impl DataStore {
comment,
ereports,
alerts_requested,
support_bundles_requested: iddqd::IdOrdMap::new(),
}
}));
}
Expand Down Expand Up @@ -1552,6 +1553,7 @@ mod tests {
de,
ereports,
alerts_requested,
support_bundles_requested: _,
} = case;
let case_id = id;
let Some(expected) = this.cases.get(&case_id) else {
Expand Down Expand Up @@ -1770,6 +1772,7 @@ mod tests {
de: fm::DiagnosisEngineKind::PowerShelf,
ereports,
alerts_requested,
support_bundles_requested: iddqd::IdOrdMap::new(),
comment: "my cool case".to_string(),
}
};
Expand Down Expand Up @@ -1802,6 +1805,7 @@ mod tests {
de: fm::DiagnosisEngineKind::PowerShelf,
ereports,
alerts_requested,
support_bundles_requested: iddqd::IdOrdMap::new(),
comment: "break in case of emergency".to_string(),
}
};
Expand Down
1 change: 1 addition & 0 deletions nexus/fm/src/builder/case.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ impl AllCases {
comment: String::new(),
ereports: Default::default(),
alerts_requested: Default::default(),
support_bundles_requested: Default::default(),
};
entry.insert(CaseBuilder::new(
&self.log, sitrep_id, case, case_rng,
Expand Down
2 changes: 2 additions & 0 deletions nexus/src/app/background/tasks/fm_rendezvous.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ mod tests {
de: fm::DiagnosisEngineKind::PowerShelf,
alerts_requested: iddqd::IdOrdMap::new(),
ereports: iddqd::IdOrdMap::new(),
support_bundles_requested: iddqd::IdOrdMap::new(),
comment: "my great case".to_string(),
};
case1
Expand Down Expand Up @@ -305,6 +306,7 @@ mod tests {
de: fm::DiagnosisEngineKind::PowerShelf,
alerts_requested: iddqd::IdOrdMap::new(),
ereports: iddqd::IdOrdMap::new(),
support_bundles_requested: iddqd::IdOrdMap::new(),
comment: "my other great case".to_string(),
};
case2
Expand Down
92 changes: 91 additions & 1 deletion nexus/types/src/fm/case.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
use crate::alert::AlertClass;
use crate::fm::DiagnosisEngineKind;
use crate::fm::Ereport;
use crate::support_bundle::BundleDataSelection;
use iddqd::{IdOrdItem, IdOrdMap};
use omicron_uuid_kinds::{AlertUuid, CaseEreportUuid, CaseUuid, SitrepUuid};
use omicron_uuid_kinds::{
AlertUuid, CaseEreportUuid, CaseUuid, SitrepUuid, SupportBundleUuid,
};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::sync::Arc;
Expand All @@ -21,6 +24,7 @@ pub struct Case {

pub ereports: IdOrdMap<CaseEreport>,
pub alerts_requested: IdOrdMap<AlertRequest>,
pub support_bundles_requested: IdOrdMap<SupportBundleRequest>,

pub comment: String,
}
Expand Down Expand Up @@ -88,6 +92,27 @@ impl iddqd::IdOrdItem for AlertRequest {
iddqd::id_upcast!();
}

/// A request to create a support bundle, associated with a [`Case`].
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct SupportBundleRequest {
/// Unique identifier for this support bundle request.
pub id: SupportBundleUuid,
/// The sitrep in which this support bundle was requested.
pub requested_sitrep_id: SitrepUuid,
/// Which data to include in the support bundle. Use
/// [`BundleDataSelection::default()`] to request all default data.
pub data_selection: BundleDataSelection,
}
Comment thread
mergeconflict marked this conversation as resolved.

impl iddqd::IdOrdItem for SupportBundleRequest {
type Key<'a> = &'a SupportBundleUuid;
fn key(&self) -> Self::Key<'_> {
&self.id
}

iddqd::id_upcast!();
}

struct DisplayCase<'a> {
case: &'a Case,
indent: usize,
Expand Down Expand Up @@ -120,6 +145,7 @@ impl fmt::Display for DisplayCase<'_> {
ereports,
comment,
alerts_requested,
support_bundles_requested,
},
indent,
sitrep_id,
Expand Down Expand Up @@ -234,6 +260,33 @@ impl fmt::Display for DisplayCase<'_> {
}
}

if !support_bundles_requested.is_empty() {
writeln!(f, "\n{:>indent$}support bundles requested:", "")?;
writeln!(f, "{:>indent$}-------------------------", "")?;

let indent = indent + 2;
for SupportBundleRequest {
id,
requested_sitrep_id,
data_selection,
} in support_bundles_requested.iter()
{
const REQUESTED_IN: &str = "requested in:";
const DATA: &str = "data:";
const WIDTH: usize = const_max_len(&[REQUESTED_IN, DATA]);

writeln!(f, "{BULLET:>indent$}bundle {id}",)?;
writeln!(
f,
"{:>indent$}{REQUESTED_IN:<WIDTH$} {requested_sitrep_id}{}",
"",
this_sitrep(*requested_sitrep_id)
)?;
writeln!(f, "{:>indent$}{DATA}", "")?;
writeln!(f, "{}\n", data_selection.display(indent + 2))?;
}
}

writeln!(f)?;

Ok(())
Expand All @@ -244,10 +297,15 @@ impl fmt::Display for DisplayCase<'_> {
mod tests {
use super::*;
use crate::fm::DiagnosisEngineKind;
use crate::fm::ereport::EreportFilters;
use crate::inventory::SpType;
use crate::support_bundle::{
BundleData, BundleDataSelection, SledSelection,
};
use ereport_types::{Ena, EreportId};
use omicron_uuid_kinds::{
AlertUuid, CaseUuid, EreporterRestartUuid, OmicronZoneUuid, SitrepUuid,
SupportBundleUuid,
};
use std::str::FromStr;
use std::sync::Arc;
Expand Down Expand Up @@ -276,6 +334,12 @@ mod tests {
let alert2_id =
AlertUuid::from_str("8a6f88ef-c436-44a9-b4cb-cae91d7306c9")
.unwrap();
let bundle1_id =
SupportBundleUuid::from_str("d1a2b3c4-e5f6-7890-abcd-ef1234567890")
.unwrap();
let bundle2_id =
SupportBundleUuid::from_str("a9b8c7d6-e5f4-3210-fedc-ba0987654321")
.unwrap();

// Create some ereports
let mut ereports = IdOrdMap::new();
Expand Down Expand Up @@ -349,6 +413,31 @@ mod tests {
})
.unwrap();

let mut bundle1_data = BundleDataSelection::new();
bundle1_data.insert(BundleData::Reconfigurator);
bundle1_data.insert(BundleData::SpDumps);
bundle1_data.insert(BundleData::HostInfo(SledSelection::All));
bundle1_data.insert(BundleData::Ereports(EreportFilters {
only_classes: vec!["hw.pwr.*".to_string()],
..Default::default()
}));

let mut support_bundles_requested = IdOrdMap::new();
support_bundles_requested
.insert_unique(SupportBundleRequest {
id: bundle1_id,
requested_sitrep_id: created_sitrep_id,
data_selection: bundle1_data,
})
.unwrap();
support_bundles_requested
.insert_unique(SupportBundleRequest {
id: bundle2_id,
requested_sitrep_id: closed_sitrep_id,
data_selection: BundleDataSelection::default(),
})
.unwrap();

// Create the case
let case = Case {
id: case_id,
Expand All @@ -357,6 +446,7 @@ mod tests {
de: DiagnosisEngineKind::PowerShelf,
ereports,
alerts_requested,
support_bundles_requested,
comment: "Power shelf rectifier added and removed here :-)"
.to_string(),
};
Expand Down
102 changes: 101 additions & 1 deletion nexus/types/src/fm/ereport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ fn get_sp_metadata_string(
}

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

/// Displayer for pretty-printing [`EreportFilters`].
#[must_use = "this struct does nothing unless displayed"]
pub struct DisplayEreportFilters<'a> {
filters: &'a EreportFilters,
}

impl EreportFilters {
pub fn display(&self) -> DisplayEreportFilters<'_> {
DisplayEreportFilters { filters: self }
}
}

impl fmt::Display for DisplayEreportFilters<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use itertools::Itertools;

let filters = self.filters;

// Writes a semicolon-separated part to the formatter, tracking whether
// we've written anything yet.
let mut empty = true;
let mut fmt_part =
|f: &mut fmt::Formatter, args: fmt::Arguments| -> fmt::Result {
if !empty {
write!(f, "; ")?;
}
empty = false;
f.write_fmt(args)
};

if let Some(start) = &filters.start_time {
fmt_part(f, format_args!("start: {start}"))?;
}
if let Some(end) = &filters.end_time {
fmt_part(f, format_args!("end: {end}"))?;
}
if !filters.only_serials.is_empty() {
fmt_part(
f,
format_args!(
"serials: {}",
filters.only_serials.iter().format(", ")
),
)?;
}
if !filters.only_classes.is_empty() {
fmt_part(
f,
format_args!(
"classes: {}",
filters.only_classes.iter().format(", ")
),
)?;
}

// If no filters are set, display "none" rather than empty output.
if empty {
write!(f, "none")?;
}
Ok(())
}
}

impl EreportFilters {
pub fn check_time_range(&self) -> Result<(), Error> {
if let (Some(start), Some(end)) = (self.start_time, self.end_time) {
Expand All @@ -253,3 +316,40 @@ impl EreportFilters {
Ok(())
}
}

#[cfg(test)]
pub(crate) mod test_utils {
use super::*;
use proptest::prelude::*;

fn arb_datetime() -> impl Strategy<Value = DateTime<Utc>> {
// Generate timestamps in a reasonable range (2020-2030).
(1577836800i64..1893456000i64)
.prop_map(|secs| DateTime::from_timestamp(secs, 0).unwrap())
}

impl Arbitrary for EreportFilters {
Comment thread
mergeconflict marked this conversation as resolved.
type Parameters = ();
type Strategy = BoxedStrategy<Self>;

fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
(
prop::option::of(arb_datetime()),
prop::option::of(arb_datetime()),
prop::collection::vec(".*", 0..=3),
prop::collection::vec(".*", 0..=3),
)
.prop_map(
|(start_time, end_time, only_serials, only_classes)| {
EreportFilters {
start_time,
end_time,
only_serials,
only_classes,
}
},
)
.boxed()
}
}
}
Loading
Loading