Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6e094f4
[fm] Add disk diagnoser with typed fact tables
smklein Jun 2, 2026
bfedaa5
better errors
smklein Jun 3, 2026
1b5106a
clearer constraints, a non-null column for physical_id
smklein Jun 3, 2026
ae1ec41
merge
smklein Jun 10, 2026
4b665e7
PR feedback: growable fact CHECK constraint, filter deleted disks, to…
smklein Jun 10, 2026
272b368
[fm] diagnosis engines read their Input from the SitrepBuilder
smklein Jun 10, 2026
14c2a06
drop unused lookup_fm_fact_physical_disk_for_case index
smklein Jun 10, 2026
d2a6767
[fm] disk diagnoser: index parent cases by disk
smklein Jun 10, 2026
82ef44f
simplify comments
smklein Jun 11, 2026
6f2e4a5
[fm] disk diagnoser: close uninterpretable cases; deflake omdb test
smklein Jun 11, 2026
cebfc8c
merge
smklein Jun 11, 2026
83c73ad
[fm] derive fact row kind from payload via exhaustive match
smklein Jun 12, 2026
546fc5d
[fm] tidy disk diagnoser per review feedback
smklein Jun 15, 2026
2718755
[fm] split Fact into FactMetadata + payload
smklein Jun 15, 2026
0ada45d
[fm] disk diagnoser: clearer names for parsing a case
smklein Jun 15, 2026
39e4228
[fm] disk diagnoser: build case_by_disk in a single pass
smklein Jun 15, 2026
4b6e928
[fm] disk diagnoser: separate case-closing from fact reconciliation
smklein Jun 15, 2026
e731c5d
[fm] disk diagnoser tests: name the disk-fact triple, dedup fact-id l…
smklein Jun 16, 2026
f9479fc
[fm] address review feedback from eliza on #10541
smklein Jun 16, 2026
221dcc2
merge
smklein Jun 16, 2026
3c5545d
merge
smklein Jun 16, 2026
0604e5e
[fm] omdb test: redact racy fm_sitrep_gc orphan counts
smklein Jun 16, 2026
8e68736
[fm] address PR review feedback
smklein Jun 17, 2026
c4ff887
[fm] use DuplicateItem error to read kept case, avoiding a second lookup
smklein Jun 17, 2026
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
50 changes: 32 additions & 18 deletions dev-tools/omdb/tests/successes.out
Original file line number Diff line number Diff line change
Expand Up @@ -700,20 +700,23 @@ task: "fm_analysis"
configured period: every <REDACTED_DURATION>m
last completed activation: <REDACTED ITERATIONS>, triggered by <TRIGGERED_BY_REDACTED>
started at <REDACTED_TIMESTAMP> (<REDACTED DURATION>s ago) and ran for <REDACTED DURATION>ms
parent sitrep ID: None
parent sitrep ID: Some(..........<REDACTED_UUID>........... (sitrep))
current inventory collection ID: Some(..........<REDACTED_UUID>........... (collection))
ereport classes consumed: (none)
FAULT MANAGEMENT ANALYSIS SUMMARY
=================================
/!\ analysis failed: FM analysis is not yet implemented
no changes from the current situation report (Some(..........<REDACTED_UUID>........... (sitrep)))

fault management analysis inputs
--------------------------------
parent sitrep: <none>
parent sitrep: ..........<REDACTED_UUID>...........
inventory collection: ..........<REDACTED_UUID>...........
<FM_INPUT_INV_COMPARISON_REDACTED>
no new ereports since the parent sitrep
no cases copied forward

no in-service control plane disks

