Skip to content

[omdb] TUI for inspecting support bundles #8034

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 48 commits into from
May 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
07dd473
[nexus] Put support bundles in internal API too
smklein Apr 11, 2025
c079c3f
[omdb] Basic commands to access support bundles
smklein Apr 14, 2025
df47341
Updated output
smklein Apr 14, 2025
219d284
Merge branch 'main' into sb-internal-api
smklein Apr 14, 2025
e39785a
Merge branch 'sb-internal-api' into omdb-sb
smklein Apr 14, 2025
12f461b
[nexus] Make it 'more default' for Debug datasets to exist in test en…
smklein Apr 15, 2025
1af91c6
test patching
smklein Apr 15, 2025
8969fbe
Merge branch 'main' into sb-internal-api
smklein Apr 15, 2025
3dfd8ab
Merge branch 'sb-internal-api' into omdb-sb
smklein Apr 15, 2025
07de40c
Merge branch 'omicron-dev-disk-test' into sb-internal-api
smklein Apr 15, 2025
4bf9d9a
Merge branch 'sb-internal-api' into omdb-sb
smklein Apr 15, 2025
73b4975
Try compiling
smklein Apr 15, 2025
c3876e2
Merge branch 'omicron-dev-disk-test' into sb-internal-api
smklein Apr 15, 2025
600a537
Merge branch 'sb-internal-api' into omdb-sb
smklein Apr 15, 2025
8022e12
Use internal opctx
smklein Apr 15, 2025
47819c0
Patching tests more
smklein Apr 15, 2025
78af872
Merge branch 'omicron-dev-disk-test' into sb-internal-api
smklein Apr 15, 2025
192e255
Merge branch 'sb-internal-api' into omdb-sb
smklein Apr 15, 2025
6374a7b
Don't inject newlines
smklein Apr 16, 2025
c28a398
Continuing to iterate on TUI
smklein Apr 23, 2025
ab14729
Merge branch 'main' into omicron-dev-disk-test
smklein Apr 23, 2025
ceedbc3
Merge branch 'omicron-dev-disk-test' into sb-internal-api
smklein Apr 23, 2025
8278c09
Merge branch 'sb-internal-api' into omdb-sb
smklein Apr 23, 2025
9070654
Merge branch 'omdb-sb' into omdb-sb-polish
smklein Apr 23, 2025
820556f
Shift to move faster, fix dirs, wrapping
smklein Apr 23, 2025
0a457dd
Enable inspection of local files
smklein Apr 24, 2025
7fe5002
Fmt
smklein Apr 24, 2025
7fb1168
Merge branch 'main' into omicron-dev-disk-test
smklein Apr 25, 2025
36d2d04
Make datasets private, add helpers to access them
smklein Apr 25, 2025
bcfbb51
Merge branch 'omicron-dev-disk-test' into sb-internal-api
smklein Apr 25, 2025
b21b525
feedback
smklein Apr 25, 2025
b9b94d5
Merge branch 'sb-internal-api' into omdb-sb
smklein Apr 25, 2025
53c0a76
feedback, less utf8
smklein Apr 25, 2025
8f75399
Merge branch 'omdb-sb' into omdb-sb-polish
smklein Apr 25, 2025
44bd6a5
Better support for binary files
smklein Apr 25, 2025
9597433
Merge branch 'main' into omicron-dev-disk-test
smklein Apr 28, 2025
d2d0c76
Merge branch 'omicron-dev-disk-test' into sb-internal-api
smklein Apr 28, 2025
c8d9546
Merge branch 'sb-internal-api' into omdb-sb
smklein Apr 28, 2025
2790d9a
expectorate
smklein Apr 28, 2025
1a218b8
Merge branch 'omdb-sb' into omdb-sb-polish
smklein Apr 28, 2025
b73b6ff
Better support for waiting for collection to finish
smklein Apr 29, 2025
82ca320
Refactoring TUI into support-bundle-reader-lib
smklein Apr 29, 2025
995ca46
More private interface
smklein Apr 29, 2025
6ad1a31
starting to buffer and stream more properly
smklein Apr 30, 2025
865467a
Less unwrapping, more cleanup
smklein Apr 30, 2025
cc94a11
Merge branch 'main' into omdb-sb-polish
smklein Apr 30, 2025
bf76518
Merge branch 'main' into omdb-sb-polish
smklein May 7, 2025
12d00d8
feedback
smklein May 7, 2025
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
20 changes: 20 additions & 0 deletions Cargo.lock

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

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ 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 @@ -199,6 +200,7 @@ 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 @@ -692,6 +694,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" }
swrite = "0.1.0"
sync-ptr = "0.1.1"
libsw = { version = "3.4.0", features = ["tokio"] }
Expand Down
1 change: 1 addition & 0 deletions dev-tools/omdb/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ slog.workspace = true
slog-error-chain.workspace = true
steno.workspace = true
strum.workspace = true
support-bundle-reader-lib.workspace = true
supports-color.workspace = true
tabled.workspace = true
textwrap.workspace = true
Expand Down
35 changes: 35 additions & 0 deletions dev-tools/omdb/src/bin/omdb/nexus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,8 @@ enum SupportBundleCommands {
GetIndex(SupportBundleIndexArgs),
/// View a file within a support bundle
GetFile(SupportBundleFileArgs),
/// Creates a dashboard for viewing the contents of a support bundle
Inspect(SupportBundleInspectArgs),
}

