Skip to content

Commit b845b31

Browse files
fm: persist support bundle requests and data selections (#10200)
Add database tables, models, and datastore methods for persisting `BundleDataSelection` — the set of data categories a support bundle should collect — in two contexts: 1. FM sitrep support bundle requests: an `fm_support_bundle_request` table (new) links a bundle request to a case and sitrep, and five per-variant child tables (`fm_support_bundle_request_data_selection_{reconfigurator, host_info, sled_cubby_info, sp_dumps, ereports}`) record which data categories to gather and with what filters. 2. Support bundle data selection: five analogous per-variant child tables (`support_bundle_data_selection_{reconfigurator, host_info, sled_cubby_info, sp_dumps, ereports}`) record the data selection for each support bundle row. Existing bundles are backfilled with `BundleDataSelection::all()` in the migration. Schema changes (version 247): - `fm_support_bundle_request` table with indexes. - `fm_support_bundle_request_data_selection_*` child tables. - `support_bundle.fm_case_id`: nullable column linking a bundle to the originating FM case. - `support_bundle_data_selection_*` child tables. - Backfill migration populates data selection rows for all pre-existing support bundles. Model changes: - `SupportBundleRequest` and child row models. - Child row models for `SupportBundle`. Datastore changes: - `fm_sitrep_read`: refactor alert-request loading into `fm_sitrep_alert_requests_read`; add symmetric `fm_sitrep_support_bundle_requests_read` that loads request rows and reassembles BundleDataSelection from child tables. - `fm_sitrep_insert`: decompose BundleDataSelection into per-variant rows on write. - Sitrep GC deletes per-variant rows before request rows. - `support_bundle_data_selection_get`: look up a bundle's data selection. - Support bundle creation inserts BundleDataSelection::all() rows. - Support bundle deletion cleans up data selection rows in a transaction. - New test `test_sitrep_support_bundle_requests_roundtrip`; extended coverage in existing sitrep roundtrip and bundle lifecycle tests. Also fix `BundleDataSelection::all()` to use `now_db_precision()` for microsecond-precision timestamps that survive CRDB roundtrips. Context: #10062.
1 parent 19d8351 commit b845b31

25 files changed

Lines changed: 1627 additions & 111 deletions

File tree

dev-tools/omdb/tests/successes.out

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -735,15 +735,23 @@ task: "fm_sitrep_gc"
735735
configured period: every <REDACTED_DURATION>s
736736
last completed activation: <REDACTED ITERATIONS>, triggered by <TRIGGERED_BY_REDACTED>
737737
started at <REDACTED_TIMESTAMP> (<REDACTED DURATION>s ago) and ran for <REDACTED DURATION>ms
738-
batch size: 1000
739-
orphaned sitreps deleted: 0
740-
batches: 1
741-
orphaned fm_alert_request rows deleted: 0
742-
batches: 1
743-
orphaned fm_case rows deleted: 0
744-
batches: 1
745-
orphaned fm_ereport_in_case rows deleted: 0
746-
batches: 1
738+
batch size: 1000
739+
orphaned sitreps deleted: 0
740+
batches: 1
741+
orphaned fm_alert_request rows deleted: 0
742+
batches: 1
743+
orphaned fm_case rows deleted: 0
744+
batches: 1
745+
orphaned fm_ereport_in_case rows deleted: 0
746+
batches: 1
747+
orphaned fm_support_bundle_request rows deleted: 0
748+
batches: 1
749+
orphaned fm_support_bundle_request_data_selection_ereports rows deleted: 0
750+
batches: 1
751+
orphaned fm_support_bundle_request_data_selection_flags rows deleted: 0
752+
batches: 1
753+
orphaned fm_support_bundle_request_data_selection_host_info rows deleted: 0
754+
batches: 1
747755

748756
task: "fm_sitrep_loader"
749757
configured period: every <REDACTED_DURATION>s
@@ -1393,15 +1401,23 @@ task: "fm_sitrep_gc"
13931401
configured period: every <REDACTED_DURATION>s
13941402
last completed activation: <REDACTED ITERATIONS>, triggered by <TRIGGERED_BY_REDACTED>
13951403
started at <REDACTED_TIMESTAMP> (<REDACTED DURATION>s ago) and ran for <REDACTED DURATION>ms
1396-
batch size: 1000
1397-
orphaned sitreps deleted: 0
1398-
batches: 1
1399-
orphaned fm_alert_request rows deleted: 0
1400-
batches: 1
1401-
orphaned fm_case rows deleted: 0
1402-
batches: 1
1403-
orphaned fm_ereport_in_case rows deleted: 0
1404-
batches: 1
1404+
batch size: 1000
1405+
orphaned sitreps deleted: 0
1406+
batches: 1
1407+
orphaned fm_alert_request rows deleted: 0
1408+
batches: 1
1409+
orphaned fm_case rows deleted: 0
1410+
batches: 1
1411+
orphaned fm_ereport_in_case rows deleted: 0
1412+
batches: 1
1413+
orphaned fm_support_bundle_request rows deleted: 0
1414+
batches: 1
1415+
orphaned fm_support_bundle_request_data_selection_ereports rows deleted: 0
1416+
batches: 1
1417+
orphaned fm_support_bundle_request_data_selection_flags rows deleted: 0
1418+
batches: 1
1419+
orphaned fm_support_bundle_request_data_selection_host_info rows deleted: 0
1420+
batches: 1
14051421

14061422
task: "fm_sitrep_loader"
14071423
configured period: every <REDACTED_DURATION>s

nexus/db-model/src/fm.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ mod case;
2626
pub use case::*;
2727
mod diagnosis_engine;
2828
pub use diagnosis_engine::*;
29+
mod support_bundle_request;
30+
pub use support_bundle_request::*;
2931

3032
#[derive(Queryable, Insertable, Clone, Debug, Selectable)]
3133
#[diesel(table_name = fm_sitrep)]

nexus/db-model/src/fm/case.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
77
use super::AlertRequest;
88
use super::DiagnosisEngine;
9+
use super::SupportBundleRequest;
910
use crate::DbTypedUuid;
1011
use crate::ereport;
1112
use nexus_db_schema::schema::{fm_case, fm_ereport_in_case};
@@ -142,4 +143,5 @@ pub struct Case {
142143
pub metadata: CaseMetadata,
143144
pub ereports: Vec<CaseEreport>,
144145
pub alerts_requested: Vec<AlertRequest>,
146+
pub support_bundles_requested: Vec<SupportBundleRequest>,
145147
}
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
//! Fault management support bundle requests and data selection models.
6+
7+
use crate::DbTypedUuid;
8+
use chrono::{DateTime, Utc};
9+
use nexus_db_schema::schema::{
10+
fm_support_bundle_request,
11+
fm_support_bundle_request_data_selection_ereports,
12+
fm_support_bundle_request_data_selection_flags,
13+
fm_support_bundle_request_data_selection_host_info,
14+
};
15+
use nexus_types::fm;
16+
use nexus_types::fm::ereport::{EreportFilters, EreportFiltersParams};
17+
use nexus_types::support_bundle::{
18+
BundleData, BundleDataCategory, SledSelection,
19+
};
20+
use omicron_uuid_kinds::{
21+
CaseKind, GenericUuid, SitrepKind, SledUuid, SupportBundleKind,
22+
};
23+
24+
#[derive(Queryable, Insertable, Clone, Debug, Selectable)]
25+
#[diesel(table_name = fm_support_bundle_request)]
26+
pub struct SupportBundleRequest {
27+
pub id: DbTypedUuid<SupportBundleKind>,
28+
pub sitrep_id: DbTypedUuid<SitrepKind>,
29+
pub requested_sitrep_id: DbTypedUuid<SitrepKind>,
30+
pub case_id: DbTypedUuid<CaseKind>,
31+
}
32+
33+
impl SupportBundleRequest {
34+
pub fn from_sitrep(
35+
sitrep_id: impl Into<DbTypedUuid<SitrepKind>>,
36+
case_id: impl Into<DbTypedUuid<CaseKind>>,
37+
req: fm::case::SupportBundleRequest,
38+
) -> Self {
39+
let fm::case::SupportBundleRequest {
40+
id,
41+
requested_sitrep_id,
42+
data_selection: _,
43+
} = req;
44+
SupportBundleRequest {
45+
id: id.into(),
46+
sitrep_id: sitrep_id.into(),
47+
requested_sitrep_id: requested_sitrep_id.into(),
48+
case_id: case_id.into(),
49+
}
50+
}
51+
}
52+
53+
/// Flags table row — tracks which payload-less data categories are selected.
54+
/// Always inserted alongside the parent bundle request.
55+
#[derive(Queryable, Insertable, Clone, Debug, Selectable)]
56+
#[diesel(table_name = fm_support_bundle_request_data_selection_flags)]
57+
pub struct DataSelectionFlags {
58+
pub sitrep_id: DbTypedUuid<SitrepKind>,
59+
pub request_id: DbTypedUuid<SupportBundleKind>,
60+
pub include_reconfigurator: bool,
61+
pub include_sled_cubby_info: bool,
62+
pub include_sp_dumps: bool,
63+
}
64+
65+
impl DataSelectionFlags {
66+
pub fn from_sitrep(
67+
sitrep_id: impl Into<DbTypedUuid<SitrepKind>>,
68+
request_id: impl Into<DbTypedUuid<SupportBundleKind>>,
69+
data_selection: &nexus_types::support_bundle::BundleDataSelection,
70+
) -> Self {
71+
DataSelectionFlags {
72+
sitrep_id: sitrep_id.into(),
73+
request_id: request_id.into(),
74+
include_reconfigurator: data_selection
75+
.contains(BundleDataCategory::Reconfigurator),
76+
include_sled_cubby_info: data_selection
77+
.contains(BundleDataCategory::SledCubbyInfo),
78+
include_sp_dumps: data_selection
79+
.contains(BundleDataCategory::SpDumps),
80+
}
81+
}
82+
}
83+
84+
#[derive(Queryable, Insertable, Clone, Debug, Selectable)]
85+
#[diesel(table_name = fm_support_bundle_request_data_selection_host_info)]
86+
pub struct HostInfo {
87+
pub sitrep_id: DbTypedUuid<SitrepKind>,
88+
pub request_id: DbTypedUuid<SupportBundleKind>,
89+
pub all_sleds: bool,
90+
pub sled_ids: Vec<uuid::Uuid>,
91+
}
92+
93+
impl HostInfo {
94+
pub fn from_sitrep(
95+
sitrep_id: impl Into<DbTypedUuid<SitrepKind>>,
96+
request_id: impl Into<DbTypedUuid<SupportBundleKind>>,
97+
sleds: SledSelection,
98+
) -> Self {
99+
let (all_sleds, sled_ids) = match sleds {
100+
SledSelection::All => (true, Vec::new()),
101+
SledSelection::Specific(ids) => (
102+
false,
103+
ids.into_iter().map(|id| id.into_untyped_uuid()).collect(),
104+
),
105+
};
106+
HostInfo {
107+
sitrep_id: sitrep_id.into(),
108+
request_id: request_id.into(),
109+
all_sleds,
110+
sled_ids,
111+
}
112+
}
113+
}
114+
115+
impl From<HostInfo> for BundleData {
116+
fn from(row: HostInfo) -> Self {
117+
let HostInfo { sitrep_id: _, request_id: _, all_sleds, sled_ids } = row;
118+
let selection = if all_sleds {
119+
SledSelection::All
120+
} else {
121+
SledSelection::Specific(
122+
sled_ids.into_iter().map(SledUuid::from_untyped_uuid).collect(),
123+
)
124+
};
125+
BundleData::HostInfo(selection)
126+
}
127+
}
128+
129+
#[derive(Queryable, Insertable, Clone, Debug, Selectable)]
130+
#[diesel(table_name = fm_support_bundle_request_data_selection_ereports)]
131+
pub struct Ereports {
132+
pub sitrep_id: DbTypedUuid<SitrepKind>,
133+
pub request_id: DbTypedUuid<SupportBundleKind>,
134+
pub start_time: Option<DateTime<Utc>>,
135+
pub end_time: Option<DateTime<Utc>>,
136+
pub only_serials: Vec<String>,
137+
pub only_classes: Vec<String>,
138+
}
139+
140+
impl Ereports {
141+
pub fn from_sitrep(
142+
sitrep_id: impl Into<DbTypedUuid<SitrepKind>>,
143+
request_id: impl Into<DbTypedUuid<SupportBundleKind>>,
144+
filters: EreportFilters,
145+
) -> Self {
146+
Ereports {
147+
sitrep_id: sitrep_id.into(),
148+
request_id: request_id.into(),
149+
start_time: filters.start_time(),
150+
end_time: filters.end_time(),
151+
only_serials: filters.only_serials().to_vec(),
152+
only_classes: filters.only_classes().to_vec(),
153+
}
154+
}
155+
}
156+
157+
impl TryFrom<Ereports> for BundleData {
158+
type Error = omicron_common::api::external::Error;
159+
160+
fn try_from(row: Ereports) -> Result<Self, Self::Error> {
161+
let Ereports {
162+
sitrep_id: _,
163+
request_id: _,
164+
start_time,
165+
end_time,
166+
only_serials,
167+
only_classes,
168+
} = row;
169+
EreportFiltersParams {
170+
start_time,
171+
end_time,
172+
only_serials,
173+
only_classes,
174+
}
175+
.try_into()
176+
.map(BundleData::Ereports)
177+
}
178+
}
179+
180+
/// Joined query result: flags + optional host_info + optional ereports.
181+
/// All fields use `#[diesel(embed)]` so no `table_name` is needed.
182+
#[derive(Queryable, Selectable)]
183+
pub struct BundleDataSelection {
184+
#[diesel(embed)]
185+
pub flags: DataSelectionFlags,
186+
#[diesel(embed)]
187+
pub host_info: Option<HostInfo>,
188+
#[diesel(embed)]
189+
pub ereports: Option<Ereports>,
190+
}
191+
192+
impl TryFrom<BundleDataSelection>
193+
for nexus_types::support_bundle::BundleDataSelection
194+
{
195+
type Error = omicron_common::api::external::Error;
196+
197+
fn try_from(row: BundleDataSelection) -> Result<Self, Self::Error> {
198+
let mut selection =
199+
nexus_types::support_bundle::BundleDataSelection::new();
200+
if row.flags.include_reconfigurator {
201+
selection.insert(BundleData::Reconfigurator);
202+
}
203+
if row.flags.include_sled_cubby_info {
204+
selection.insert(BundleData::SledCubbyInfo);
205+
}
206+
if row.flags.include_sp_dumps {
207+
selection.insert(BundleData::SpDumps);
208+
}
209+
if let Some(host_info) = row.host_info {
210+
selection.insert(host_info.into());
211+
}
212+
if let Some(ereports) = row.ereports {
213+
selection.insert(ereports.try_into()?);
214+
}
215+
Ok(selection)
216+
}
217+
}

nexus/db-model/src/schema_versions.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use std::{collections::BTreeMap, sync::LazyLock};
1616
///
1717
/// This must be updated when you change the database schema. Refer to
1818
/// schema/crdb/README.adoc in the root of this repository for details.
19-
pub const SCHEMA_VERSION: Version = Version::new(248, 0, 0);
19+
pub const SCHEMA_VERSION: Version = Version::new(249, 0, 0);
2020

2121
/// List of all past database schema versions, in *reverse* order
2222
///
@@ -28,6 +28,7 @@ pub static KNOWN_VERSIONS: LazyLock<Vec<KnownVersion>> = LazyLock::new(|| {
2828
// | leaving the first copy as an example for the next person.
2929
// v
3030
// KnownVersion::new(next_int, "unique-dirname-with-the-sql-files"),
31+
KnownVersion::new(249, "fm-support-bundle-request"),
3132
KnownVersion::new(248, "cleanup-orphaned-subnet-pool-silo-links"),
3233
KnownVersion::new(247, "remove-tuf-base-url"),
3334
KnownVersion::new(246, "ereport-marked-seen"),

0 commit comments

Comments
 (0)