Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
77 changes: 69 additions & 8 deletions dev-tools/omdb/src/bin/omdb/nexus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use crate::helpers::ConfirmationPrompt;
use crate::helpers::const_max_len;
use crate::helpers::display_option_blank;
use crate::helpers::should_colorize;
use crate::support_bundle_collect::TimeWindowArgs;
use anyhow::Context as _;
use anyhow::bail;
use camino::Utf8PathBuf;
Expand Down Expand Up @@ -95,6 +96,7 @@ use nexus_types::internal_api::background::TufArtifactReplicationRequest;
use nexus_types::internal_api::background::TufArtifactReplicationStatus;
use nexus_types::internal_api::background::TufRepoPrunerStatus;
use nexus_types::internal_api::background::fm_rendezvous;
use nexus_types::support_bundle::BundleDataCategory;
use omicron_uuid_kinds::BlueprintUuid;
use omicron_uuid_kinds::CollectionUuid;
use omicron_uuid_kinds::DemoSagaUuid;
Expand Down Expand Up @@ -573,7 +575,7 @@ enum SupportBundleCommands {
/// List all support bundles
List,
/// Create a new support bundle
Create,
Create(SupportBundleCreateArgs),
/// Delete a support bundle
Delete(SupportBundleDeleteArgs),
/// Download an entire support bundle
Expand Down Expand Up @@ -664,6 +666,17 @@ impl FromStr for TrustQuorumEpochOrLatest {
}
}

#[derive(Debug, Args)]
struct SupportBundleCreateArgs {
/// Categories of data to collect. May be supplied multiple times.
/// Defaults to all categories.
#[clap(long, value_enum)]
include: Vec<BundleDataCategory>,

#[command(flatten)]
window: TimeWindowArgs,
}