#[derive(Debug, Args)]
Expand All @@ -523,6 +525,23 @@ struct SupportBundleFileArgs {
output: Option<Utf8PathBuf>,
}

#[derive(Debug, Args)]
struct SupportBundleInspectArgs {
/// A specific bundle to inspect.
///
/// If none is supplied, the latest active bundle is used.
/// Mutually exclusive with "path".
#[arg(short, long)]
id: Option<SupportBundleUuid>,

/// A local bundle file to inspect.
///
/// If none is supplied, the latest active bundle is used.
/// Mutually exclusive with "id".
#[arg(short, long)]
path: Option<Utf8PathBuf>,
}

impl NexusArgs {
/// Run a `omdb nexus` subcommand.
pub(crate) async fn run_cmd(
Expand Down Expand Up @@ -737,6 +756,9 @@ impl NexusArgs {
NexusCommands::SupportBundles(SupportBundleArgs {
command: SupportBundleCommands::GetFile(args),
}) => cmd_nexus_support_bundles_get_file(&client, args).await,
NexusCommands::SupportBundles(SupportBundleArgs {
command: SupportBundleCommands::Inspect(args),
}) => cmd_nexus_support_bundles_inspect(&client, args).await,
}
}
}
Expand Down Expand Up @@ -3880,3 +3902,16 @@ async fn cmd_nexus_support_bundles_get_file(
})?;
Ok(())
}

/// Runs `omdb nexus support-bundles inspect`
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
}
23 changes: 23 additions & 0 deletions dev-tools/support-bundle-reader-lib/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "support-bundle-reader-lib"
version = "0.1.0"
edition = "2021"
license = "MPL-2.0"

[lints]
workspace = true

[dependencies]
anyhow.workspace = true
async-trait.workspace = true
bytes.workspace = true
camino.workspace = true
crossterm.workspace = true
futures.workspace = true
nexus-client.workspace = true
omicron-workspace-hack.workspace = true
omicron-uuid-kinds.workspace = true
ratatui.workspace = true
reqwest.workspace = true
tokio.workspace = true
zip.workspace = true
243 changes: 243 additions & 0 deletions dev-tools/support-bundle-reader-lib/src/bundle_accessor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// 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

use crate::index::SupportBundleIndex;
use anyhow::Context as _;
use anyhow::Result;
use async_trait::async_trait;
use bytes::Buf;
use bytes::Bytes;
use camino::Utf8Path;
use camino::Utf8PathBuf;
use futures::Stream;
use futures::StreamExt;
use omicron_uuid_kinds::GenericUuid;
use omicron_uuid_kinds::SupportBundleUuid;
use std::io;
use std::pin::Pin;
use std::task::Context;
use std::task::Poll;
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,
path: Utf8PathBuf,
stream: Option<Pin<Box<dyn Stream<Item = reqwest::Result<Bytes>> + Send>>>,
buffer: Bytes,
}

