Skip to content

Commit 0c999b9

Browse files
committed
schema + db-model: support bundle request data selection tables
Adds fm_support_bundle_request and fm_sb_req_data_selection tables for persisting FM-initiated support bundle requests as part of a sitrep. Each request belongs to a case and tracks which sitrep originally requested it. The data selection table uses a bundle_data_category enum and category-specific nullable columns with CHECK constraints to store per-category parameters (sled selection for host_info, time/serial/class filters for ereports).
1 parent c171063 commit 0c999b9

14 files changed

Lines changed: 397 additions & 3 deletions

File tree

nexus/db-model/src/fm.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ mod case;
2525
pub use case::*;
2626
mod diagnosis_engine;
2727
pub use diagnosis_engine::*;
28+
mod support_bundle_request;
29+
pub use support_bundle_request::*;
2830

2931
#[derive(Queryable, Insertable, Clone, Debug, Selectable)]
3032
#[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};
@@ -139,4 +140,5 @@ pub struct Case {
139140
pub metadata: CaseMetadata,
140141
pub ereports: Vec<CaseEreport>,
141142
pub alerts_requested: Vec<AlertRequest>,
143+
pub support_bundles_requested: Vec<SupportBundleRequest>,
142144
}
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
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 crate::impl_enum_type;
9+
use chrono::{DateTime, Utc};
10+
use nexus_db_schema::schema::{
11+
fm_sb_req_data_selection, fm_support_bundle_request,
12+
};
13+
use nexus_types::fm;
14+
use nexus_types::fm::ereport::EreportFilters;
15+
use nexus_types::support_bundle as support_bundle_types;
16+
use nexus_types::support_bundle::{
17+
BundleData, BundleDataSelection, SledSelection,
18+
};
19+
use omicron_uuid_kinds::{
20+
CaseKind, GenericUuid, SitrepKind, SledUuid, SupportBundleKind,
21+
};
22+
use serde::{Deserialize, Serialize};
23+
24+
impl_enum_type!(
25+
BundleDataCategoryEnum:
26+
27+
#[derive(
28+
Copy,
29+
Clone,
30+
Debug,
31+
PartialEq,
32+
Serialize,
33+
Deserialize,
34+
AsExpression,
35+
FromSqlRow,
36+
)]
37+
pub enum BundleDataCategory;
38+
39+
Reconfigurator => b"reconfigurator"
40+
HostInfo => b"host_info"
41+
SledCubbyInfo => b"sled_cubby_info"
42+
SpDumps => b"sp_dumps"
43+
Ereports => b"ereports"
44+
);
45+
46+
impl From<&support_bundle_types::BundleData> for BundleDataCategory {
47+
fn from(data: &support_bundle_types::BundleData) -> Self {
48+
match data {
49+
support_bundle_types::BundleData::Reconfigurator => {
50+
BundleDataCategory::Reconfigurator
51+
}
52+
support_bundle_types::BundleData::HostInfo(_) => {
53+
BundleDataCategory::HostInfo
54+
}
55+
support_bundle_types::BundleData::SledCubbyInfo => {
56+
BundleDataCategory::SledCubbyInfo
57+
}
58+
support_bundle_types::BundleData::SpDumps => {
59+
BundleDataCategory::SpDumps
60+
}
61+
support_bundle_types::BundleData::Ereports(_) => {
62+
BundleDataCategory::Ereports
63+
}
64+
}
65+
}
66+
}
67+
68+
// --- SupportBundleRequest (parent row) ---
69+
70+
#[derive(Queryable, Insertable, Clone, Debug, Selectable)]
71+
#[diesel(table_name = fm_support_bundle_request)]
72+
pub struct SupportBundleRequest {
73+
pub id: DbTypedUuid<SupportBundleKind>,
74+
pub sitrep_id: DbTypedUuid<SitrepKind>,
75+
pub requested_sitrep_id: DbTypedUuid<SitrepKind>,
76+
pub case_id: DbTypedUuid<CaseKind>,
77+
}
78+
79+
impl SupportBundleRequest {
80+
pub fn from_sitrep(
81+
sitrep_id: impl Into<DbTypedUuid<SitrepKind>>,
82+
case_id: impl Into<DbTypedUuid<CaseKind>>,
83+
req: fm::case::SupportBundleRequest,
84+
) -> Self {
85+
let fm::case::SupportBundleRequest {
86+
id,
87+
requested_sitrep_id,
88+
data_selection: _,
89+
} = req;
90+
SupportBundleRequest {
91+
id: id.into(),
92+
sitrep_id: sitrep_id.into(),
93+
requested_sitrep_id: requested_sitrep_id.into(),
94+
case_id: case_id.into(),
95+
}
96+
}
97+
}
98+
99+
// --- Data selection rows ---
100+
101+
#[derive(Queryable, Insertable, Clone, Debug, Selectable)]
102+
#[diesel(table_name = fm_sb_req_data_selection)]
103+
pub struct SbReqDataSelection {
104+
pub sitrep_id: DbTypedUuid<SitrepKind>,
105+
pub request_id: DbTypedUuid<SupportBundleKind>,
106+
pub category: BundleDataCategory,
107+
// HostInfo fields
108+
pub all_sleds: Option<bool>,
109+
pub sled_ids: Option<Vec<uuid::Uuid>>,
110+
// Ereports fields
111+
pub ereport_start_time: Option<DateTime<Utc>>,
112+
pub ereport_end_time: Option<DateTime<Utc>>,
113+
pub ereport_only_serials: Option<Vec<String>>,
114+
pub ereport_only_classes: Option<Vec<String>>,
115+
}
116+
117+
impl SbReqDataSelection {
118+
/// Create rows from a `BundleDataSelection` for a given request.
119+
pub fn from_data_selection(
120+
sitrep_id: impl Into<DbTypedUuid<SitrepKind>> + Copy,
121+
request_id: impl Into<DbTypedUuid<SupportBundleKind>> + Copy,
122+
selection: &BundleDataSelection,
123+
) -> Vec<Self> {
124+
selection
125+
.iter()
126+
.map(|data| Self::from_bundle_data(sitrep_id, request_id, data))
127+
.collect()
128+
}
129+
130+
fn from_bundle_data(
131+
sitrep_id: impl Into<DbTypedUuid<SitrepKind>>,
132+
request_id: impl Into<DbTypedUuid<SupportBundleKind>>,
133+
data: &BundleData,
134+
) -> Self {
135+
let sitrep_id = sitrep_id.into();
136+
let request_id = request_id.into();
137+
let base = Self {
138+
sitrep_id,
139+
request_id,
140+
category: BundleDataCategory::from(data),
141+
all_sleds: None,
142+
sled_ids: None,
143+
ereport_start_time: None,
144+
ereport_end_time: None,
145+
ereport_only_serials: None,
146+
ereport_only_classes: None,
147+
};
148+
match data {
149+
BundleData::Reconfigurator
150+
| BundleData::SledCubbyInfo
151+
| BundleData::SpDumps => base,
152+
BundleData::HostInfo(sled_selection) => match sled_selection {
153+
SledSelection::All => Self {
154+
all_sleds: Some(true),
155+
sled_ids: Some(vec![]),
156+
..base
157+
},
158+
SledSelection::Specific(set) => Self {
159+
all_sleds: Some(false),
160+
sled_ids: Some(
161+
set.iter().map(|id| id.into_untyped_uuid()).collect(),
162+
),
163+
..base
164+
},
165+
},
166+
BundleData::Ereports(filters) => Self {
167+
ereport_start_time: filters.start_time(),
168+
ereport_end_time: filters.end_time(),
169+
ereport_only_serials: Some(filters.only_serials().to_vec()),
170+
ereport_only_classes: Some(filters.only_classes().to_vec()),
171+
..base
172+
},
173+
}
174+
}
175+
176+
/// Convert a set of data selection rows back into a `BundleDataSelection`.
177+
/// Empty input produces the default selection (collect everything).
178+
pub fn into_data_selection(rows: Vec<Self>) -> BundleDataSelection {
179+
rows.into_iter().map(Self::into_bundle_data).collect()
180+
}
181+
182+
fn into_bundle_data(self) -> BundleData {
183+
match self.category {
184+
BundleDataCategory::Reconfigurator => BundleData::Reconfigurator,
185+
BundleDataCategory::SledCubbyInfo => BundleData::SledCubbyInfo,
186+
BundleDataCategory::SpDumps => BundleData::SpDumps,
187+
BundleDataCategory::HostInfo => {
188+
if self.all_sleds.expect(
189+
"CHECK constraint guarantees all_sleds IS NOT NULL \
190+
for host_info rows",
191+
) {
192+
BundleData::HostInfo(SledSelection::All)
193+
} else {
194+
let ids = self.sled_ids.expect(
195+
"CHECK constraint guarantees sled_ids IS NOT NULL \
196+
for host_info rows",
197+
);
198+
BundleData::HostInfo(SledSelection::Specific(
199+
ids.into_iter()
200+
.map(SledUuid::from_untyped_uuid)
201+
.collect(),
202+
))
203+
}
204+
}
205+
BundleDataCategory::Ereports => {
206+
let mut filters = EreportFilters::new();
207+
if let Some(t) = self.ereport_start_time {
208+
filters = filters.with_start_time(t).expect(
209+
"CHECK constraint guarantees start_time <= end_time",
210+
);
211+
}
212+
if let Some(t) = self.ereport_end_time {
213+
filters = filters.with_end_time(t).expect(
214+
"CHECK constraint guarantees start_time <= end_time",
215+
);
216+
}
217+
filters =
218+
filters.with_serials(self.ereport_only_serials.expect(
219+
"CHECK constraint guarantees ereport_only_serials \
220+
IS NOT NULL for ereports rows",
221+
));
222+
filters =
223+
filters.with_classes(self.ereport_only_classes.expect(
224+
"CHECK constraint guarantees ereport_only_classes \
225+
IS NOT NULL for ereports rows",
226+
));
227+
BundleData::Ereports(filters)
228+
}
229+
}
230+
}
231+
}

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(241, 0, 0);
19+
pub const SCHEMA_VERSION: Version = Version::new(242, 0, 0);
2020