fault management analysis report
--------------------------------
sitrep ID: ..........<REDACTED_UUID>...........
Expand All @@ -724,28 +727,28 @@ task: "fm_rendezvous"
configured period: every <REDACTED_DURATION>m
last completed activation: <REDACTED ITERATIONS>, triggered by <TRIGGERED_BY_REDACTED>
started at <REDACTED_TIMESTAMP> (<REDACTED DURATION>s ago) and ran for <REDACTED DURATION>ms
(i) no FM situation report loaded, so rendezvous was not performed
current sitrep: ..........<REDACTED_UUID>...........
creating requested alerts:
(i) note: this operation was not executed
started at <REDACTED_TIMESTAMP> (<REDACTED DURATION>s ago) and ran for <REDACTED DURATION>ms
alerts requested: 0
requested in this sitrep: 0
created in this activation: 0
already created: 0
errors: 0
creating requested support bundles:
(i) note: this operation was not executed
started at <REDACTED_TIMESTAMP> (<REDACTED DURATION>s ago) and ran for <REDACTED DURATION>ms
support bundles requested: 0
requested in this sitrep: 0
created in this activation: 0
already created: 0
errors: 0
marking ereports as seen:
(i) note: this operation was not executed
started at <REDACTED_TIMESTAMP> (<REDACTED DURATION>s ago) and ran for <REDACTED DURATION>ms
total ereports in sitrep: 0
not marked when the sitrep was loaded: 0
marked seen by this activation: 0
already marked seen: 0
batch size: 0
batch size: 1000
batches: 0
errors: 0

Expand All @@ -762,6 +765,8 @@ task: "fm_sitrep_gc"
batches: 1
orphaned fm_ereport_in_case rows deleted: 0
batches: 1
orphaned fm_fact_physical_disk rows deleted: 0
batches: 1
orphaned fm_support_bundle_request rows deleted: 0
batches: 1
orphaned fm_support_bundle_request_data_selection_ereports rows deleted: 0
Expand All @@ -775,7 +780,9 @@ task: "fm_sitrep_loader"
configured period: every <REDACTED_DURATION>s
last completed activation: <REDACTED ITERATIONS>, triggered by <TRIGGERED_BY_REDACTED>
started at <REDACTED_TIMESTAMP> (<REDACTED DURATION>s ago) and ran for <REDACTED DURATION>ms
no FM situation report available to load
loaded latest FM situation report as of <REDACTED_TIMESTAMP>:
sitrep ..........<REDACTED_UUID>........... (sitrep) (v1)
made current at: <REDACTED_TIMESTAMP>

task: "instance_reincarnation"
configured period: every <REDACTED_DURATION>m
Expand Down Expand Up @@ -1391,20 +1398,23 @@ task: "fm_analysis"
configured period: every <REDACTED_DURATION>m
last completed activation: <REDACTED ITERATIONS>, triggered by <TRIGGERED_BY_REDACTED>
started at <REDACTED_TIMESTAMP> (<REDACTED DURATION>s ago) and ran for <REDACTED DURATION>ms
parent sitrep ID: None
parent sitrep ID: Some(..........<REDACTED_UUID>........... (sitrep))
current inventory collection ID: Some(..........<REDACTED_UUID>........... (collection))
ereport classes consumed: (none)
FAULT MANAGEMENT ANALYSIS SUMMARY
=================================
/!\ analysis failed: FM analysis is not yet implemented
no changes from the current situation report (Some(..........<REDACTED_UUID>........... (sitrep)))

fault management analysis inputs
--------------------------------
parent sitrep: <none>
parent sitrep: ..........<REDACTED_UUID>...........
inventory collection: ..........<REDACTED_UUID>...........
<FM_INPUT_INV_COMPARISON_REDACTED>
no new ereports since the parent sitrep
no cases copied forward

no in-service control plane disks

fault management analysis report
--------------------------------
sitrep ID: ..........<REDACTED_UUID>...........
Expand All @@ -1415,28 +1425,28 @@ task: "fm_rendezvous"
configured period: every <REDACTED_DURATION>m
last completed activation: <REDACTED ITERATIONS>, triggered by <TRIGGERED_BY_REDACTED>
started at <REDACTED_TIMESTAMP> (<REDACTED DURATION>s ago) and ran for <REDACTED DURATION>ms
(i) no FM situation report loaded, so rendezvous was not performed
current sitrep: ..........<REDACTED_UUID>...........
creating requested alerts:
(i) note: this operation was not executed
started at <REDACTED_TIMESTAMP> (<REDACTED DURATION>s ago) and ran for <REDACTED DURATION>ms
alerts requested: 0
requested in this sitrep: 0
created in this activation: 0
already created: 0
errors: 0
creating requested support bundles:
(i) note: this operation was not executed
started at <REDACTED_TIMESTAMP> (<REDACTED DURATION>s ago) and ran for <REDACTED DURATION>ms
support bundles requested: 0
requested in this sitrep: 0
created in this activation: 0
already created: 0
errors: 0
marking ereports as seen:
(i) note: this operation was not executed
started at <REDACTED_TIMESTAMP> (<REDACTED DURATION>s ago) and ran for <REDACTED DURATION>ms
total ereports in sitrep: 0
not marked when the sitrep was loaded: 0
marked seen by this activation: 0
already marked seen: 0
batch size: 0
batch size: 1000
batches: 0
errors: 0