impl<'a> StreamedFile<'a> {
fn new(
client: &'a nexus_client::Client,
id: SupportBundleUuid,
path: Utf8PathBuf,
) -> Self {
Self { client, id, path, stream: None, buffer: Bytes::new() }
}

// NOTE: This is a distinct method from "new", because ideally some day we could
// 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<()> {
// TODO: Add range headers, for range requests? Though this
// will require adding support to Progenitor + Nexus too.
let stream = self
.client
.support_bundle_download_file(
self.id.as_untyped_uuid(),
self.path.as_str(),
)
.await
.with_context(|| {
format!(
"downloading support bundle file {}: {}",
self.id, self.path
)
})?
.into_inner_stream();

self.stream = Some(Box::pin(stream));
Ok(())
}
}

impl AsyncRead for StreamedFile<'_> {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
while self.buffer.is_empty() {
match futures::ready!(
self.stream
.as_mut()
.expect("Stream must be initialized before polling")
.as_mut()
.poll_next(cx)
) {
Some(Ok(bytes)) => {
self.buffer = bytes;
}
Some(Err(e)) => {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::Other,
e,
)));
}
None => return Poll::Ready(Ok(())), // EOF
}
}

let to_copy = std::cmp::min(self.buffer.len(), buf.remaining());
buf.put_slice(&self.buffer[..to_copy]);
self.buffer.advance(to_copy);

Poll::Ready(Ok(()))
}
}

/// Access to a support bundle from the internal API
pub struct InternalApiAccess<'a> {
client: &'a nexus_client::Client,
id: SupportBundleUuid,
}

impl<'a> InternalApiAccess<'a> {
pub fn new(
client: &'a nexus_client::Client,
id: SupportBundleUuid,
) -> Self {
Self { client, id }
}
}

// Access for: The nexus internal API
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might need to refactor this crate a bit, but I'm expecting we'll also add one of these for ExternalApiAccess if we want to re-use this crate in the CLI

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The need for ExternalApiAccess would be a tool consuming the support-bundle-reader-lib that a customer could use rather than omdb?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am expecting that:

  • The CLI can add an implementation using "ExternalApiAccess" (or we can add one in this crate; I don't really care strongly where it lives).
  • The CLI will depend on support-bundle-reader-lib. We could use feature flags to let them avoid building against the internal API at all.
  • Customers aren't really supposed to be using omdb -- it's a support staff binary which exists in the switch zone, and requires techport access to use.

#[async_trait]
impl<'c> SupportBundleAccessor for InternalApiAccess<'c> {
async fn get_index(&self) -> Result<SupportBundleIndex> {
let stream = self
.client
.support_bundle_index(self.id.as_untyped_uuid())
.await
.with_context(|| {
format!("downloading support bundle index {}", self.id)
})?
.into_inner_stream();
let s = utf8_stream_to_string(stream).await?;

Ok(SupportBundleIndex::new(&s))
}

async fn get_file<'a>(
&mut self,
path: &Utf8Path,
) -> Result<BoxedFileAccessor<'a>>
where
'c: 'a,
{
let mut file =
StreamedFile::new(self.client, self.id, path.to_path_buf());
file.start_stream()
.await
.with_context(|| "failed to start stream in get_file")?;
Ok(Box::new(file))
}
}

pub struct LocalFileAccess {
archive: zip::read::ZipArchive<std::fs::File>,
}

impl LocalFileAccess {
pub fn new(path: &Utf8Path) -> Result<Self> {
let file = std::fs::File::open(path)?;
Ok(Self { archive: zip::read::ZipArchive::new(file)? })
}
}

// 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))
}

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)?;

Ok(Box::new(AsyncZipFile { buf, copied: 0 }))
}
}

// 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,
}

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(()));
}
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)?)
}
Loading
Loading