2121
/// List of all past database schema versions, in *reverse* order
2222
///
@@ -28,6 +28,7 @@ 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(242, "fm-support-bundle-request"),
3132
KnownVersion::new(241, "audit-log-incomplete-timeout"),
3233
KnownVersion::new(240, "multicast-drop-mvlan"),
3334
KnownVersion::new(239, "fm-alert-request"),

nexus/db-model/src/support_bundle.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use nexus_db_schema::schema::support_bundle;
88

99
use chrono::{DateTime, Utc};
1010
use nexus_types::external_api::support_bundle as support_bundle_types;
11+
use omicron_uuid_kinds::CaseKind;
1112
use omicron_uuid_kinds::DatasetKind;
1213
use omicron_uuid_kinds::DatasetUuid;
1314
use omicron_uuid_kinds::OmicronZoneKind;
@@ -95,6 +96,7 @@ pub struct SupportBundle {
9596
pub dataset_id: DbTypedUuid<DatasetKind>,
9697
pub assigned_nexus: Option<DbTypedUuid<OmicronZoneKind>>,
9798
pub user_comment: Option<String>,
99+
pub fm_case_id: Option<DbTypedUuid<CaseKind>>,
98100
}
99101

100102
impl SupportBundle {
@@ -115,6 +117,7 @@ impl SupportBundle {
115117
dataset_id: dataset_id.into(),
116118
assigned_nexus: Some(nexus_id.into()),
117119
user_comment,
120+
fm_case_id: None,
118121
}
119122
}
120123

