Skip to content

Commit 5137bc3

Browse files
support-bundle: expose fm_case_id in internal API and omdb (#10245)
Create an `internal_api::views::SupportBundleInfo`, which wraps the existing external version and adds `fm_case_id`, so we can show the case ID in `omdb nexus support-bundles list` output.
1 parent 62637f7 commit 5137bc3

7 files changed

Lines changed: 141 additions & 23 deletions

File tree

dev-tools/omdb/src/bin/omdb/nexus.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5006,19 +5006,24 @@ async fn cmd_nexus_support_bundles_list(
50065006
struct SupportBundleInfo {
50075007
id: Uuid,
50085008
time_created: DateTime<Utc>,
5009+
state: String,
5010+
fm_case_id: String,
50095011
reason_for_creation: String,
50105012
reason_for_failure: String,
5011-
state: String,
50125013
user_comment: String,
50135014
}
50145015
let rows = support_bundles.into_iter().map(|sb| SupportBundleInfo {
50155016
id: sb.id,
50165017
time_created: sb.time_created,
5018+
state: format!("{:?}", sb.state),
5019+
fm_case_id: sb
5020+
.fm_case_id
5021+
.map(|id| id.to_string())
5022+
.unwrap_or_else(|| "-".to_string()),
50175023
reason_for_creation: sb.reason_for_creation,
50185024
reason_for_failure: sb
50195025
.reason_for_failure
50205026
.unwrap_or_else(|| "-".to_string()),
5021-
state: format!("{:?}", sb.state),
50225027
user_comment: sb.user_comment.unwrap_or_else(|| "-".to_string()),
50235028
});
50245029
let table = tabled::Table::new(rows)

nexus/db-model/src/support_bundle.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use nexus_db_schema::schema::{
1414
use chrono::{DateTime, Utc};
1515
use nexus_types::external_api::support_bundle as support_bundle_types;
1616
use nexus_types::fm::ereport::{EreportFilters, EreportFiltersParams};
17+
use nexus_types::internal_api::views as internal_views;
1718
use nexus_types::support_bundle::BundleData;
1819
use nexus_types::support_bundle::SledSelection;
1920
use omicron_uuid_kinds::CaseKind;
@@ -152,6 +153,14 @@ impl From<SupportBundle> for support_bundle_types::SupportBundleInfo {
152153
}
153154
}
154155

156+
impl From<SupportBundle> for internal_views::SupportBundleInfo {
157+
fn from(bundle: SupportBundle) -> Self {
158+
let fm_case_id = bundle.fm_case_id.map(Into::into);
159+
let base: support_bundle_types::SupportBundleInfo = bundle.into();
160+
Self { base, fm_case_id }
161+
}
162+
}
163+
155164
// --- Data selection tables owned by support_bundle ---
156165

157166
/// Flags table row — tracks which payload-less data categories are selected.

nexus/lockstep-api/src/lib.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ use nexus_types::internal_api::views::DemoSaga;
3838
use nexus_types::internal_api::views::MgsUpdateDriverStatus;
3939
use nexus_types::internal_api::views::QuiesceStatus;
4040
use nexus_types::internal_api::views::Saga;
41+
use nexus_types::internal_api::views::SupportBundleInfo;
4142
use nexus_types::internal_api::views::UpdateStatus;
4243
use nexus_types::trust_quorum::TrustQuorumConfig;
4344
use nexus_types_versions::latest::headers::RangeRequest;
@@ -385,10 +386,7 @@ pub trait NexusLockstepApi {
385386
async fn support_bundle_list(
386387
rqctx: RequestContext<Self::Context>,
387388
query_params: Query<PaginatedByTimeAndId>,
388-
) -> Result<
389-
HttpResponseOk<ResultsPage<support_bundle::SupportBundleInfo>>,
390-
HttpError,
391-
>;
389+
) -> Result<HttpResponseOk<ResultsPage<SupportBundleInfo>>, HttpError>;
392390

393391
/// View a support bundle
394392
#[endpoint {
@@ -398,7 +396,7 @@ pub trait NexusLockstepApi {
398396
async fn support_bundle_view(
399397
rqctx: RequestContext<Self::Context>,
400398
path_params: Path<support_bundle::SupportBundlePath>,
401-
) -> Result<HttpResponseOk<support_bundle::SupportBundleInfo>, HttpError>;
399+
) -> Result<HttpResponseOk<SupportBundleInfo>, HttpError>;
402400

403401
/// Download the index of a support bundle
404402
#[endpoint {
@@ -463,7 +461,7 @@ pub trait NexusLockstepApi {
463461
async fn support_bundle_create(
464462
rqctx: RequestContext<Self::Context>,
465463
body: TypedBody<support_bundle::SupportBundleCreate>,
466-
) -> Result<HttpResponseCreated<support_bundle::SupportBundleInfo>, HttpError>;
464+
) -> Result<HttpResponseCreated<SupportBundleInfo>, HttpError>;
467465

468466
/// Delete an existing support bundle
469467
///
@@ -487,7 +485,7 @@ pub trait NexusLockstepApi {
487485
rqctx: RequestContext<Self::Context>,
488486
path_params: Path<support_bundle::SupportBundlePath>,
489487
body: TypedBody<support_bundle::SupportBundleUpdate>,
490-
) -> Result<HttpResponseOk<support_bundle::SupportBundleInfo>, HttpError>;
488+
) -> Result<HttpResponseOk<SupportBundleInfo>, HttpError>;
491489

492490
/// Get the current clickhouse policy
493491
#[endpoint {

nexus/src/lockstep_api/http_entrypoints.rs

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ use nexus_types::external_api::path_params::{BlueprintPath, PhysicalDiskPath};
3838
use nexus_types::external_api::rack::RackMembershipConfigPathParams;
3939
use nexus_types::external_api::sled::{SledPolicy, SledSelector};
4040
use nexus_types::external_api::support_bundle::{
41-
self, SupportBundleFilePath, SupportBundlePath, SupportBundleUpdate,
41+
SupportBundleCreate, SupportBundleFilePath, SupportBundlePath,
42+
SupportBundleUpdate,
4243
};
4344
use nexus_types::internal_api::params::InstanceMigrateRequest;
4445
use nexus_types::internal_api::params::RackInitializationRequest;
@@ -47,6 +48,7 @@ use nexus_types::internal_api::views::DemoSaga;
4748
use nexus_types::internal_api::views::MgsUpdateDriverStatus;
4849
use nexus_types::internal_api::views::QuiesceStatus;
4950
use nexus_types::internal_api::views::Saga;
51+
use nexus_types::internal_api::views::SupportBundleInfo;
5052
use nexus_types::internal_api::views::UpdateStatus;
5153
use nexus_types::internal_api::views::to_list;
5254
use nexus_types::trust_quorum::TrustQuorumConfig;
@@ -618,10 +620,7 @@ impl NexusLockstepApi for NexusLockstepApiImpl {
618620
async fn support_bundle_list(
619621
rqctx: RequestContext<ApiContext>,
620622
query_params: Query<PaginatedByTimeAndId>,
621-
) -> Result<
622-
HttpResponseOk<ResultsPage<support_bundle::SupportBundleInfo>>,
623-
HttpError,
624-
> {
623+
) -> Result<HttpResponseOk<ResultsPage<SupportBundleInfo>>, HttpError> {
625624
let apictx = rqctx.context();
626625
let handler = async {
627626
let nexus = &apictx.context.nexus;
@@ -642,8 +641,11 @@ impl NexusLockstepApi for NexusLockstepApiImpl {
642641
Ok(HttpResponseOk(ScanByTimeAndId::results_page(
643642
&query,
644643
bundles,
645-
&|_, bundle: &support_bundle::SupportBundleInfo| {
646-
(bundle.time_created, bundle.id.into_untyped_uuid())
644+
&|_, bundle: &SupportBundleInfo| {
645+
(
646+
bundle.base.time_created,
647+
bundle.base.id.into_untyped_uuid(),
648+
)
647649
},
648650
)?))
649651
};
@@ -657,8 +659,7 @@ impl NexusLockstepApi for NexusLockstepApiImpl {
657659
async fn support_bundle_view(
658660
rqctx: RequestContext<Self::Context>,
659661
path_params: Path<SupportBundlePath>,
660-
) -> Result<HttpResponseOk<support_bundle::SupportBundleInfo>, HttpError>
661-
{
662+
) -> Result<HttpResponseOk<SupportBundleInfo>, HttpError> {
662663
let apictx = rqctx.context();
663664
let handler = async {
664665
let nexus = &apictx.context.nexus;
@@ -862,9 +863,8 @@ impl NexusLockstepApi for NexusLockstepApiImpl {
862863

863864
async fn support_bundle_create(
864865
rqctx: RequestContext<Self::Context>,
865-
body: TypedBody<support_bundle::SupportBundleCreate>,
866-
) -> Result<HttpResponseCreated<support_bundle::SupportBundleInfo>, HttpError>
867-
{
866+
body: TypedBody<SupportBundleCreate>,
867+
) -> Result<HttpResponseCreated<SupportBundleInfo>, HttpError> {
868868
let apictx = rqctx.context();
869869
let handler = async {
870870
let nexus = &apictx.context.nexus;
@@ -921,8 +921,7 @@ impl NexusLockstepApi for NexusLockstepApiImpl {
921921
rqctx: RequestContext<Self::Context>,
922922
path_params: Path<SupportBundlePath>,
923923
body: TypedBody<SupportBundleUpdate>,
924-
) -> Result<HttpResponseOk<support_bundle::SupportBundleInfo>, HttpError>
925-
{
924+
) -> Result<HttpResponseOk<SupportBundleInfo>, HttpError> {
926925
let apictx = rqctx.context();
927926
let handler = async {
928927
let nexus = &apictx.context.nexus;

nexus/tests/integration_tests/support_bundles.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use anyhow::Context;
88
use anyhow::Result;
99
use dropshot::HttpErrorResponseBody;
1010
use dropshot::test_util::ClientTestContext;
11+
use futures::TryStreamExt;
1112
use http::StatusCode;
1213
use http::method::Method;
1314
use nexus_db_model::SupportBundleState as DbSupportBundleState;
@@ -939,3 +940,76 @@ async fn test_support_bundle_delete_failed_bundle(
939940
"Deleted bundle should not appear in bundle list"
940941
);
941942
}
943+
944+
// Test that fm_case_id is exposed through the lockstep API. User-created
945+
// bundles should have fm_case_id: None, while FM-created bundles should
946+
// show their case ID.
947+
#[nexus_test]
948+
async fn test_support_bundle_fm_case_id(cptestctx: &ControlPlaneTestContext) {
949+
use nexus_db_queries::db::datastore::SupportBundleCreateParams;
950+
use nexus_db_queries::db::datastore::SupportBundleProvenance;
951+
use nexus_types::support_bundle::BundleDataSelection;
952+
use omicron_uuid_kinds::CaseUuid;
953+
use omicron_uuid_kinds::GenericUuid;
954+
955+
let client = &cptestctx.external_client;
956+
let nexus = &cptestctx.server.server_context().nexus;
957+
let datastore = nexus.datastore();
958+
let opctx =
959+
OpContext::for_tests(cptestctx.logctx.log.clone(), datastore.clone());
960+
let lockstep = cptestctx.lockstep_client();
961+
962+
let _disk_test =
963+
DiskTestBuilder::new(&cptestctx).with_zpool_count(2).build().await;
964+
965+
// Create a user bundle through the external API.
966+
let user_bundle = bundle_create(&client).await.unwrap();
967+
968+
// Create an FM bundle directly through the datastore.
969+
let case_id = CaseUuid::new_v4();
970+
let fm_bundle = datastore
971+
.support_bundle_create(
972+
&opctx,
973+
SupportBundleCreateParams {
974+
provenance: SupportBundleProvenance::Fm {
975+
id: SupportBundleUuid::new_v4(),
976+
case_id,
977+
},
978+
reason: "FM test bundle",
979+
nexus_id: nexus.id(),
980+
user_comment: None,
981+
data_selection: BundleDataSelection::all(),
982+
},
983+
)
984+
.await
985+
.expect("Should be able to create FM bundle");
986+
987+
// Fetch all bundles via the lockstep API and verify fm_case_id.
988+
let bundles: Vec<_> = lockstep
989+
.support_bundle_list_stream(None, None)
990+
.try_collect()
991+
.await
992+
.expect("listing bundles via lockstep API");
993+
994+
let user_bundle_uuid: uuid::Uuid = user_bundle.id.into_untyped_uuid();
995+
let listed_user = bundles
996+
.iter()
997+
.find(|b| b.id == user_bundle_uuid)
998+
.expect("user bundle should appear in list");
999+
assert_eq!(listed_user.fm_case_id, None);
1000+
1001+
let fm_bundle_uuid: uuid::Uuid = *fm_bundle.id.as_untyped_uuid();
1002+
let listed_fm = bundles
1003+
.iter()
1004+
.find(|b| b.id == fm_bundle_uuid)
1005+
.expect("FM bundle should appear in list");
1006+
assert_eq!(listed_fm.fm_case_id, Some(case_id));
1007+
1008+
// Verify fm_case_id via the lockstep API get endpoint.
1009+
let fetched_fm = lockstep
1010+
.support_bundle_view(&fm_bundle_uuid)
1011+
.await
1012+
.expect("fetching FM bundle via lockstep API")
1013+
.into_inner();
1014+
assert_eq!(fetched_fm.fm_case_id, Some(case_id));
1015+
}

nexus/types/src/internal_api/views.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use crate::deployment::PendingMgsUpdate;
66
use crate::deployment::TargetReleaseDescription;
7+
use crate::external_api::support_bundle::SupportBundleInfo as ExternalSupportBundleInfo;
78
use crate::inventory::CabooseWhich;
89
use crate::inventory::Collection;
910
use crate::quiesce::SagaQuiesceStatus;
@@ -24,6 +25,7 @@ use omicron_common::api::external::Vni;
2425
use omicron_common::disk::M2Slot;
2526
use omicron_common::snake_case_result;
2627
use omicron_common::snake_case_result::SnakeCaseResult;
28+
use omicron_uuid_kinds::CaseUuid;
2729
use omicron_uuid_kinds::DemoSagaUuid;
2830
use omicron_uuid_kinds::{OmicronZoneUuid, SledUuid};
2931
use schemars::JsonSchema;
@@ -1249,6 +1251,18 @@ impl IdOrdItem for HeldDbClaimInfo {
12491251
id_upcast!();
12501252
}
12511253

1254+
/// Support bundle info with internal-only fields.
1255+
///
1256+
/// This wraps the external API's [`ExternalSupportBundleInfo`] and extends it
1257+
/// with fields that are only exposed through the lockstep API (e.g., for OMDB).
1258+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1259+
pub struct SupportBundleInfo {
1260+
#[serde(flatten)]
1261+
pub base: ExternalSupportBundleInfo,
1262+
/// The FM case ID that triggered creation of this bundle, if any.
1263+
pub fm_case_id: Option<CaseUuid>,
1264+
}
1265+
12521266
#[cfg(test)]
12531267
mod test {
12541268
use super::CompletedAttempt;

openapi/nexus-lockstep.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3291,6 +3291,15 @@
32913291
"stage0_next"
32923292
]
32933293
},
3294+
"CaseUuid": {
3295+
"x-rust-type": {
3296+
"crate": "omicron-uuid-kinds",
3297+
"path": "omicron_uuid_kinds::CaseUuid",
3298+
"version": "*"
3299+
},
3300+
"type": "string",
3301+
"format": "uuid"
3302+
},
32943303
"Certificate": {
32953304
"type": "object",
32963305
"properties": {
@@ -9166,8 +9175,18 @@
91669175
}
91679176
},
91689177
"SupportBundleInfo": {
9178+
"description": "Support bundle info with internal-only fields.\n\nThis wraps the external API's [`ExternalSupportBundleInfo`] and extends it with fields that are only exposed through the lockstep API (e.g., for OMDB).",
91699179
"type": "object",
91709180
"properties": {
9181+
"fm_case_id": {
9182+
"nullable": true,
9183+
"description": "The FM case ID that triggered creation of this bundle, if any.",
9184+
"allOf": [
9185+
{
9186+
"$ref": "#/components/schemas/CaseUuid"
9187+
}
9188+
]
9189+
},
91719190
"id": {
91729191
"type": "string",
91739192
"format": "uuid"

0 commit comments

Comments
 (0)