diff --git a/Cargo.lock b/Cargo.lock index f266bf4172e..26127542bf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7175,6 +7175,7 @@ dependencies = [ "serde_json", "slog", "slog-error-chain", + "strum 0.27.2", "thiserror 2.0.18", "typed-rng", ] diff --git a/nexus/db-model/src/alert_class.rs b/nexus/db-model/src/alert_class.rs index 416b5714137..5a832c4441c 100644 --- a/nexus/db-model/src/alert_class.rs +++ b/nexus/db-model/src/alert_class.rs @@ -29,6 +29,8 @@ impl_enum_type!( TestFooBaz => b"test.foo.baz" TestQuuxBar => b"test.quux.bar" TestQuuxBarBaz => b"test.quux.bar.baz" + PsuInserted => b"hardware.power_shelf.psu.insert" + PsuRemoved => b"hardware.power_shelf.psu.remove" ); impl AlertClass { @@ -62,6 +64,8 @@ impl From for AlertClass { In::TestFooBaz => Self::TestFooBaz, In::TestQuuxBar => Self::TestQuuxBar, In::TestQuuxBarBaz => Self::TestQuuxBarBaz, + In::PsuInserted => Self::PsuInserted, + In::PsuRemoved => Self::PsuRemoved, } } } @@ -75,6 +79,8 @@ impl From for nexus_types::alert::AlertClass { AlertClass::TestFooBaz => Self::TestFooBaz, AlertClass::TestQuuxBar => Self::TestQuuxBar, AlertClass::TestQuuxBarBaz => Self::TestQuuxBarBaz, + AlertClass::PsuInserted => Self::PsuInserted, + AlertClass::PsuRemoved => Self::PsuRemoved, } } } diff --git a/nexus/db-model/src/schema_versions.rs b/nexus/db-model/src/schema_versions.rs index 9e54b5d1957..71dea74d314 100644 --- a/nexus/db-model/src/schema_versions.rs +++ b/nexus/db-model/src/schema_versions.rs @@ -16,7 +16,7 @@ use std::{collections::BTreeMap, sync::LazyLock}; /// /// This must be updated when you change the database schema. Refer to /// schema/crdb/README.adoc in the root of this repository for details. -pub const SCHEMA_VERSION: Version = Version::new(272, 0, 0); +pub const SCHEMA_VERSION: Version = Version::new(273, 0, 0); /// List of all past database schema versions, in *reverse* order /// @@ -28,6 +28,7 @@ pub static KNOWN_VERSIONS: LazyLock> = LazyLock::new(|| { // | leaving the first copy as an example for the next person. // v // KnownVersion::new(next_int, "unique-dirname-with-the-sql-files"), + KnownVersion::new(273, "psu-presence-alert-classes"), KnownVersion::new(272, "ereporter-restart-order-v2"), KnownVersion::new(271, "inv-fmd"), KnownVersion::new(270, "fm-alert-resource-deletion"), diff --git a/nexus/fm/Cargo.toml b/nexus/fm/Cargo.toml index 0defe566368..9147d16c88a 100644 --- a/nexus/fm/Cargo.toml +++ b/nexus/fm/Cargo.toml @@ -29,6 +29,7 @@ pq-sys = "*" rand.workspace = true serde.workspace = true serde_json.workspace = true +strum.workspace = true slog.workspace = true slog-error-chain.workspace = true thiserror.workspace = true diff --git a/nexus/fm/src/analysis_input.rs b/nexus/fm/src/analysis_input.rs index 8db94e48030..59ae50d6a7f 100644 --- a/nexus/fm/src/analysis_input.rs +++ b/nexus/fm/src/analysis_input.rs @@ -41,7 +41,7 @@ pub struct Input { inv: Arc, /// Ereports which are new and should be input to analysis in the next /// sitrep. - new_ereports: IdOrdMap, + new_ereports: IdOrdMap>, open_cases: IdOrdMap, closed_cases_copied_forward: IdOrdMap, ereporter_restarts: IdOrdMap, @@ -63,7 +63,7 @@ impl Input { &self.inv } - pub fn new_ereports(&self) -> &IdOrdMap { + pub fn new_ereports(&self) -> &IdOrdMap> { &self.new_ereports } @@ -151,7 +151,7 @@ pub struct Builder { in_service_disks: Arc>, /// Ereports which are new and should be input to analysis in the next /// sitrep. - new_ereports: IdOrdMap, + new_ereports: IdOrdMap>, /// The IDs of any ereports which have been included in the parent sitrep, /// but which have *not* yet been marked as seen in the database. @@ -196,7 +196,7 @@ impl Builder { } } - Some(ereport) + Some(Arc::new(ereport)) })) } diff --git a/nexus/fm/src/diagnosis/mod.rs b/nexus/fm/src/diagnosis/mod.rs index 30e210779e3..a38954c4de7 100644 --- a/nexus/fm/src/diagnosis/mod.rs +++ b/nexus/fm/src/diagnosis/mod.rs @@ -7,12 +7,12 @@ //! Each submodule defines one diagnosis engine (DE). `analyze` dispatches to //! each engine in turn; engines are deterministic and idempotent per RFD 603. -use crate::SitrepBuilder; - mod physical_disk; +mod power_shelf; pub fn analyze(builder: &mut SitrepBuilder<'_>) -> anyhow::Result<()> { physical_disk::analyze(builder)?; + power_shelf::analyze(builder)?; Ok(()) } @@ -33,5 +33,5 @@ pub fn analyze(builder: &mut SitrepBuilder<'_>) -> anyhow::Result<()> { /// (`class LIKE 'ereport.cpu.amd.%'`) or a `known_ereport_class` lookup /// table joined into the query. Revisit this if the list grows that large. pub fn known_ereport_classes() -> &'static [&'static str] { - &[] + &[power_shelf::PSU_INSERT_EREPORT, power_shelf::PSU_REMOVE_EREPORT] } diff --git a/nexus/fm/src/diagnosis/power_shelf.rs b/nexus/fm/src/diagnosis/power_shelf.rs new file mode 100644 index 00000000000..710b55830c1 --- /dev/null +++ b/nexus/fm/src/diagnosis/power_shelf.rs @@ -0,0 +1,576 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use crate::SitrepBuilder; +use crate::analysis_input::Input; +use crate::ereport; +use crate::ereport::Ereport; +use anyhow::Context; +use iddqd::{IdHashItem, IdHashMap, IdOrdItem, IdOrdMap, id_upcast}; +use nexus_types::fm::DiagnosisEngineKind; +use nexus_types::inventory; +use omicron_uuid_kinds::CaseUuid; +use omicron_uuid_kinds::EreporterRestartUuid; +use serde::Deserialize; +use slog_error_chain::InlineErrorChain; +use std::collections::BTreeMap; +use std::collections::HashSet; +use std::fmt; +use std::sync::Arc; +use strum::VariantArray; + +pub const PSU_REMOVE_EREPORT: &str = "hw.remove.psu"; +pub const PSU_INSERT_EREPORT: &str = "hw.insert.psu"; + +const KNOWN_EREPORTS: &[&str] = &[PSU_INSERT_EREPORT, PSU_REMOVE_EREPORT]; + +pub fn analyze(builder: &mut SitrepBuilder<'_>) -> anyhow::Result<()> { + let input = builder.input(); + let log = builder.log.new(slog::o!("de" => "power_shelf")); + // Okay so basically, here's what we do: + // 1. index existing cases + // 2. look at ereports and open/close/assign to case + // + // There's two kinds of cases, which are: + // - has an ereport which indicates the rectifier was removed, + // - has only a rectifier inserted ereport + let parent_cases = input + .open_cases() + .iter() + .filter(|c| c.metadata.de == DiagnosisEngineKind::PowerShelf); + + let mut cases_by_id = IdOrdMap::new(); + let mut cases_by_psu = BTreeMap::new(); + 'cases: for case in parent_cases { + // Reconstruct the case by looking at its ereports: + // - the ereports should all be associated with a single PSC at this + // point. + // - put them in a map by PSC location + let mut psc_case = cases_by_id + .entry(&case.id) + .or_insert_with(|| PscCase::new(case.id)); + for case_ereport in case.ereports.iter() { + let ereport = &case_ereport.ereport; + let ereport = match PsuEreport::parse(&ereport) { + Ok(ereport) => ereport, + Err(e) => { + // This is weird: a case in the parent sitrep created by + // this DE contained an ereport that we couldn't understand. + // Close the case since we don't know what to do with it. + let err = InlineErrorChain::new(&*e); + slog::warn!( + &log, + "couldn't interpret ereport assigned to a case in the \ + parent sitrep!"; + "case_id" => %case.id, + "ereport_id" => %ereport.id(), + "case_ereport_id" => %case_ereport.id, + "error" => &err, + ); + let comment = format!( + "I couldn't understand this case, as it \ + contained an incomprehensible ereport {} \ + (case ereport {}). The ereport could not be \ + interpreted because: {err}", + ereport.id(), + case_ereport.id, + ); + builder + .cases + .case_mut(&case.id) + .expect("open case in parent sitrep should be present") + .close(comment); + drop(psc_case); + cases_by_id.remove(&case.id); + continue 'cases; + } + }; + psc_case.insert_ereport(ereport).expect( + "ereport can't possibly be a duplicate because it came \ + from the case's existing ereport set", + ); + } + + // Now, add the case to the index of cases by PSU. + for location in psc_case.impacted_psus() { + // N.B. that this allows us to model multiple cases impacting the + // same PSU. At present we will not do this, but it will become + // important later. + cases_by_psu + .entry(location) + .or_insert_with(HashSet::new) + .insert(case.id); + } + } + + // For each ereport that we haven't already seen before: + for ereport in input.new_ereports().iter() { + let Some(class) = ereport.class.as_deref() else { + // if there's no class, nothing we can do with this. + continue; + }; + if !KNOWN_EREPORTS.contains(&class) { + // if it's not something we know what to do with, skip it. + continue; + } + + let psu_ereport = match PsuEreport::parse(&ereport) { + Ok(psu_ereport) => psu_ereport, + Err(e) => { + slog::warn!( + &log, + "skipping a new ereport that isn't an interpretable PSU \ + insert/remove event"; + "ereport_id" => %ereport.id(), + "ereport_class" => ?ereport.class, + "error" => InlineErrorChain::new(&*e), + ); + continue; + } + }; + let location = psu_ereport.location; + let verbed = match psu_ereport.kind { + PsuEreportKind::Insert => "inserted", + PsuEreportKind::Remove => "removed", + }; + // See if there is an open case for the PSC and PSU slot named in the + // ereport. `cases_by_psu` models multiple cases per PSU for the future, + // but at present a PSU is impacted by at most one case, so we assign to + // whichever case the index already knows about. + let case_id = cases_by_psu + .get(&location) + .and_then(|case_ids| case_ids.iter().copied().next()); + let mut case_builder = match case_id { + Some(id) => builder.cases.case_mut(&id).expect( + "an open case from the parent sitrep should be in the builder", + ), + None => { + let mut c = + builder.cases.open_case(DiagnosisEngineKind::PowerShelf); + *c.comment_mut() = format!( + "opened because {location} was {verbed} (in ereport {})", + ereport.id + ); + cases_by_psu + .entry(location) + .or_insert_with(HashSet::new) + .insert(c.id); + c + } + }; + + // that location sure got verbed! + case_builder.add_ereport(&ereport, format!("{location} {verbed}")); + cases_by_id + .get_mut(&case_builder.id) + .expect("the case must be present in the by-id index") + .insert_ereport(psu_ereport) + .expect("distinct new ereports have distinct IDs"); + } + + // for each case: + // - looking at the sequence of ereports based on their ENAs and timestamps, + // determine if the PSU is present or not. + // - if the PSU is present (i.e., the most recent ereport is an insert), + // close the case + // - if the PSU is not present (i.e., the most recent ereport is a_ + // remove), leave the case open + // + // - generate an alert for each PSU insert/remove ereport in the case that + // does not already have an alert for that event. + // + // We don't actually *need* to leave the case open, and *could* get away + // with just making a case for every ereport and requesting an alert, but + // doing it like this sets us up for being able to produce a "rectifier + // missing" active problem for the open cases later. + Ok(()) +} + +#[derive( + Copy, + Clone, + Eq, + PartialEq, + Ord, + PartialOrd, + Hash, + Debug, + strum::VariantArray, + strum::FromRepr, +)] +#[repr(u8)] +enum PsuSlot { + Psu0 = 0, + Psu1 = 1, + Psu2 = 2, + Psu3 = 3, + Psu4 = 4, + Psu5 = 5, +} + +impl fmt::Display for PsuSlot { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&(*self as u8), f) + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Hash, Default)] +struct PsuSet(u8); + +impl PsuSet { + fn contains(&self, slot: PsuSlot) -> bool { + (self.0 & PsuSet::bit(slot)) != 0 + } + + fn insert(&mut self, slot: PsuSlot) { + self.0 |= PsuSet::bit(slot); + } + + fn remove(&mut self, slot: PsuSlot) { + self.0 &= !PsuSet::bit(slot); + } + + fn bit(slot: PsuSlot) -> u8 { + 1 << slot as u8 + } + + fn iter(&self) -> impl Iterator + '_ { + PsuSlot::VARIANTS.iter().copied().filter(|slot| self.contains(*slot)) + } +} +impl fmt::Debug for PsuSet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_set().entries(self.iter()).finish() + } +} + +#[derive(Debug)] +struct PscCase { + case_id: CaseUuid, + impacted: [PsuSet; 2], + restarts: IdHashMap, +} + +impl IdOrdItem for PscCase { + type Key<'a> = &'a CaseUuid; + + fn key(&self) -> Self::Key<'_> { + &self.case_id + } + + id_upcast!(); +} + +impl PscCase { + fn new(case_id: CaseUuid) -> Self { + Self { + case_id, + impacted: [PsuSet::default(), PsuSet::default()], + restarts: IdHashMap::default(), + } + } + + fn insert_ereport(&mut self, ereport: PsuEreport) -> anyhow::Result<()> { + let ereport_id = ereport.ereport.id; + let restart_id = ereport_id.restart_id; + let location = ereport.location; + self.restarts + .entry(&restart_id) + .or_insert_with(|| Restart { + restart_id, + ereports: IdOrdMap::default(), + }) + .ereports + .insert_unique(ereport) + .map_err(|e| { + anyhow::anyhow!( + "an ereport with id {ereport_id} already exists: {e}" + ) + })?; + self.impacted[location.shelf as usize].insert(location.slot); + Ok(()) + } + + /// Iterates over the `PsuLocation` of every PSU impacted by this case. + fn impacted_psus(&self) -> impl Iterator + '_ { + self.impacted.iter().enumerate().flat_map(|(shelf, slots)| { + let shelf = shelf as u8; + slots.iter().map(move |slot| PsuLocation { shelf, slot }) + }) + } +} + +#[derive(Debug)] +struct Restart { + restart_id: EreporterRestartUuid, + ereports: IdOrdMap, +} + +impl IdHashItem for Restart { + type Key<'a> = &'a EreporterRestartUuid; + + fn key(&self) -> Self::Key<'_> { + &self.restart_id + } + + id_upcast!(); +} + +#[derive(Debug)] +struct PsuEreport { + location: PsuLocation, + ereport: Arc, + kind: PsuEreportKind, + data: PsuEreportData, +} + +impl IdOrdItem for PsuEreport { + type Key<'a> = &'a ereport::Ena; + + fn key(&self) -> Self::Key<'_> { + &self.ereport.id.ena + } + + id_upcast!(); +} + +impl PsuEreport { + fn parse(ereport: &Arc) -> anyhow::Result { + let kind = match ereport.data.class.as_deref() { + Some(k) if k == PSU_INSERT_EREPORT => PsuEreportKind::Insert, + Some(k) if k == PSU_REMOVE_EREPORT => PsuEreportKind::Remove, + k => anyhow::bail!("unknown ereport class: {k:?}"), + }; + let shelf = match ereport.reporter { + ereport::Reporter::Sp { + sp_type: inventory::SpType::Power, + slot, + } => u8::try_from(slot).with_context(|| { + format!("power shelf slot number {slot} is way too big") + })?, + reporter => anyhow::bail!( + "invalid reporter type for what seems to be a PSC ereport: \ + {reporter:?}" + ), + }; + let data: PsuEreportData = + serde_json::from_value(ereport.data.report.clone()) + .context("invalid data for a PSC ereport")?; + let slot = PsuSlot::from_repr(data.slot).ok_or_else(|| { + anyhow::anyhow!("PSU slot {} out of range (must be 0-5)", data.slot) + })?; + let location = PsuLocation { shelf, slot }; + Ok(Self { kind, data, location, ereport: ereport.clone() }) + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +enum PsuEreportKind { + Insert, + Remove, +} + +#[derive(Debug, Eq, PartialEq, serde::Deserialize)] +struct PsuEreportData { + fruid: Option, + rail: String, + slot: u8, + refdes: String, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +struct PsuLocation { + shelf: u8, + slot: PsuSlot, +} + +impl fmt::Display for PsuLocation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { shelf, slot } = *self; + write!(f, "power shelf {shelf}, PSU {slot}") + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +struct PsuFruid { + fw_rev: String, + mfr: String, + mpn: String, + serial: String, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_util::FmTest; + use chrono::Utc; + use nexus_types::inventory::SpType; + + // These are real life ereports I copied from the dogfood rack. + mod ereports { + use super::*; + + pub(super) const PSU_REMOVE_JSON: &str = r#"{ + "baseboard_part_number": "913-0000003", + "baseboard_rev": 8, + "baseboard_serial_number": "BRM45220004", + "ereport_message_version": 0, + "fruid": { + "fw_rev": "0701", + "mfr": "Murata-PS", + "mpn": "MWOCP68-3600-D-RM", + "serial": "LL2216RB003Z" + }, + "hubris_archive_id": "qSm4IUtvQe0", + "hubris_task_gen": 0, + "hubris_task_name": "sequencer", + "hubris_uptime_ms": 1197337481, + "k": "hw.remove.psu", + "rail": "V54_PSU4", + "refdes": "PSU4", + "slot": 4, + "v": 0 + }"#; + + pub(super) const PSU_INSERT_JSON: &str = r#"{ + "baseboard_part_number": "913-0000003", + "baseboard_rev": 8, + "baseboard_serial_number": "BRM45220004", + "ereport_message_version": 0, + "fruid": { + "fw_rev": "0701", + "mfr": "Murata-PS", + "mpn": "MWOCP68-3600-D-RM", + "serial": "LL2216RB003Z" + }, + "hubris_archive_id": "qSm4IUtvQe0", + "hubris_task_gen": 0, + "hubris_task_name": "sequencer", + "hubris_uptime_ms": 1197337481, + "k": "hw.insert.psu", + "rail": "V54_PSU4", + "refdes": "PSU4", + "slot": 4, + "v": 0 + }"#; + + pub(super) const PSU_PWR_BAD_JSON: &str = r#"{ + "baseboard_part_number": "913-0000003", + "baseboard_rev": 8, + "baseboard_serial_number": "BRM45220004", + "ereport_message_version": 0, + "fruid": { + "fw_rev": "0701", + "mfr": "Murata-PS", + "mpn": "MWOCP68-3600-D-RM", + "serial": "LL2216RB003Z" + }, + "hubris_archive_id": "qSm4IUtvQe0", + "hubris_task_gen": 0, + "hubris_task_name": "sequencer", + "hubris_uptime_ms": 1197408566, + "k": "hw.pwr.pwr_good.bad", + "pmbus_status": { + "cml": 0, + "input": 48, + "iout": 0, + "mfr": 0, + "temp": 0, + "vout": 0, + "word": 10312 + }, + "rail": "V54_PSU4", + "refdes": "PSU4", + "slot": 4, + "v": 0 + }"#; + + pub(super) const PSU_PWR_GOOD_JSON: &str = r#"{ + "baseboard_part_number": "913-0000003", + "baseboard_rev": 8, + "baseboard_serial_number": "BRM45220004", + "ereport_message_version": 0, + "fruid": { + "fw_rev": "0701", + "mfr": "Murata-PS", + "mpn": "MWOCP68-3600-D-RM", + "serial": "LL2216RB003Z" + }, + "hubris_archive_id": "qSm4IUtvQe0", + "hubris_task_gen": 0, + "hubris_task_name": "sequencer", + "hubris_uptime_ms": 1197408580, + "k": "hw.pwr.pwr_good.good", + "pmbus_status": { + "cml": 0, + "input": 0, + "iout": 0, + "mfr": 0, + "temp": 0, + "vout": 0, + "word": 0 + }, + "rail": "V54_PSU4", + "refdes": "PSU4", + "slot": 4, + "v": 0 + }"#; + } + + fn test_dogfood_ereport_parses( + test_name: &str, + expected_class: PsuEreportKind, + json: &str, + ) { + const SHELF: u16 = 0; + let (mut fmtest, logctx) = FmTest::new_with_logctx(test_name); + let mut reporter = fmtest.reporters.reporter(ereport::Reporter::Sp { + sp_type: SpType::Power, + slot: SHELF, + }); + let ereport = dbg!(Arc::new(reporter.parse_ereport(Utc::now(), json))); + let parsed = dbg!(PsuEreport::parse(&ereport)) + .expect("dogfood ereport should parse as a PsuEreport"); + + // The payload fields shared by every dogfood ereport above; all of them + // describe PSU4 on rail V54_PSU4. + let expected_data = PsuEreportData { + fruid: Some(PsuFruid { + fw_rev: "0701".to_string(), + mfr: "Murata-PS".to_string(), + mpn: "MWOCP68-3600-D-RM".to_string(), + serial: "LL2216RB003Z".to_string(), + }), + rail: "V54_PSU4".to_string(), + slot: 4, + refdes: "PSU4".to_string(), + }; + + assert_eq!(parsed.kind, expected_class); + assert_eq!( + parsed.location, + PsuLocation { shelf: SHELF as u8, slot: PsuSlot::Psu4 } + ); + assert_eq!(parsed.data, expected_data); + logctx.cleanup_successful(); + } + + #[test] + fn test_psu_remove_json_parses() { + test_dogfood_ereport_parses( + "test_psu_remove_json_parses", + PsuEreportKind::Remove, + ereports::PSU_REMOVE_JSON, + ); + } + + #[test] + fn test_psu_insert_json_parses() { + test_dogfood_ereport_parses( + "test_psu_insert_json_parses", + PsuEreportKind::Insert, + ereports::PSU_INSERT_JSON, + ); + } +} diff --git a/nexus/types/src/alert.rs b/nexus/types/src/alert.rs index 99c9af2b38e..2ad9184e49e 100644 --- a/nexus/types/src/alert.rs +++ b/nexus/types/src/alert.rs @@ -8,6 +8,8 @@ use schemars::JsonSchema; use serde::Serialize; use std::fmt; +pub mod power_shelf; + /// Trait implemented by alerts. pub trait AlertPayload: Serialize + JsonSchema + std::fmt::Debug { const CLASS: AlertClass; @@ -72,6 +74,10 @@ pub enum AlertClass { TestQuuxBar, #[strum(serialize = "test.quux.bar.baz")] TestQuuxBarBaz, + #[strum(serialize = "hardware.power_shelf.psu.insert")] + PsuInserted, + #[strum(serialize = "hardware.power_shelf.psu.remove")] + PsuRemoved, } impl AlertClass { @@ -109,6 +115,12 @@ impl AlertClass { | Self::TestQuuxBarBaz => { "This is a test of the emergency alert system" } + Self::PsuInserted => { + "A power supply unit (PSU) has been inserted into a power shelf" + } + Self::PsuRemoved => { + "A power supply unit (PSU) has been removed from a power shelf" + } } } diff --git a/nexus/types/src/alert/power_shelf.rs b/nexus/types/src/alert/power_shelf.rs new file mode 100644 index 00000000000..fc949af5663 --- /dev/null +++ b/nexus/types/src/alert/power_shelf.rs @@ -0,0 +1,60 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Power shelf alert types. + +use super::*; +use chrono::DateTime; +use chrono::Utc; + +#[derive(Debug, Clone, Serialize, JsonSchema)] +pub struct PsuInsertedV0 { + pub power_shelf: PowerShelf, + pub psu: Psu, + pub time: DateTime, +} + +impl AlertPayload for PsuInsertedV0 { + const CLASS: AlertClass = AlertClass::PsuInserted; + const VERSION: u32 = 0; +} + +#[derive(Debug, Clone, Serialize, JsonSchema)] +pub struct PsuRemovedV0 { + pub power_shelf: PowerShelf, + pub psu: Psu, + pub time: DateTime, +} + +impl AlertPayload for PsuRemovedV0 { + const CLASS: AlertClass = AlertClass::PsuRemoved; + const VERSION: u32 = 0; +} + +#[derive(Debug, Clone, Serialize, JsonSchema)] +pub struct PowerShelf { + pub slot: u8, + pub identity: Option, +} + +#[derive(Debug, Clone, Serialize, JsonSchema)] +pub struct Psu { + pub slot: u8, + pub identity: Option, +} + +#[derive(Debug, Clone, Serialize, JsonSchema)] +pub struct PsuIdentity { + pub manufacturer: Option, + pub part_number: Option, + pub firmware_revision: Option, + pub serial_number: Option, +} + +#[derive(Debug, Clone, Serialize, JsonSchema)] +pub struct OxideVpdIdentity { + pub part_number: String, + pub revision: u32, + pub serial_number: String, +} diff --git a/schema/crdb/dbinit.sql b/schema/crdb/dbinit.sql index 44380352dc1..2e640d711a9 100644 --- a/schema/crdb/dbinit.sql +++ b/schema/crdb/dbinit.sql @@ -7131,7 +7131,9 @@ CREATE TYPE IF NOT EXISTS omicron.public.alert_class AS ENUM ( 'test.foo.bar', 'test.foo.baz', 'test.quux.bar', - 'test.quux.bar.baz' + 'test.quux.bar.baz', + 'hardware.power_shelf.psu.insert', + 'hardware.power_shelf.psu.remove' -- Add new alert classes here! ); @@ -8929,7 +8931,7 @@ INSERT INTO omicron.public.db_metadata ( version, target_version ) VALUES - (TRUE, NOW(), NOW(), '272.0.0', NULL) + (TRUE, NOW(), NOW(), '273.0.0', NULL) ON CONFLICT DO NOTHING; COMMIT; diff --git a/schema/crdb/psu-presence-alert-classes/up01.sql b/schema/crdb/psu-presence-alert-classes/up01.sql new file mode 100644 index 00000000000..920f613d157 --- /dev/null +++ b/schema/crdb/psu-presence-alert-classes/up01.sql @@ -0,0 +1,6 @@ +ALTER TYPE + omicron.public.alert_class +ADD VALUE IF NOT EXISTS + 'hardware.power_shelf.psu.insert' +AFTER + 'test.quux.bar.baz' diff --git a/schema/crdb/psu-presence-alert-classes/up02.sql b/schema/crdb/psu-presence-alert-classes/up02.sql new file mode 100644 index 00000000000..d7d03917237 --- /dev/null +++ b/schema/crdb/psu-presence-alert-classes/up02.sql @@ -0,0 +1,6 @@ +ALTER TYPE + omicron.public.alert_class +ADD VALUE IF NOT EXISTS + 'hw.remove.power.power_shelf.psu' +AFTER + 'hardware.power_shelf.psu.insert'