Skip to content
4 changes: 2 additions & 2 deletions dev-tools/omdb/tests/successes.out
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,7 @@ task: "fm_analysis"
/!\ analysis failed: FM analysis is not yet implemented

fault management analysis inputs
----- ---------- -------- ------
--------------------------------
parent sitrep: <none>
inventory collection: ..........<REDACTED_UUID>...........
no new ereports since the parent sitrep
Expand Down Expand Up @@ -1378,7 +1378,7 @@ task: "fm_analysis"
/!\ analysis failed: FM analysis is not yet implemented

fault management analysis inputs
----- ---------- -------- ------
--------------------------------
parent sitrep: <none>
inventory collection: ..........<REDACTED_UUID>...........
no new ereports since the parent sitrep
Expand Down
151 changes: 120 additions & 31 deletions nexus/fm/src/analysis_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,9 @@ mod tests {
let mut reporter = fm_test
.reporters
.reporter(Reporter::Sp { sp_type: SpType::Sled, slot: 0 });
let ereport_in_open_case =
let ereport_in_open_case1 =
Arc::new(reporter.mk_ereport(now, Default::default()));
let ereport_in_open_case2 =
Arc::new(reporter.mk_ereport(now, Default::default()));
let ereport_in_closed_unmarked =
Arc::new(reporter.mk_ereport(now, Default::default()));
Expand All @@ -273,28 +275,30 @@ mod tests {
let ereport_new =
Arc::new(reporter.mk_ereport(now, Default::default()));

// Make a parent sitrep, with three cases:
// Make a parent sitrep, with four cases:
//
// 1. an open case
let open_case_id = CaseUuid::new_v4();
// 2. a closed case with an unmarked ereport in it,
let open_case1_id = CaseUuid::new_v4();
// 2. another open case
let open_case2_id = CaseUuid::new_v4();
// 3. a closed case with an unmarked ereport in it,
let closed_case_with_unmarked_id = CaseUuid::new_v4();
// 3. a closed case with only marked ereports.
// 4. a closed case with only marked ereports.
let closed_case_without_unmarked_id = CaseUuid::new_v4();
let parent_sitrep_id = SitrepUuid::new_v4();
let parent_sitrep = {
let open_case = {
let open_case1 = {
let created_sitrep_id = parent_sitrep_id;
fm::Case {
id: open_case_id,
id: open_case1_id,
metadata: fm::case::Metadata {
created_sitrep_id,
closed_sitrep_id: None,
de: DiagnosisEngineKind::PowerShelf,
comment: "open case".to_string(),
comment: "open case one".to_string(),
},
ereports: [
case_ereport(&ereport_in_open_case, parent_sitrep_id),
case_ereport(&ereport_in_open_case1, parent_sitrep_id),
case_ereport(
&ereport_in_closed_unmarked,
created_sitrep_id,
Expand All @@ -306,6 +310,26 @@ mod tests {
support_bundles_requested: Default::default(),
}
};
let open_case2 = {
let created_sitrep_id = parent_sitrep_id;
fm::Case {
id: open_case2_id,
metadata: fm::case::Metadata {
created_sitrep_id,
closed_sitrep_id: None,
de: DiagnosisEngineKind::PowerShelf,
comment: "open case two".to_string(),
},
ereports: [case_ereport(
&ereport_in_open_case2,
parent_sitrep_id,
)]
.into_iter()
.collect(),
alerts_requested: Default::default(),
support_bundles_requested: Default::default(),
}
};
let closed_case_with_unmarked = {
let created_sitrep_id = SitrepUuid::new_v4();
fm::Case {
Expand Down Expand Up @@ -356,7 +380,8 @@ mod tests {
};

let cases = [
open_case,
open_case1,
open_case2,
closed_case_with_unmarked,
closed_case_without_unmarked,
]
Expand All @@ -367,7 +392,8 @@ mod tests {
// sitrep — add_unmarked_ereports uses this map to detect which ereports
// have already appeared in the parent sitrep.
let ereports_by_id = [
ereport_in_open_case.clone(),
ereport_in_open_case1.clone(),
ereport_in_open_case2.clone(),
ereport_in_closed_unmarked.clone(),
ereport_in_closed_marked.clone(),
]
Expand Down Expand Up @@ -399,21 +425,22 @@ mod tests {
// Build analysis input
let (input, report) = {
let mut builder = Input::builder(Some(parent_sitrep), inv);
// Pass in three ereports:
// - one that is in the open case of the parent sitrep
// Pass in four ereports:
// - two that are in the open cases of the parent sitrep
// - one that is in the (to-be-copied-forward) closed case
// - one that is brand-new
//
// Notably, `ereport_in_closed_marked` is NOT passed here,
// simulating that it was already marked seen in the database.
builder.add_unmarked_ereports([
(*ereport_in_open_case).clone(),
(*ereport_in_open_case1).clone(),
(*ereport_in_open_case2).clone(),
(*ereport_in_closed_unmarked).clone(),
(*ereport_new).clone(),
]);
builder.build()
};
dbg!(report);
eprintln!("{}", report.display_multiline(0));

// Check the "new ereports" in the constructed input.
assert!(
Expand All @@ -422,8 +449,13 @@ mod tests {
sitrep)"
);
assert!(
!input.new_ereports().contains_key(ereport_in_open_case.id()),
"ereport_in_open_case should NOT be in new_ereports (it is \
!input.new_ereports().contains_key(ereport_in_open_case1.id()),
"ereport_in_open_case1 should NOT be in new_ereports (it is \
already associated with an open case in the parent sitrep)"
);
assert!(
!input.new_ereports().contains_key(ereport_in_open_case2.id()),
"ereport_in_open_case2 should NOT be in new_ereports (it is \
already associated with an open case in the parent sitrep)"
);
assert!(
Expand Down Expand Up @@ -462,20 +494,28 @@ mod tests {

// Check the contents of open cases.
assert!(
input.cases().contains_key(&open_case_id),
"the open case from the parent sitrep should be in input.cases()"
input.cases().contains_key(&open_case1_id),
"open case 1 from the parent sitrep should be in input.cases()"
);
assert_eq!(input.cases().len(), 1, "exactly one case should be open");
assert!(
input.cases().contains_key(&open_case2_id),
"open case 2 from the parent sitrep should be in input.cases()"
);
assert_eq!(input.cases().len(), 2, "exactly two cases should be open");

// Start building a sitrep...
let mut sitrep_builder =
SitrepBuilder::new_with_rng(log, &input, fm_test.sitrep_rng);

// The open case from the parent sitrep must be accessible via
// case_mut() so that the diagnosis engine can update it.
// The open cases from the parent sitrep must be accessible via
// case_mut() so that the diagnosis engines can update it.
assert!(
sitrep_builder.cases.case_mut(&open_case1_id).is_some(),
"open case 1 should be accessible via case_mut()"
);
assert!(
sitrep_builder.cases.case_mut(&open_case_id).is_some(),
"the open case should be accessible via case_mut()"
sitrep_builder.cases.case_mut(&open_case2_id).is_some(),
"open case 2 should be accessible via case_mut()"
);
assert!(
sitrep_builder
Expand All @@ -493,16 +533,64 @@ mod tests {
"the closed_case_without_unmarked should NOT be accessible via \
case_mut() (closed cases are not open for modification)"
);
sitrep_builder.comment_mut().push_str("my cool sitrep");

let new_case_id = {
let mut new_case =
sitrep_builder.cases.open_case(DiagnosisEngineKind::PowerShelf);
new_case.add_ereport(
&ereport_new,
"this ereport is important to the case somehow",
);
new_case
.request_alert(
nexus_types::alert::AlertClass::TestFooBar,
&serde_json::json!({"alert": true}),
"requesting an alert to tell someone about something",
)
.unwrap();
*new_case.id()
};

// Close open case 2
sitrep_builder
.cases
.case_mut(&open_case2_id)
.unwrap()
.close("i'm closing it because i want to");

// Build the final sitrep
let output_sitrep = dbg!(
sitrep_builder.build(OmicronZoneUuid::new_v4(), chrono::Utc::now())
let (output_sitrep, report) =
sitrep_builder.build(OmicronZoneUuid::new_v4(), chrono::Utc::now());
eprintln!("{}", report.display_multiline(0));

let open_case1 = output_sitrep
.cases
.get(&open_case1_id)
.expect("open case 1 should be in the output sitrep's cases");
assert!(
open_case1.is_open(),
"open case 1 should be open in the output sitrep"
);

let open_case2 = output_sitrep
.cases
.get(&open_case2_id)
.expect("open case 2 should be in the output sitrep's cases");
assert!(
output_sitrep.cases.contains_key(&open_case_id),
"open case should be in the output sitrep's cases"
!open_case2.is_open(),
"open case 2 should be closed in the output sitrep"
);

let new_case = output_sitrep
.cases
.get(&new_case_id)
.expect("new case should be in the output sitrep's cases");
assert!(
new_case.is_open(),
"new case should be open in the output sitrep"
);

assert!(
output_sitrep.cases.contains_key(&closed_case_with_unmarked_id),
"closed cases with unmarked ereports should be copied forward \
Expand All @@ -515,9 +603,10 @@ mod tests {
);
assert_eq!(
output_sitrep.cases.len(),
2,
"the output sitrep should have exactly 2 cases: the open case and \
the closed-but-copied-forward case"
4,
"the output sitrep should have exactly 4 cases: the open case, \
the newly-closed case, the closed-but-copied-forward case, and \
the new case"
);

logctx.cleanup_successful();
Expand Down
31 changes: 26 additions & 5 deletions nexus/fm/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,28 +75,48 @@ impl<'a> SitrepBuilder<'a> {
&self.comment
}

pub fn comment_mut(&mut self) -> &mut str {
pub fn comment_mut(&mut self) -> &mut String {
&mut self.comment
}

pub fn build(
self,
creator_id: OmicronZoneUuid,
time_created: chrono::DateTime<chrono::Utc>,
) -> fm::Sitrep {
) -> (fm::Sitrep, fm::analysis_reports::AnalysisReport) {
let mut ereports_by_id = iddqd::IdOrdMap::new();
let mut report_cases = IdOrdMap::new();
let cases = self
.cases
.cases
.into_iter()
.map(fm::Case::from)
// Note that entries are only pushed to `report_cases` for open
// cases, as the closed cases which are just being copied forward
// into the next sitrep have, by construction, not been changed in
// this builder, since they weren't exposed for modification by the
// builder API. Thus, we really don't have anything new to say about
// them that's worth including in the report, as the fact that they
// were copied forward will be recorded in the input report.
.map(|case_builder| {
let (case, report) = case_builder.build();
report_cases.insert_unique(report).expect(
"we are iterating over an IdOrdMap, so the entries \
should already be unique",
);
case
})
.chain(self.closed_cases_copied_forward.iter().cloned())
Comment thread
hawkw marked this conversation as resolved.
.inspect(|case| {
ereports_by_id
.extend(case.ereports.iter().map(|ce| ce.ereport.clone()));
})
.collect();
fm::Sitrep {
let report = fm::analysis_reports::AnalysisReport {
sitrep_id: self.sitrep_id,
comment: self.comment.clone(),
cases: report_cases,
};
let sitrep = fm::Sitrep {
metadata: fm::SitrepMetadata {
id: self.sitrep_id,
parent_sitrep_id: self.parent_sitrep.map(|s| s.metadata.id),
Expand All @@ -107,6 +127,7 @@ impl<'a> SitrepBuilder<'a> {
},
cases,
ereports_by_id,
}
};
(sitrep, report)
}
}
Loading
Loading