Skip to content

Commit c17b702

Browse files
authored
Move Support Bundle Inspector logic (#8122)
The support bundle viewer logic has been moved to an external crate: https://github.com/oxidecomputer/support-bundle-viewer This PR is mostly "move-only": - Use that crate - Keep the "internal Nexus API" bits within omdb This enables external usage of the support bundle inspector, e.g., in: oxidecomputer/oxide.rs#1093
1 parent c5329d7 commit c17b702

File tree

10 files changed

+141
-627
lines changed

10 files changed

+141
-627
lines changed

Cargo.lock

Lines changed: 5 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ members = [
4949
"dev-tools/releng",
5050
"dev-tools/repl-utils",
5151
"dev-tools/repo-depot-standalone",
52-
"dev-tools/support-bundle-reader-lib",
5352
"dev-tools/xtask",
5453
"dns-server",
5554
"dns-server-api",
@@ -201,7 +200,6 @@ default-members = [
201200
"dev-tools/releng",
202201
"dev-tools/repl-utils",
203202
"dev-tools/repo-depot-standalone",
204-
"dev-tools/support-bundle-reader-lib",
205203
# Do not include xtask in the list of default members, because this causes
206204
# hakari to not work as well and build times to be longer.
207205
# See omicron#4392.
@@ -699,7 +697,7 @@ strum = { version = "0.26", features = [ "derive" ] }
699697
subprocess = "0.2.9"
700698
subtle = "2.6.1"
701699
supports-color = "3.0.2"
702-
support-bundle-reader-lib = { path = "dev-tools/support-bundle-reader-lib" }
700+
support-bundle-viewer = "0.1.0"
703701
swrite = "0.1.0"
704702
sync-ptr = "0.1.1"
705703
libsw = { version = "3.4.0", features = ["tokio"] }

dev-tools/omdb/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ omicron-rpaths.workspace = true
1313
[dependencies]
1414
anyhow.workspace = true
1515
async-bb8-diesel.workspace = true
16+
async-trait.workspace = true
1617
bytes.workspace = true
1718
camino.workspace = true
1819
chrono.workspace = true
@@ -59,7 +60,7 @@ slog.workspace = true
5960
slog-error-chain.workspace = true
6061
steno.workspace = true
6162
strum.workspace = true
62-
support-bundle-reader-lib.workspace = true
63+
support-bundle-viewer.workspace = true
6364
supports-color.workspace = true
6465
tabled.workspace = true
6566
textwrap.workspace = true

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ mod oximeter;
5656
mod oxql;
5757
mod reconfigurator;
5858
mod sled_agent;
59+
mod support_bundle;
5960

6061
#[tokio::main]
6162
async fn main() -> Result<(), anyhow::Error> {

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

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use crate::helpers::ConfirmationPrompt;
1212
use crate::helpers::const_max_len;
1313
use crate::helpers::display_option_blank;
1414
use crate::helpers::should_colorize;
15-
use anyhow::Context;
15+
use anyhow::Context as _;
1616
use anyhow::bail;
1717
use camino::Utf8PathBuf;
1818
use chrono::DateTime;
@@ -78,6 +78,8 @@ use std::collections::BTreeMap;
7878
use std::collections::BTreeSet;
7979
use std::str::FromStr;
8080
use std::sync::Arc;
81+
use support_bundle_viewer::LocalFileAccess;
82+
use support_bundle_viewer::SupportBundleAccessor;
8183
use tabled::Tabled;
8284
use tabled::settings::Padding;
8385
use tabled::settings::object::Columns;
@@ -3945,10 +3947,16 @@ async fn cmd_nexus_support_bundles_inspect(
39453947
client: &nexus_client::Client,
39463948
args: &SupportBundleInspectArgs,
39473949
) -> Result<(), anyhow::Error> {
3948-
support_bundle_reader_lib::run_dashboard(
3949-
client,
3950-
args.id,
3951-
args.path.as_ref(),
3952-
)
3953-
.await
3950+
let accessor: Box<dyn SupportBundleAccessor> = match (args.id, &args.path) {
3951+
(None, Some(path)) => Box::new(LocalFileAccess::new(path)?),
3952+
(maybe_id, None) => Box::new(
3953+
crate::support_bundle::access_bundle_from_id(client, maybe_id)
3954+
.await?,
3955+
),
3956+
(Some(_), Some(_)) => {
3957+
bail!("Cannot specify both UUID and path");
3958+
}
3959+
};
3960+
3961+
support_bundle_viewer::run_dashboard(accessor).await
39543962
}

dev-tools/support-bundle-reader-lib/src/bundle_accessor.rs renamed to dev-tools/omdb/src/bin/omdb/support_bundle.rs

Lines changed: 117 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,34 @@
22
// License, v. 2.0. If a copy of the MPL was not distributed with this
33
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
44

5-
//! APIs to help access bundles
5+
//! Utilities to access support bundles via the internal API
66
7-
use crate::index::SupportBundleIndex;
87
use anyhow::Context as _;
9-
use anyhow::Result;
8+
use anyhow::bail;
109
use async_trait::async_trait;
1110
use bytes::Buf;
1211
use bytes::Bytes;
1312
use camino::Utf8Path;
1413
use camino::Utf8PathBuf;
1514
use futures::Stream;
1615
use futures::StreamExt;
16+
use futures::TryStreamExt;
17+
use nexus_client::types::SupportBundleInfo;
18+
use nexus_client::types::SupportBundleState;
1719
use omicron_uuid_kinds::GenericUuid;
1820
use omicron_uuid_kinds::SupportBundleUuid;
1921
use std::io;
22+
use std::io::Write;
2023
use std::pin::Pin;
2124
use std::task::Context;
2225
use std::task::Poll;
26+
use std::time::Duration;
27+
use support_bundle_viewer::BoxedFileAccessor;
28+
use support_bundle_viewer::SupportBundleAccessor;
29+
use support_bundle_viewer::SupportBundleIndex;
2330
use tokio::io::AsyncRead;
2431
use tokio::io::ReadBuf;
2532

26-
/// An I/O source which can read to a buffer
27-
///
28-
/// This describes access to individual files within the bundle.
29-
pub trait FileAccessor: AsyncRead + Unpin {}
30-
impl<T: AsyncRead + Unpin + ?Sized> FileAccessor for T {}
31-
32-
pub type BoxedFileAccessor<'a> = Box<dyn FileAccessor + 'a>;
33-
34-
/// Describes how the support bundle's data and metadata are accessed.
35-
#[async_trait]
36-
pub trait SupportBundleAccessor {
37-
/// Access the index of a support bundle
38-
async fn get_index(&self) -> Result<SupportBundleIndex>;
39-
40-
/// Access a file within the support bundle
41-
async fn get_file<'a>(
42-
&mut self,
43-
path: &Utf8Path,
44-
) -> Result<BoxedFileAccessor<'a>>
45-
where
46-
Self: 'a;
47-
}
48-
4933
pub struct StreamedFile<'a> {
5034
client: &'a nexus_client::Client,
5135
id: SupportBundleUuid,
@@ -67,7 +51,7 @@ impl<'a> StreamedFile<'a> {
6751
// use range requests to stream out portions of the file.
6852
//
6953
// This means that we would potentially want to restart the stream with a different position.
70-
async fn start_stream(&mut self) -> Result<()> {
54+
async fn start_stream(&mut self) -> anyhow::Result<()> {
7155
// TODO: Add range headers, for range requests? Though this
7256
// will require adding support to Progenitor + Nexus too.
7357
let stream = self
@@ -140,10 +124,22 @@ impl<'a> InternalApiAccess<'a> {
140124
}
141125
}
142126

127+
async fn utf8_stream_to_string(
128+
mut stream: impl futures::Stream<Item = reqwest::Result<bytes::Bytes>>
129+
+ std::marker::Unpin,
130+
) -> anyhow::Result<String> {
131+
let mut bytes = Vec::new();
132+
while let Some(chunk) = stream.next().await {
133+
let chunk = chunk?;
134+
bytes.extend_from_slice(&chunk);
135+
}
136+
Ok(String::from_utf8(bytes)?)
137+
}
138+
143139
// Access for: The nexus internal API
144140
#[async_trait]
145141
impl<'c> SupportBundleAccessor for InternalApiAccess<'c> {
146-
async fn get_index(&self) -> Result<SupportBundleIndex> {
142+
async fn get_index(&self) -> anyhow::Result<SupportBundleIndex> {
147143
let stream = self
148144
.client
149145
.support_bundle_index(self.id.as_untyped_uuid())
@@ -160,7 +156,7 @@ impl<'c> SupportBundleAccessor for InternalApiAccess<'c> {
160156
async fn get_file<'a>(
161157
&mut self,
162158
path: &Utf8Path,
163-
) -> Result<BoxedFileAccessor<'a>>
159+
) -> anyhow::Result<BoxedFileAccessor<'a>>
164160
where
165161
'c: 'a,
166162
{
@@ -173,71 +169,105 @@ impl<'c> SupportBundleAccessor for InternalApiAccess<'c> {
173169
}
174170
}
175171

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

180-
impl LocalFileAccess {
181-
pub fn new(path: &Utf8Path) -> Result<Self> {
182-
let file = std::fs::File::open(path)?;
183-
Ok(Self { archive: zip::read::ZipArchive::new(file)? })
185+
match sb.state {
186+
SupportBundleState::Active => {
187+
if printed_wait_msg {
188+
eprintln!("");
189+
}
190+
return Ok(sb.into_inner());
191+
}
192+
SupportBundleState::Collecting => {
193+
if !printed_wait_msg {
194+
eprint!("Waiting for {} to finish collection...", id);
195+
printed_wait_msg = true;
196+
}
197+
tokio::time::sleep(Duration::from_secs(1)).await;
198+
eprint!(".");
199+
std::io::stderr().flush().context("cannot flush stderr")?;
200+
}
201+
other => bail!("Unexepcted state: {other}"),
202+
}
184203
}
185204
}
186205

187-
// Access for: Local zip files
188-
#[async_trait]
189-
impl SupportBundleAccessor for LocalFileAccess {
190-
async fn get_index(&self) -> Result<SupportBundleIndex> {
191-
let names: Vec<&str> = self.archive.file_names().collect();
192-
let all_names = names.join("\n");
193-
Ok(SupportBundleIndex::new(&all_names))
194-
}
206+
/// Returns either a specific bundle or the latest active bundle.
207+
///
208+
/// If a bundle is being collected, waits for it.
209+
pub async fn access_bundle_from_id(
210+
client: &nexus_client::Client,
211+
id: Option<SupportBundleUuid>,
212+
) -> Result<InternalApiAccess<'_>, anyhow::Error> {
213+
let id = match id {
214+
Some(id) => {
215+
// Ensure the bundle has been collected
216+
let sb = wait_for_bundle_to_be_collected(
217+
client,
218+
SupportBundleUuid::from_untyped_uuid(*id.as_untyped_uuid()),
219+
)
220+
.await?;
221+
SupportBundleUuid::from_untyped_uuid(sb.id.into_untyped_uuid())
222+
}
223+
None => {
224+
// Grab the latest if one isn't supplied
225+
let support_bundle_stream =
226+
client.support_bundle_list_stream(None, None);
227+
let mut support_bundles = support_bundle_stream
228+
.try_collect::<Vec<_>>()
229+
.await
230+
.context("listing support bundles")?;
231+
support_bundles.sort_by_key(|k| k.time_created);
195232

196-
async fn get_file<'a>(
197-
&mut self,
198-
path: &Utf8Path,
199-
) -> Result<BoxedFileAccessor<'a>> {
200-
let mut file = self.archive.by_name(path.as_str())?;
201-
let mut buf = Vec::new();
202-
std::io::copy(&mut file, &mut buf)?;
233+
let active_sb = support_bundles
234+
.iter()
235+
.find(|sb| matches!(sb.state, SupportBundleState::Active));
203236

204-
Ok(Box::new(AsyncZipFile { buf, copied: 0 }))
205-
}
206-
}
237+
let sb = match active_sb {
238+
Some(sb) => sb.clone(),
239+
None => {
240+
// This is a special case, but not an uncommon one:
241+
//
242+
// - Someone just created a bundle...
243+
// - ... but collection is still happening.
244+
//
245+
// To smooth out this experience for users, we wait for the
246+
// collection to complete.
247+
let collecting_sb = support_bundles.iter().find(|sb| {
248+
matches!(sb.state, SupportBundleState::Collecting)
249+
});
250+
if let Some(collecting_sb) = collecting_sb {
251+
let id = &collecting_sb.id;
252+
wait_for_bundle_to_be_collected(
253+
client,
254+
SupportBundleUuid::from_untyped_uuid(
255+
*id.as_untyped_uuid(),
256+
),
257+
)
258+
.await?
259+
} else {
260+
bail!(
261+
"Cannot find active support bundle. Try creating one"
262+
)
263+
}
264+
}
265+
};
207266

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

215-
impl AsyncRead for AsyncZipFile {
216-
fn poll_read(
217-
mut self: Pin<&mut Self>,
218-
_cx: &mut Context<'_>,
219-
buf: &mut ReadBuf<'_>,
220-
) -> Poll<io::Result<()>> {
221-
let to_copy =
222-
std::cmp::min(self.buf.len() - self.copied, buf.remaining());
223-
if to_copy == 0 {
224-
return Poll::Ready(Ok(()));
269+
SupportBundleUuid::from_untyped_uuid(sb.id.into_untyped_uuid())
225270
}
226-
let src = &self.buf[self.copied..];
227-
buf.put_slice(&src[..to_copy]);
228-
self.copied += to_copy;
229-
Poll::Ready(Ok(()))
230-
}
231-
}
232-
233-
async fn utf8_stream_to_string(
234-
mut stream: impl futures::Stream<Item = reqwest::Result<bytes::Bytes>>
235-
+ std::marker::Unpin,
236-
) -> Result<String> {
237-
let mut bytes = Vec::new();
238-
while let Some(chunk) = stream.next().await {
239-
let chunk = chunk?;
240-
bytes.extend_from_slice(&chunk);
241-
}
242-
Ok(String::from_utf8(bytes)?)
271+
};
272+
Ok(InternalApiAccess::new(client, id))
243273
}

0 commit comments

Comments
 (0)