Expand All @@ -1453,6 +1463,8 @@ task: "fm_sitrep_gc"
batches: 1
orphaned fm_ereport_in_case rows deleted: 0
batches: 1
orphaned fm_fact_physical_disk rows deleted: 0
batches: 1
orphaned fm_support_bundle_request rows deleted: 0
batches: 1
orphaned fm_support_bundle_request_data_selection_ereports rows deleted: 0
Expand All @@ -1466,7 +1478,9 @@ task: "fm_sitrep_loader"
configured period: every <REDACTED_DURATION>s
last completed activation: <REDACTED ITERATIONS>, triggered by <TRIGGERED_BY_REDACTED>
started at <REDACTED_TIMESTAMP> (<REDACTED DURATION>s ago) and ran for <REDACTED DURATION>ms
no FM situation report available to load
loaded latest FM situation report as of <REDACTED_TIMESTAMP>:
sitrep ..........<REDACTED_UUID>........... (sitrep) (v1)
made current at: <REDACTED_TIMESTAMP>

task: "instance_reincarnation"
configured period: every <REDACTED_DURATION>m
Expand Down
15 changes: 15 additions & 0 deletions dev-tools/omdb/tests/test_all_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@ async fn test_omdb_success_cases() {
.wait_for_at_least_one_inventory_collection(Duration::from_secs(60))
.await;

// Wait until `fm_analysis` has committed at least one sitrep, so that the
// omdb snapshot for FM tasks is stable. (Otherwise sitrep IDs render as
// `None` or `Some(...)` depending on whether the task's natural cadence
// had landed by the time we sample it.)
cptestctx.wait_for_at_least_one_sitrep(Duration::from_secs(60)).await;

let mut output = String::new();

let invocations: &[&[&str]] = &[
Expand Down Expand Up @@ -350,6 +356,15 @@ async fn test_omdb_success_cases() {
.field("triggered by", r"[\w ]+")
.section(&["task: \"tuf_artifact_replication\"", "request ringbuf:"]);

// The `fm_analysis` task's input report includes a line comparing the
// current inventory collection against the parent sitrep's collection,
// which can be either "same" or "different" depending on whether a new
// inventory was collected between sitreps. Collapse both forms.
redactor.variable_regex(
"fm_input_inv_comparison",
r" --> (same collection as parent sitrep|different from parent sitrep \(collection [-a-f0-9]+\))",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

urgh, I wanted to suggest we wrap this line, but it's a raw string, so that's hard. never mind.

);

// The `sp_ereport_ingester` task's output depends on how many simulated
// sled agents ahppen to register with Nexus before its first execution.
// These redactions work around the issue described in
Expand Down
2 changes: 2 additions & 0 deletions nexus/db-model/src/fm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ mod case;
pub use case::*;
mod diagnosis_engine;
pub use diagnosis_engine::*;
mod fact_physical_disk;
pub use fact_physical_disk::*;
mod support_bundle_request;
pub use support_bundle_request::*;

Expand Down
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 @@ -64,6 +64,7 @@ impl CaseMetadata {
alerts_requested: _,
support_bundles_requested: _,
ereports: _,
facts: _,
} = case;
Self {
sitrep_id: sitrep_id.into(),
Expand Down
7 changes: 7 additions & 0 deletions nexus/db-model/src/fm/diagnosis_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,17 @@ impl_enum_type!(
pub enum DiagnosisEngine;

PowerShelf => b"power_shelf"
PhysicalDisk => b"physical_disk"

);

impl From<DiagnosisEngine> for fm::DiagnosisEngineKind {
fn from(de: DiagnosisEngine) -> Self {
match de {
DiagnosisEngine::PowerShelf => fm::DiagnosisEngineKind::PowerShelf,
DiagnosisEngine::PhysicalDisk => {
fm::DiagnosisEngineKind::PhysicalDisk
}
}
}
}
Expand All @@ -39,6 +43,9 @@ impl From<fm::DiagnosisEngineKind> for DiagnosisEngine {
fn from(fm_de: fm::DiagnosisEngineKind) -> Self {
match fm_de {
fm::DiagnosisEngineKind::PowerShelf => DiagnosisEngine::PowerShelf,
fm::DiagnosisEngineKind::PhysicalDisk => {
DiagnosisEngine::PhysicalDisk
}
}
}
}
Expand Down
158 changes: 158 additions & 0 deletions nexus/db-model/src/fm/fact_physical_disk.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// 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/.

//! Database representation of the physical-disk diagnosis engine's facts.
//!
//! Each physical-disk fact is stored as typed columns in the
//! `fm_fact_physical_disk` table. The `kind` discriminant selects which payload
//! columns are populated; a CHECK constraint
//! (`zpool_unhealthy_columns_present`) enforces that the right columns are
//! non-NULL for each kind. See [`nexus_types::fm::DiskFact`] for semantics.

use crate::DbTypedUuid;
use crate::impl_enum_type;
use crate::inventory::InvZpoolHealth;
use chrono::{DateTime, Utc};
use nexus_db_schema::schema::fm_fact_physical_disk;
use nexus_types::fm;
use nexus_types::fm::{DiskFact, FactPayload, ZpoolUnhealthyFactPayload};
use omicron_common::api::external::Error;
use omicron_uuid_kinds::{
CaseKind, CollectionKind, FactKind, PhysicalDiskKind, SitrepKind, ZpoolKind,
};

impl_enum_type!(
FmFactPhysicalDiskKindEnum:

#[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, PartialEq, Eq)]
pub enum FmFactPhysicalDiskKind;

ZpoolUnhealthy => b"zpool_unhealthy"
);

/// Diesel row for the `fm_fact_physical_disk` table.
///
/// The payload columns are populated according to `kind`: a column is `Some`
/// if it belongs to that `kind`'s payload, and `None` otherwise (by
/// convention; the per-kind CHECK constraints, e.g.
/// `zpool_unhealthy_columns_present`, enforce only the "present for the
/// matching kind" direction so that future kinds may share columns).
#[derive(Queryable, Insertable, Clone, Debug, Selectable)]
#[diesel(table_name = fm_fact_physical_disk)]
pub struct FmFactPhysicalDisk {
pub id: DbTypedUuid<FactKind>,
/// The sitrep to which this fact belongs.
///
/// This will change as the fact is carried forward from one sitrep to the
/// next.
pub sitrep_id: DbTypedUuid<SitrepKind>,
pub case_id: DbTypedUuid<CaseKind>,
/// Sitrep in which this fact was first added.
///
/// Preserved unchanged when the fact is carried forward; debug-only.
pub created_sitrep_id: DbTypedUuid<SitrepKind>,
pub comment: String,
Comment on lines +40 to +51

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extremely not a blocker for this PR, but it occurs to me that these fields are going to be present on every fact table and also on the preexisting non-DE-specific "case objects" (i.e. alert requests, support bundle requests, and so on). I wonder if we might eventually want to have a macro or something similar to the existing #[derive(Asset)] and #[derive(Resource)] macros in nexus-db-macros for these structures.

For one thing, this could generate a fact_metadata() accessor automagically, and maybe it could also eventually be something we could hang the sitrep GC stuff off of, rather than having to manually add the GCed tables in the db-queries file.

I may try to hack something like that together if I have some spare time, or someone else might want to.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is reasonable idea - also, prioritization-wise, I do really want to get this merged with some DB schema first.

I really want "many DEs, some of which have multiple facts" before investing the time in macros, just to have extra validation that we won't need to churn. I do think those macros will make it easier for developers to add facts, but the "total number of DEs and facts" seems very small right now, so I'd prefer to "wait a little, and do it with high confidence" rather than do it too early.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, for sure!


/// The physical disk this fact is about. Common to every `kind`, so it is
/// always present (the column is `NOT NULL`).
pub physical_disk_id: DbTypedUuid<PhysicalDiskKind>,
pub kind: FmFactPhysicalDiskKind,

// Columns for the `ZpoolUnhealthy` kind.
pub zpool_id: Option<DbTypedUuid<ZpoolKind>>,
pub last_seen_health: Option<InvZpoolHealth>,
pub observed_in_inv: Option<DbTypedUuid<CollectionKind>>,
pub time_observed: Option<DateTime<Utc>>,
Comment on lines +55 to +61

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I understand it, the design here is that we would allow a DE to record multiple distinct fact types in a case by creating multiple records in the fact table for that DE associated with the case, and each of those records would map to exactly one variant of a Rust enum when the sitrep is loaded. I'm starting to wonder whether we really need to do this, or if there might be a simpler approach. Do we really anticipate that we will need to have this one-to-many relationship between a case and its facts, or could we do something where instead of having a "fact record", we have a "facts record" for each case?

Here, if we wanted to add a different kind of fact to the "physical disk" diagnosis engine, we would do something like adding a new variant of FmFactPhysicalDiskKind (say, "PcieLinkDown" or something), and then adding a bunch of nullable fields to the facts table, and having the DE insert a new fact record to represent the "PcieLinkDown" fact. But...what if we just had one record for each case that could represent both the unhealthy zpool facts and the PCIe link down facts? If the DE wanted to be able to have Rust types for each set of facts, you could imagine doing something like:

impl FmFactsPhysicalDisk {
    pub fn as_zpool_unhealthy(&self) -> Option<ZpoolUnhealthy> {
       let Self {
            zpool_id: Some(zpool_id), 
            last_seen_health: Some(last_seen_health), 
            unhealthy_zpool_observed_in_inv: Some(observed_in_inv),
            unhealthy_zpool_time_observed: Some(time_observed),
            ... // ignore unrelated fields
        } = self else {
            return None;
        };
        Some(ZpoolUnhealthy {
            zpool_id,
            last_seen_health,
            observed_in_inv,
            time_observed,
        })
    }

    
    pub fn as_pcie_link_down(&self) -> Option<PcieLinkDown> {
       let Self {
            link_down_whatever_fields_this_has: Some(whatever),
            link_down_other_field: Some(other_field),
            ... // ignore unrelated fields
        } = self else {
            return None;
        };
        Some(PcieLinkDown {
            whatever,
            other_field,
        })
    }
}

We could still have an enum with variants for all those structs, too, if we wanted to, and could do something like having an iterator over each kind of fact the case contains, but I'm not sure if we would need that.

And, in some cases, the facts we want to represent may be simple enough that we don't actually need all this ceremony and they're just single columns that may or may not be present in a record for a particular case. This is the most attractive part of this approach, IMO --- if a DE just wants to be able to store a few different facts that are each representable by a single column, it can just add them to the schema for its table without having to go through all the ceremony of messing around with enum variants and stuff.

This might end up making things a bit easier, and prevents us from being able to represent some of the nonsensical cases we currently can (such as a case with several different zpool_unhealthy facts for the same zpool).

On the other hand, there may also be future DEs that do need the one-to-many fact relationship --- for example, you might want to be able to record that various crucible volumes were all impacted by an unhealthy physical disk. In that case, we might be able to represent that by using SQL arrays to store the multiple instances of the same fact in one record, like impacted_crucible_volume_ids UUID ARRAY or something. This might get messy if we wanted these one-to-many facts to store multiple fields per fact, but maybe there's a nice way to do that as well? Here, we don't care about the main advantage of representing one-to-many relationships as separate tables, because we're not going to be doing a bunch of JOINs on sitrep data; we're just loading the whole thing in one go.

Basically, I think the crux of what I'm getting at here is whether we actually need "fact tables" to represent a case's DE-specific facts, or if all we really need are "case tables" that extend a case with one record in a DE-specific table. This isn't a strongly-held belief and I'm very open to being convinced we do need the additional expressiveness of multiple fact records per case, but I wanted to bring it up so we could weigh some of the pros and cons.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per a Matrix conversation with @smklein, I think we've determined that SQL arrays are not going to be sufficiently expressive for a lot of the multiple fact cases we may want to be able to represent, so we're going to continue with the current thing.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my motivation towards "have a one to many relationship between cases and facts" is that I suspect, especially for more complex cases, we'll need to build up a variety of facts over time to actually determine whether the case should be transformed into an active problem. I don't really know, yet, how complex this is going to get, but IMO the flexibility of: "you want to be able to add arbitrary data points as facts in the DB, and then re-assemble them in memory to analyze the current case status" seems like it should be possible.

There are also some limitations with ARRAY types in CRDB specifically, they warn:

The ARRAY data type is useful for ensuring compatibility with ORMs and other tools. However, if such compatibility is not a concern, it's more flexible to design your schema with normalized tables.

Which doesn't mean they're always bad! just that... there may be footguns we're dropping for ourselves if we want them to support arbitrary data this way. For example, they cannot support nesting, and not all types can be used in arrays.

Comment on lines +57 to +61

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

much as it pains me to suggest this, I wonder somewhat if we might want to begin a convention of prefixing all of these with the fact kind name (so here, zpool_unhealthy_) to make it more obvious what thing they correspond to? on the other hand it feels so unpleasant that i regret suggesting this.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asdf I'm on the fence about this. I understand "why", but this could result in some obnoxiously long column names, where we're already guarded by the CONSTRAINT + in-memory reconstructed types.

}

impl FmFactPhysicalDisk {
/// Build a row from a fact's shared metadata (`fact`) and its
/// already-dispatched physical-disk payload (`disk_fact`).
///
/// Callers route each fact to its engine's table by matching on
/// [`fact.payload`](fm::case::Fact::payload) and pass the matched payload
/// here, so this never has to interpret another engine's payload.
pub fn from_sitrep(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorta following the convention of the other fm-based models here (they all use from_sitrep), but I kinda think this method should maybe just be called "new"?

IDK; might change this in a follow-up.

sitrep_id: impl Into<DbTypedUuid<SitrepKind>>,
case_id: impl Into<DbTypedUuid<CaseKind>>,
fact: &fm::case::Fact,
disk_fact: &DiskFact,
) -> Self {
let base = Self {
id: fact.id.into(),
sitrep_id: sitrep_id.into(),
case_id: case_id.into(),
created_sitrep_id: fact.created_sitrep_id.into(),
comment: fact.comment.clone(),
physical_disk_id: disk_fact.physical_disk_id().into(),
kind: FmFactPhysicalDiskKind::ZpoolUnhealthy,
zpool_id: None,
last_seen_health: None,
observed_in_inv: None,
time_observed: None,
};
match disk_fact {
DiskFact::ZpoolUnhealthy(p) => Self {
kind: FmFactPhysicalDiskKind::ZpoolUnhealthy,
zpool_id: Some(p.zpool_id.into()),
last_seen_health: Some(p.last_seen_health.into()),
observed_in_inv: Some(p.observed_in_inv.into()),
time_observed: Some(p.time_observed),
..base
},
}
}

/// Reconstruct an in-memory fact from a row.
///
/// The payload columns the database's CHECK constraint guarantees are
/// non-NULL for this `kind` are unwrapped; a NULL where one is required
/// indicates a corrupt row (e.g. hand-edited) and yields an internal
/// error rather than a panic.
pub fn into_fact(self) -> Result<fm::case::Fact, Error> {
let kind = self.kind;
let payload = match kind {
FmFactPhysicalDiskKind::ZpoolUnhealthy => {
FactPayload::PhysicalDisk(DiskFact::ZpoolUnhealthy(
ZpoolUnhealthyFactPayload {
physical_disk_id: self.physical_disk_id.into(),
zpool_id: self
.zpool_id
.ok_or_else(|| missing_column(kind, "zpool_id"))?
.into(),
last_seen_health: self
.last_seen_health
.ok_or_else(|| {
missing_column(kind, "last_seen_health")
})?
.into(),
observed_in_inv: self
.observed_in_inv
.ok_or_else(|| {
missing_column(kind, "observed_in_inv")
})?
.into(),
time_observed: self.time_observed.ok_or_else(|| {
missing_column(kind, "time_observed")
})?,
},
))
}
};
Ok(fm::case::Fact {
id: self.id.into(),
created_sitrep_id: self.created_sitrep_id.into(),
payload,
comment: self.comment,
})
}
Comment on lines +103 to +145

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

someday, i bet we'll be able to figure out a way to derive all this from an enum, but let's save that for later. :)

}

fn missing_column(kind: FmFactPhysicalDiskKind, column: &str) -> Error {
Error::internal_error(&format!(
"fm_fact_physical_disk row of kind {kind:?} has a NULL {column}, \
violating the CHECK constraint requiring it to be non-NULL for \
this kind"
))
}
Loading