Skip to content

Move Support Bundle Inspector logic #8122

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 12, 2025
Merged
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
12 changes: 5 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ members = [
"dev-tools/releng",
"dev-tools/repl-utils",
"dev-tools/repo-depot-standalone",
"dev-tools/support-bundle-reader-lib",
"dev-tools/xtask",
"dns-server",
"dns-server-api",
Expand Down Expand Up @@ -201,7 +200,6 @@ default-members = [
"dev-tools/releng",
"dev-tools/repl-utils",
"dev-tools/repo-depot-standalone",
"dev-tools/support-bundle-reader-lib",
# Do not include xtask in the list of default members, because this causes
# hakari to not work as well and build times to be longer.
# See omicron#4392.
Expand Down Expand Up @@ -697,7 +695,7 @@ strum = { version = "0.26", features = [ "derive" ] }
subprocess = "0.2.9"
subtle = "2.6.1"
supports-color = "3.0.2"
support-bundle-reader-lib = { path = "dev-tools/support-bundle-reader-lib" }
support-bundle-viewer = "0.1.0"
swrite = "0.1.0"
sync-ptr = "0.1.1"
libsw = { version = "3.4.0", features = ["tokio"] }
Expand Down
3 changes: 2 additions & 1 deletion dev-tools/omdb/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ omicron-rpaths.workspace = true
[dependencies]
anyhow.workspace = true
async-bb8-diesel.workspace = true
async-trait.workspace = true
bytes.workspace = true
camino.workspace = true
chrono.workspace = true
Expand Down Expand Up @@ -59,7 +60,7 @@ slog.workspace = true
slog-error-chain.workspace = true
steno.workspace = true
strum.workspace = true
support-bundle-reader-lib.workspace = true
support-bundle-viewer.workspace = true
supports-color.workspace = true
tabled.workspace = true
textwrap.workspace = true
Expand Down
1 change: 1 addition & 0 deletions dev-tools/omdb/src/bin/omdb/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ mod oximeter;
mod oxql;
mod reconfigurator;
mod sled_agent;
mod support_bundle;

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
Expand Down
22 changes: 15 additions & 7 deletions dev-tools/omdb/src/bin/omdb/nexus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::helpers::ConfirmationPrompt;
use crate::helpers::const_max_len;
use crate::helpers::display_option_blank;
use crate::helpers::should_colorize;
use anyhow::Context;
use anyhow::Context as _;
use anyhow::bail;
use camino::Utf8PathBuf;
use chrono::DateTime;
Expand Down Expand Up @@ -78,6 +78,8 @@ use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::str::FromStr;
use std::sync::Arc;
use support_bundle_viewer::LocalFileAccess;
use support_bundle_viewer::SupportBundleAccessor;
use tabled::Tabled;
use tabled::settings::Padding;
use tabled::settings::object::Columns;
Expand Down Expand Up @@ -3945,10 +3947,16 @@ async fn cmd_nexus_support_bundles_inspect(
client: &nexus_client::Client,
args: &SupportBundleInspectArgs,
) -> Result<(), anyhow::Error> {
support_bundle_reader_lib::run_dashboard(
client,
args.id,
args.path.as_ref(),
)
.await
let accessor: Box<dyn SupportBundleAccessor> = match (args.id, &args.path) {
(None, Some(path)) => Box::new(LocalFileAccess::new(path)?),
(maybe_id, None) => Box::new(
crate::support_bundle::access_bundle_from_id(client, maybe_id)
.await?,
),
(Some(_), Some(_)) => {
bail!("Cannot specify both UUID and path");
}
};

support_bundle_viewer::run_dashboard(accessor).await
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,34 @@
// 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/.

//! APIs to help access bundles
//! Utilities to access support bundles via the internal API

use crate::index::SupportBundleIndex;
use anyhow::Context as _;
use anyhow::Result;
use anyhow::bail;
use async_trait::async_trait;
use bytes::Buf;
use bytes::Bytes;
use camino::Utf8Path;
use camino::Utf8PathBuf;
use futures::Stream;
use futures::StreamExt;
use futures::TryStreamExt;
use nexus_client::types::SupportBundleInfo;
use nexus_client::types::SupportBundleState;
use omicron_uuid_kinds::GenericUuid;
use omicron_uuid_kinds::SupportBundleUuid;
use std::io;
use std::io::Write;
use std::pin::Pin;
use std::task::Context;
use std::task::Poll;
use std::time::Duration;
use support_bundle_viewer::BoxedFileAccessor;
use support_bundle_viewer::SupportBundleAccessor;
use support_bundle_viewer::SupportBundleIndex;
use tokio::io::AsyncRead;
use tokio::io::ReadBuf;

/// An I/O source which can read to a buffer
///
/// This describes access to individual files within the bundle.
pub trait FileAccessor: AsyncRead + Unpin {}
impl<T: AsyncRead + Unpin + ?Sized> FileAccessor for T {}

pub type BoxedFileAccessor<'a> = Box<dyn FileAccessor + 'a>;

/// Describes how the support bundle's data and metadata are accessed.
#[async_trait]
pub trait SupportBundleAccessor {
/// Access the index of a support bundle
async fn get_index(&self) -> Result<SupportBundleIndex>;

/// Access a file within the support bundle
async fn get_file<'a>(
&mut self,
path: &Utf8Path,
) -> Result<BoxedFileAccessor<'a>>
where
Self: 'a;
}

pub struct StreamedFile<'a> {
client: &'a nexus_client::Client,
id: SupportBundleUuid,
Expand All @@ -67,7 +51,7 @@ impl<'a> StreamedFile<'a> {
// use range requests to stream out portions of the file.
//
// This means that we would potentially want to restart the stream with a different position.
async fn start_stream(&mut self) -> Result<()> {
async fn start_stream(&mut self) -> anyhow::Result<()> {
// TODO: Add range headers, for range requests? Though this
// will require adding support to Progenitor + Nexus too.
let stream = self
Expand Down Expand Up @@ -140,10 +124,22 @@ impl<'a> InternalApiAccess<'a> {
}
}

async fn utf8_stream_to_string(
mut stream: impl futures::Stream<Item = reqwest::Result<bytes::Bytes>>
+ std::marker::Unpin,
) -> anyhow::Result<String> {
let mut bytes = Vec::new();
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
bytes.extend_from_slice(&chunk);
}
Ok(String::from_utf8(bytes)?)
}

// Access for: The nexus internal API
#[async_trait]
impl<'c> SupportBundleAccessor for InternalApiAccess<'c> {
async fn get_index(&self) -> Result<SupportBundleIndex> {
async fn get_index(&self) -> anyhow::Result<SupportBundleIndex> {
let stream = self
.client
.support_bundle_index(self.id.as_untyped_uuid())
Expand All @@ -160,7 +156,7 @@ impl<'c> SupportBundleAccessor for InternalApiAccess<'c> {
async fn get_file<'a>(
&mut self,
path: &Utf8Path,
) -> Result<BoxedFileAccessor<'a>>
) -> anyhow::Result<BoxedFileAccessor<'a>>
where
'c: 'a,
{
Expand All @@ -173,71 +169,105 @@ impl<'c> SupportBundleAccessor for InternalApiAccess<'c> {
}
}

pub struct LocalFileAccess {
archive: zip::read::ZipArchive<std::fs::File>,
}
async fn wait_for_bundle_to_be_collected(
client: &nexus_client::Client,
id: SupportBundleUuid,
) -> Result<SupportBundleInfo, anyhow::Error> {
let mut printed_wait_msg = false;
loop {
let sb = client
.support_bundle_view(id.as_untyped_uuid())
.await
.with_context(|| {
format!("failed to query for support bundle {}", id)
})?;

impl LocalFileAccess {
pub fn new(path: &Utf8Path) -> Result<Self> {
let file = std::fs::File::open(path)?;
Ok(Self { archive: zip::read::ZipArchive::new(file)? })
match sb.state {
SupportBundleState::Active => {
if printed_wait_msg {
eprintln!("");
}
return Ok(sb.into_inner());
}
SupportBundleState::Collecting => {
if !printed_wait_msg {
eprint!("Waiting for {} to finish collection...", id);
printed_wait_msg = true;
}
tokio::time::sleep(Duration::from_secs(1)).await;
eprint!(".");
std::io::stderr().flush().context("cannot flush stderr")?;
}
other => bail!("Unexepcted state: {other}"),
}
}
}

// Access for: Local zip files
#[async_trait]
impl SupportBundleAccessor for LocalFileAccess {
async fn get_index(&self) -> Result<SupportBundleIndex> {
let names: Vec<&str> = self.archive.file_names().collect();
let all_names = names.join("\n");
Ok(SupportBundleIndex::new(&all_names))
}
/// Returns either a specific bundle or the latest active bundle.
///
/// If a bundle is being collected, waits for it.
pub async fn access_bundle_from_id(
client: &nexus_client::Client,
id: Option<SupportBundleUuid>,
) -> Result<InternalApiAccess<'_>, anyhow::Error> {
let id = match id {
Some(id) => {
// Ensure the bundle has been collected
let sb = wait_for_bundle_to_be_collected(
client,
SupportBundleUuid::from_untyped_uuid(*id.as_untyped_uuid()),
)
.await?;
SupportBundleUuid::from_untyped_uuid(sb.id.into_untyped_uuid())
}
None => {
// Grab the latest if one isn't supplied
let support_bundle_stream =
client.support_bundle_list_stream(None, None);
let mut support_bundles = support_bundle_stream
.try_collect::<Vec<_>>()
.await
.context("listing support bundles")?;
support_bundles.sort_by_key(|k| k.time_created);

async fn get_file<'a>(
&mut self,
path: &Utf8Path,
) -> Result<BoxedFileAccessor<'a>> {
let mut file = self.archive.by_name(path.as_str())?;
let mut buf = Vec::new();
std::io::copy(&mut file, &mut buf)?;
let active_sb = support_bundles
.iter()
.find(|sb| matches!(sb.state, SupportBundleState::Active));

Ok(Box::new(AsyncZipFile { buf, copied: 0 }))
}
}
let sb = match active_sb {
Some(sb) => sb.clone(),
None => {
// This is a special case, but not an uncommon one:
//
// - Someone just created a bundle...
// - ... but collection is still happening.
//
// To smooth out this experience for users, we wait for the
// collection to complete.
let collecting_sb = support_bundles.iter().find(|sb| {
matches!(sb.state, SupportBundleState::Collecting)
});
if let Some(collecting_sb) = collecting_sb {
let id = &collecting_sb.id;
wait_for_bundle_to_be_collected(
client,
SupportBundleUuid::from_untyped_uuid(
*id.as_untyped_uuid(),
),
)
.await?
} else {
bail!(
"Cannot find active support bundle. Try creating one"
)
}
}
};

// We're currently buffering the entire file into memory, mostly because dealing with the lifetime
// of ZipArchive and ZipFile objects is so difficult.
pub struct AsyncZipFile {
buf: Vec<u8>,
copied: usize,
}
eprintln!("Inspecting bundle {} from {}", sb.id, sb.time_created);

impl AsyncRead for AsyncZipFile {
fn poll_read(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
let to_copy =
std::cmp::min(self.buf.len() - self.copied, buf.remaining());
if to_copy == 0 {
return Poll::Ready(Ok(()));
SupportBundleUuid::from_untyped_uuid(sb.id.into_untyped_uuid())
}
let src = &self.buf[self.copied..];
buf.put_slice(&src[..to_copy]);
self.copied += to_copy;
Poll::Ready(Ok(()))
}
}

async fn utf8_stream_to_string(
mut stream: impl futures::Stream<Item = reqwest::Result<bytes::Bytes>>
+ std::marker::Unpin,
) -> Result<String> {
let mut bytes = Vec::new();
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
bytes.extend_from_slice(&chunk);
}
Ok(String::from_utf8(bytes)?)
};
Ok(InternalApiAccess::new(client, id))
}
Loading
Loading