#[derive(Debug, Args)]
struct SupportBundleDeleteArgs {
id: SupportBundleUuid,
Expand Down Expand Up @@ -929,10 +942,10 @@ impl NexusArgs {
command: SupportBundleCommands::List,
}) => cmd_nexus_support_bundles_list(&client).await,
NexusCommands::SupportBundles(SupportBundleArgs {
command: SupportBundleCommands::Create,
command: SupportBundleCommands::Create(args),
}) => {
let token = omdb.check_allow_destructive()?;
cmd_nexus_support_bundles_create(&client, token).await
cmd_nexus_support_bundles_create(&client, args, token).await
}
NexusCommands::SupportBundles(SupportBundleArgs {
command: SupportBundleCommands::Delete(args),
Expand Down Expand Up @@ -5649,14 +5662,62 @@ async fn lookup_sled_by_id(
/// Runs `omdb nexus support-bundles create`
async fn cmd_nexus_support_bundles_create(
client: &nexus_lockstep_client::Client,
args: &SupportBundleCreateArgs,
_destruction_token: DestructiveOperationToken,
) -> Result<(), anyhow::Error> {
use nexus_lockstep_client::types;

// No --include collects everything, matching `support-bundle collect`.
// The API's other reading of an empty selection, "collect nothing",
// cannot be specified by clap, which rejects --include without a value.
let data = if args.include.is_empty() {
types::SupportBundleData::All
} else {
let mut reconfigurator = false;
let mut sled_cubby_info = false;
let mut sp_dumps = false;
let mut host_info = None;
let mut ereports = None;

for category in &args.include {
match category {
BundleDataCategory::Reconfigurator => reconfigurator = true,
BundleDataCategory::SledCubbyInfo => sled_cubby_info = true,
BundleDataCategory::SpDumps => sp_dumps = true,
BundleDataCategory::HostInfo => {
host_info = Some(types::SupportBundleHostInfo {
sleds: types::SupportBundleSledSelection::All,
})
}
BundleDataCategory::Ereports => {
ereports = Some(types::SupportBundleEreports {
only_serials: Vec::new(),
only_classes: Vec::new(),
})
}
}
}

types::SupportBundleData::Explicit {
reconfigurator,
sled_cubby_info,
sp_dumps,
host_info,
ereports,
}
};

let window = args.window.bounds()?;

let support_bundle_id = client
.support_bundle_create(
&nexus_lockstep_client::types::SupportBundleCreate {
user_comment: None,
},
)
.support_bundle_create(&types::SupportBundleCreate {
user_comment: None,
data_selection: Some(types::SupportBundleDataSelection {
data,
start_time: window.start,
end_time: window.end,
}),
})
.await
.context("creating support bundle")?
.into_inner()
Expand Down
58 changes: 42 additions & 16 deletions dev-tools/omdb/src/bin/omdb/support_bundle_collect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ use crate::db::DbUrlOptions;
use anyhow::Context;
use camino::Utf8PathBuf;
use camino_tempfile::tempdir_in;
use chrono::DateTime;
use chrono::Utc;
use clap::Args;
use clap::Subcommand;
use clap::ValueEnum;
Expand Down Expand Up @@ -79,17 +81,8 @@ struct CollectArgs {
#[clap(long, value_enum)]
include: Vec<BundleDataCategory>,

/// Only collect time-bounded data (zone logs, ereports) newer than
/// this age, e.g. "2days" or "12h 30m". Defaults to a 7-day window
/// ending at --until (or now).
#[clap(long, value_parser = humantime::parse_duration)]
since: Option<std::time::Duration>,

/// Only collect time-bounded data (zone logs, ereports) older than
/// this age, e.g. "1h". Must be a smaller age than --since. Log files
/// that span this bound are included in full.
#[clap(long, value_parser = humantime::parse_duration)]
until: Option<std::time::Duration>,
#[command(flatten)]
window: TimeWindowArgs,
}

impl CollectArgs {
Expand All @@ -113,10 +106,43 @@ impl CollectArgs {
};
}

// Both flags are ages relative to now: --since is the oldest data
// to include (the window's start), --until the newest (its end).
// Without --since, `collect` fills in the default lookback,
// anchored to the end bound when one is given.
let window = self.window.bounds()?;
Ok(sel.with_time_range(BundleTimeRange::new(window.start, window.end)?))
}
}

/// The absolute window that [`TimeWindowArgs`] resolves to.
pub struct TimeWindowBounds {
pub start: Option<DateTime<Utc>>,
pub end: Option<DateTime<Utc>>,
}

/// The `--since`/`--until` flags naming the window that bounds
/// time-bounded bundle data.
#[derive(Debug, Args)]
pub struct TimeWindowArgs {
/// Only collect time-bounded data (zone logs, ereports) newer than
/// this age, e.g. "2days" or "12h 30m". Defaults to a 7-day window
/// ending at --until (or now).
#[clap(long, value_parser = humantime::parse_duration)]
since: Option<std::time::Duration>,

/// Only collect time-bounded data (zone logs, ereports) older than
/// this age, e.g. "1h". Must be a smaller age than --since. Log files
/// that span this bound are included in full.
#[clap(long, value_parser = humantime::parse_duration)]
until: Option<std::time::Duration>,
}

impl TimeWindowArgs {
/// Resolves both flags into absolute timestamps.
///
/// Both flags are ages relative to now: --since is the oldest data
/// to include (the window's start), --until the newest (its end).
/// Without --since, the start bound is filled in with the default
/// lookback where the selection enters the system, anchored to the
/// end bound when one is given.
pub fn bounds(&self) -> anyhow::Result<TimeWindowBounds> {
let now = omicron_common::now_db_precision();
let age_to_timestamp = |flag: &str, age: std::time::Duration| {
chrono::Duration::from_std(age)
Expand All @@ -135,7 +161,7 @@ impl CollectArgs {
--until ({end})",
);
}
Ok(sel.with_time_range(BundleTimeRange::new(start, end)?))
Ok(TimeWindowBounds { start, end })
}
}

Expand Down
49 changes: 44 additions & 5 deletions nexus/external-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ api_versions!([
// | date-based version should be at the top of the list.
// v
// (next_yyyy_mm_dd_nn, IDENT),
(2026_08_27_00, SUPPORT_BUNDLE_DATA_SELECTION),
(2026_08_19_01, BGP_PEER_SRC_ADDR),
(2026_08_17_00, SUPPORT_BUNDLES_STABLE),
(2026_08_14_00, ALERT_LIST),
Expand Down Expand Up @@ -8574,16 +8575,36 @@ pub trait NexusExternalApi {
method = GET,
path = "/v1/system/support-bundles/{bundle_id}",
tags = ["system/support-bundles"],
versions = VERSION_SUPPORT_BUNDLES_STABLE..,
versions = VERSION_SUPPORT_BUNDLE_DATA_SELECTION..,
}]
async fn support_bundle_view(
rqctx: RequestContext<Self::Context>,
path_params: Path<latest::support_bundle::SupportBundlePath>,
) -> Result<
HttpResponseOk<latest::support_bundle::SupportBundleInfo>,
HttpResponseOk<latest::support_bundle::SupportBundleView>,
HttpError,
>;

/// View support bundle
#[endpoint {
operation_id = "support_bundle_view",
method = GET,
path = "/v1/system/support-bundles/{bundle_id}",
tags = ["system/support-bundles"],
versions = VERSION_SUPPORT_BUNDLES_STABLE..VERSION_SUPPORT_BUNDLE_DATA_SELECTION,
}]
async fn support_bundle_view_v2026_08_17_00(
rqctx: RequestContext<Self::Context>,
path_params: Path<v2025_11_20_00::support_bundle::SupportBundlePath>,
) -> Result<
HttpResponseOk<v2025_11_20_00::support_bundle::SupportBundleInfo>,
HttpError,
> {
Ok(Self::support_bundle_view(rqctx, path_params)
.await?
.map(v2025_11_20_00::support_bundle::SupportBundleInfo::from))
}

/// View support bundle
#[endpoint {
operation_id = "support_bundle_view",
Expand All @@ -8599,7 +8620,7 @@ pub trait NexusExternalApi {
HttpResponseOk<v2025_11_20_00::support_bundle::SupportBundleInfo>,
HttpError,
> {
Self::support_bundle_view(rqctx, path_params).await
Self::support_bundle_view_v2026_08_17_00(rqctx, path_params).await
}

/// Download support bundle index
Expand Down Expand Up @@ -8756,7 +8777,7 @@ pub trait NexusExternalApi {
method = POST,
path = "/v1/system/support-bundles",
tags = ["system/support-bundles"],
versions = VERSION_SUPPORT_BUNDLES_STABLE..,
versions = VERSION_SUPPORT_BUNDLE_DATA_SELECTION..,
}]
async fn support_bundle_create(
rqctx: RequestContext<Self::Context>,
Expand All @@ -8766,6 +8787,24 @@ pub trait NexusExternalApi {
HttpError,
>;

/// Create support bundle
#[endpoint {
operation_id = "support_bundle_create",
method = POST,
path = "/v1/system/support-bundles",
tags = ["system/support-bundles"],
versions = VERSION_SUPPORT_BUNDLES_STABLE..VERSION_SUPPORT_BUNDLE_DATA_SELECTION,
}]
async fn support_bundle_create_v2026_08_17_00(
rqctx: RequestContext<Self::Context>,
body: TypedBody<v2025_11_20_00::support_bundle::SupportBundleCreate>,
) -> Result<
HttpResponseCreated<v2025_11_20_00::support_bundle::SupportBundleInfo>,
HttpError,
> {
Self::support_bundle_create(rqctx, body.map(Into::into)).await
}

/// Create support bundle
#[endpoint {
operation_id = "support_bundle_create",
Expand All @@ -8781,7 +8820,7 @@ pub trait NexusExternalApi {
HttpResponseCreated<v2025_11_20_00::support_bundle::SupportBundleInfo>,
HttpError,
> {
Self::support_bundle_create(rqctx, body).await
Self::support_bundle_create_v2026_08_17_00(rqctx, body).await
}

/// Delete support bundle
Expand Down
Loading
Loading