nexus/db-schema/src/enums.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,11 @@ define_enums! {
4343
BlockSizeEnum => "block_size",
4444
BpDatasetDispositionEnum => "bp_dataset_disposition",
4545
BpPhysicalDiskDispositionEnum => "bp_physical_disk_disposition",
46-
BpSourceEnum => "bp_source",
4746
BpSledMeasurementsEnum => "bp_sled_measurements",
47+
BpSourceEnum => "bp_source",
4848
BpZoneDispositionEnum => "bp_zone_disposition",
4949
BpZoneImageSourceEnum => "bp_zone_image_source",
50+
BundleDataCategoryEnum => "bundle_data_category",
5051
CabooseWhichEnum => "caboose_which",
5152
ClickhouseModeEnum => "clickhouse_mode",
5253
DatasetKindEnum => "dataset_kind",

nexus/db-schema/src/schema.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1590,6 +1590,7 @@ table! {
15901590

15911591
assigned_nexus -> Nullable<Uuid>,
15921592
user_comment -> Nullable<Text>,
1593+
fm_case_id -> Nullable<Uuid>,
15931594
}
15941595
}
15951596

@@ -3183,6 +3184,41 @@ table! {
31833184
}
31843185
}
31853186

3187+
// FM support bundle requests, stored per-sitrep like alert requests.
3188+
table! {
3189+
fm_support_bundle_request (sitrep_id, id) {
3190+
id -> Uuid,
3191+
sitrep_id -> Uuid,
3192+
requested_sitrep_id -> Uuid,
3193+
case_id -> Uuid,
3194+
}
3195+
}
3196+
3197+
// Per-category data selection for support bundle requests.
3198+
// One row per selected BundleData category. Row existence means
3199+
// "include this category in the bundle." No rows for a request means
3200+
// "collect everything."
3201+
table! {
3202+
fm_sb_req_data_selection (sitrep_id, request_id, category) {
3203+
sitrep_id -> Uuid,
3204+
request_id -> Uuid,
3205+
category -> crate::enums::BundleDataCategoryEnum,
3206+
// HostInfo fields (non-null iff category = 'host_info')
3207+
all_sleds -> Nullable<Bool>,
3208+
sled_ids -> Nullable<Array<Uuid>>,
3209+
// Ereports fields (non-null iff category = 'ereports')
3210+
ereport_start_time -> Nullable<Timestamptz>,
3211+
ereport_end_time -> Nullable<Timestamptz>,
3212+
ereport_only_serials -> Nullable<Array<Text>>,
3213+
ereport_only_classes -> Nullable<Array<Text>>,
3214+
}
3215+
}
3216+
3217+
allow_tables_to_appear_in_same_query!(
3218+
fm_support_bundle_request,
3219+
fm_sb_req_data_selection,
3220+
);
3221+
31863222
table! {
31873223
trust_quorum_configuration (rack_id, epoch) {
31883224
rack_id -> Uuid,

0 commit comments

Comments
 (0)