From 863c24b73f2de285ca8c9a6081ceb1d7a3c86a3f Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Tue, 5 May 2026 13:08:49 -0700 Subject: [PATCH] Move support bundle collection into a shared crate Lifts `nexus/src/app/background/tasks/support_bundle/` (the mechanism layer) into a new top-level crate `support-bundle-collection` so that both Nexus and omdb can call it. No logic changes; pure relocation plus import rewriting. --- Cargo.lock | 41 +++++++ Cargo.toml | 3 + dev-tools/ls-apis/tests/api_dependencies.out | 4 +- nexus/Cargo.toml | 1 + nexus/src/app/background/tasks/mod.rs | 1 - .../tasks/support_bundle_collector.rs | 102 +--------------- support-bundle-collection/Cargo.toml | 47 ++++++++ .../README.md | 0 .../build.rs | 13 +- .../src}/cache.rs | 2 +- .../src}/collection.rs | 8 +- support-bundle-collection/src/lib.rs | 26 ++++ .../src}/perfetto.rs | 0 .../src}/step.rs | 2 +- .../src}/steps/ereports.rs | 11 +- .../src}/steps/host_info.rs | 8 +- .../src}/steps/metadata.rs | 4 +- .../src}/steps/mod.rs | 4 +- .../src}/steps/reconfigurator.rs | 4 +- .../src}/steps/sled_cubby.rs | 6 +- .../src}/steps/sp_dumps.rs | 10 +- support-bundle-collection/src/zip.rs | 113 ++++++++++++++++++ 22 files changed, 272 insertions(+), 138 deletions(-) create mode 100644 support-bundle-collection/Cargo.toml rename {nexus/src/app/background/tasks/support_bundle => support-bundle-collection}/README.md (100%) rename nexus/src/app/background/tasks/support_bundle/mod.rs => support-bundle-collection/build.rs (50%) rename {nexus/src/app/background/tasks/support_bundle => support-bundle-collection/src}/cache.rs (97%) rename {nexus/src/app/background/tasks/support_bundle => support-bundle-collection/src}/collection.rs (97%) create mode 100644 support-bundle-collection/src/lib.rs rename {nexus/src/app/background/tasks/support_bundle => support-bundle-collection/src}/perfetto.rs (100%) rename {nexus/src/app/background/tasks/support_bundle => support-bundle-collection/src}/step.rs (98%) rename {nexus/src/app/background/tasks/support_bundle => support-bundle-collection/src}/steps/ereports.rs (96%) rename {nexus/src/app/background/tasks/support_bundle => support-bundle-collection/src}/steps/host_info.rs (97%) rename {nexus/src/app/background/tasks/support_bundle => support-bundle-collection/src}/steps/metadata.rs (86%) rename {nexus/src/app/background/tasks/support_bundle => support-bundle-collection/src}/steps/mod.rs (95%) rename {nexus/src/app/background/tasks/support_bundle => support-bundle-collection/src}/steps/reconfigurator.rs (93%) rename {nexus/src/app/background/tasks/support_bundle => support-bundle-collection/src}/steps/sled_cubby.rs (95%) rename {nexus/src/app/background/tasks/support_bundle => support-bundle-collection/src}/steps/sp_dumps.rs (92%) create mode 100644 support-bundle-collection/src/zip.rs diff --git a/Cargo.lock b/Cargo.lock index f61caae2532..7a133d5af03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8765,6 +8765,7 @@ dependencies = [ "steno", "strum 0.27.2", "subprocess", + "support-bundle-collection", "swrite", "tempfile", "term 0.7.0", @@ -14496,6 +14497,46 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "support-bundle-collection" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.22.1", + "camino", + "camino-tempfile", + "chrono", + "dropshot 0.17.0", + "futures", + "gateway-client", + "gateway-types", + "internal-dns-resolver", + "internal-dns-types", + "jiff", + "nexus-db-model", + "nexus-db-queries", + "nexus-networking", + "nexus-reconfigurator-preparation", + "nexus-types", + "omicron-common", + "omicron-rpaths", + "omicron-uuid-kinds", + "omicron-workspace-hack", + "parallel-task-set", + "pq-sys", + "serde", + "serde_json", + "sha2", + "sled-agent-client", + "slog", + "slog-error-chain", + "tokio", + "tokio-util", + "tufaceous-artifact", + "uuid", + "zip 4.6.1", +] + [[package]] name = "support-bundle-viewer" version = "0.1.2" diff --git a/Cargo.toml b/Cargo.toml index 3e5adc43e60..daa23af3dd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -161,6 +161,7 @@ members = [ "sled-storage", "sled-storage/zfs-test-harness", "sp-sim", + "support-bundle-collection", "test-utils", "trust-quorum", "trust-quorum/gfss", @@ -347,6 +348,7 @@ default-members = [ "sled-storage", "sled-storage/zfs-test-harness", "sp-sim", + "support-bundle-collection", "trust-quorum", "trust-quorum/gfss", "trust-quorum/protocol", @@ -823,6 +825,7 @@ strum = { version = "0.27.2", features = [ "derive" ] } subprocess = "0.2.9" subtle = "2.6.1" supports-color = "3.0.2" +support-bundle-collection = { path = "support-bundle-collection" } support-bundle-viewer = "0.1.2" swrite = "0.1.0" sync-ptr = "0.1.4" diff --git a/dev-tools/ls-apis/tests/api_dependencies.out b/dev-tools/ls-apis/tests/api_dependencies.out index a6cd556f371..a876ebff042 100644 --- a/dev-tools/ls-apis/tests/api_dependencies.out +++ b/dev-tools/ls-apis/tests/api_dependencies.out @@ -49,7 +49,7 @@ Management Gateway Service (client: gateway-client) consumed by: dpd (dendrite/dpd) via 1 path consumed by: lldpd (lldp/lldpd) via 1 path consumed by: mgd (maghemite/mgd) via 1 path - consumed by: omicron-nexus (omicron/nexus) via 5 paths + consumed by: omicron-nexus (omicron/nexus) via 6 paths consumed by: omicron-sled-agent (omicron/sled-agent) via 1 path consumed by: wicketd (omicron/wicketd) via 3 paths @@ -96,7 +96,7 @@ Repo Depot API (client: repo-depot-client) consumed by: omicron-sled-agent (omicron/sled-agent) via 1 path Sled Agent (client: sled-agent-client) - consumed by: omicron-nexus (omicron/nexus) via 8 paths + consumed by: omicron-nexus (omicron/nexus) via 9 paths Wicketd (client: wicketd-client) diff --git a/nexus/Cargo.toml b/nexus/Cargo.toml index 0a24e5eb3e2..18ffa16c8e4 100644 --- a/nexus/Cargo.toml +++ b/nexus/Cargo.toml @@ -159,6 +159,7 @@ raw-cpuid = { workspace = true, features = ["std"] } rustls = { workspace = true } rustls-pemfile = { workspace = true } scim2-rs.workspace = true +support-bundle-collection.workspace = true update-common.workspace = true update-engine.workspace = true omicron-workspace-hack.workspace = true diff --git a/nexus/src/app/background/tasks/mod.rs b/nexus/src/app/background/tasks/mod.rs index fdcb45ef8d0..e4438208813 100644 --- a/nexus/src/app/background/tasks/mod.rs +++ b/nexus/src/app/background/tasks/mod.rs @@ -49,7 +49,6 @@ pub mod region_snapshot_replacement_step; pub mod saga_recovery; pub mod service_firewall_rules; pub mod session_cleanup; -pub mod support_bundle; pub mod support_bundle_collector; pub mod sync_service_zone_nat; pub mod sync_switch_configuration; diff --git a/nexus/src/app/background/tasks/support_bundle_collector.rs b/nexus/src/app/background/tasks/support_bundle_collector.rs index 2f3aeadbb31..6f173d1b4a0 100644 --- a/nexus/src/app/background/tasks/support_bundle_collector.rs +++ b/nexus/src/app/background/tasks/support_bundle_collector.rs @@ -6,11 +6,9 @@ use crate::app::background::BackgroundTask; use anyhow::Context; -use camino::Utf8DirEntry; use camino::Utf8Path; use camino_tempfile::Utf8TempDir; use camino_tempfile::tempdir_in; -use camino_tempfile::tempfile_in; use futures::FutureExt; use futures::future::BoxFuture; use internal_dns_resolver::Resolver; @@ -38,16 +36,14 @@ use slog_error_chain::InlineErrorChain; use std::io::Write; use std::num::NonZeroU64; use std::sync::Arc; +use support_bundle_collection::BundleCollection; +use support_bundle_collection::BundleInfo; +use support_bundle_collection::zip::bundle_to_zipfile; use tokio::io::AsyncReadExt; use tokio::io::AsyncSeekExt; use tokio::io::SeekFrom; use tokio_util::sync::CancellationToken; use tufaceous_artifact::ArtifactHash; -use zip::ZipWriter; -use zip::write::FullFileOptions; - -use super::support_bundle::collection::BundleCollection; -use super::support_bundle::collection::BundleInfo; /// We use "/var/tmp" to use Nexus' filesystem for temporary storage, /// rather than "/tmp", which would keep this collected data in-memory. @@ -648,71 +644,6 @@ async fn check_for_cancellation( } } -// Takes a directory "dir", and zips the contents into a single zipfile -// stored as a tempfile under `tempdir`. -fn bundle_to_zipfile( - dir: &Utf8TempDir, - tempdir: &Utf8Path, -) -> anyhow::Result { - let tempfile = tempfile_in(tempdir)?; - let mut zip = ZipWriter::new(tempfile); - - recursively_add_directory_to_zipfile(&mut zip, dir.path(), dir.path())?; - - Ok(zip.finish()?) -} - -fn recursively_add_directory_to_zipfile( - zip: &mut ZipWriter, - root_path: &Utf8Path, - dir_path: &Utf8Path, -) -> anyhow::Result<()> { - // Readdir might return entries in a non-deterministic order. - // Let's sort it for the zipfile, to be nice. - let mut entries = dir_path - .read_dir_utf8()? - .filter_map(Result::ok) - .collect::>(); - entries.sort_by(|a, b| a.file_name().cmp(&b.file_name())); - - for entry in &entries { - // Remove the "/tmp/..." prefix from the path when we're storing it in the - // zipfile. - let dst = entry.path().strip_prefix(root_path)?; - - let file_type = entry.file_type()?; - if file_type.is_file() { - let src = entry.path(); - - let zip_time = entry - .path() - .metadata() - .and_then(|m| m.modified()) - .ok() - .and_then(|sys_time| jiff::Zoned::try_from(sys_time).ok()) - .and_then(|zoned| { - zip::DateTime::try_from(zoned.datetime()).ok() - }) - .unwrap_or_else(zip::DateTime::default); - - let opts = FullFileOptions::default() - .last_modified_time(zip_time) - .compression_method(zip::CompressionMethod::Deflated) - .large_file(true); - - zip.start_file_from_path(dst, opts)?; - let mut file = std::fs::File::open(&src)?; - std::io::copy(&mut file, zip)?; - } - if file_type.is_dir() { - let opts = FullFileOptions::default(); - zip.add_directory_from_path(dst, opts)?; - recursively_add_directory_to_zipfile(zip, root_path, entry.path())?; - } - } - Ok(()) -} - async fn sha2_hash(file: &mut tokio::fs::File) -> anyhow::Result { let mut buf = vec![0u8; 65536]; let mut ctx = Sha256::new(); @@ -774,7 +705,6 @@ impl BackgroundTask for SupportBundleCollector { mod test { use super::*; - use crate::app::background::tasks::support_bundle::perfetto; use crate::app::support_bundles::SupportBundleQueryType; use http_body_util::BodyExt; use nexus_db_model::PhysicalDisk; @@ -805,6 +735,7 @@ mod test { }; use sled_agent_types::inventory::ZpoolHealth; use std::num::NonZeroU64; + use support_bundle_collection::perfetto; use uuid::Uuid; type ControlPlaneTestContext = @@ -2154,29 +2085,4 @@ mod test { SupportBundleCollectionStepStatus::Skipped ); } - - // Ensure that we can convert a temporary directory into a zipfile - #[test] - fn test_zipfile_creation() { - let dir = camino_tempfile::tempdir().unwrap(); - let tempdir_for_zip = camino_tempfile::tempdir().unwrap(); - - std::fs::create_dir_all(dir.path().join("dir-a")).unwrap(); - std::fs::create_dir_all(dir.path().join("dir-b")).unwrap(); - std::fs::write(dir.path().join("dir-a").join("file-a"), "some data") - .unwrap(); - std::fs::write(dir.path().join("file-b"), "more data").unwrap(); - - let zipfile = bundle_to_zipfile(&dir, tempdir_for_zip.path()) - .expect("Should have been able to bundle zipfile"); - let archive = zip::read::ZipArchive::new(zipfile).unwrap(); - - // We expect the order to be deterministically alphabetical - let mut names = archive.file_names(); - assert_eq!(names.next(), Some("dir-a/")); - assert_eq!(names.next(), Some("dir-a/file-a")); - assert_eq!(names.next(), Some("dir-b/")); - assert_eq!(names.next(), Some("file-b")); - assert_eq!(names.next(), None); - } } diff --git a/support-bundle-collection/Cargo.toml b/support-bundle-collection/Cargo.toml new file mode 100644 index 00000000000..6b892921db7 --- /dev/null +++ b/support-bundle-collection/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "support-bundle-collection" +version = "0.1.0" +edition.workspace = true + +[lints] +workspace = true + +[build-dependencies] +omicron-rpaths.workspace = true + +[dependencies] +anyhow.workspace = true +base64.workspace = true +camino.workspace = true +camino-tempfile.workspace = true +chrono.workspace = true +dropshot.workspace = true +futures.workspace = true +gateway-client.workspace = true +gateway-types.workspace = true +internal-dns-resolver.workspace = true +internal-dns-types.workspace = true +jiff.workspace = true +nexus-db-model.workspace = true +nexus-db-queries.workspace = true +nexus-networking.workspace = true +nexus-reconfigurator-preparation.workspace = true +nexus-types.workspace = true +omicron-common.workspace = true +omicron-uuid-kinds.workspace = true +parallel-task-set.workspace = true +# See omicron-rpaths for more about the "pq-sys" dependency. +pq-sys = "*" +serde.workspace = true +serde_json.workspace = true +sha2.workspace = true +sled-agent-client.workspace = true +slog.workspace = true +slog-error-chain.workspace = true +tokio.workspace = true +tokio-util.workspace = true +tufaceous-artifact.workspace = true +uuid.workspace = true +zip.workspace = true + +omicron-workspace-hack.workspace = true diff --git a/nexus/src/app/background/tasks/support_bundle/README.md b/support-bundle-collection/README.md similarity index 100% rename from nexus/src/app/background/tasks/support_bundle/README.md rename to support-bundle-collection/README.md diff --git a/nexus/src/app/background/tasks/support_bundle/mod.rs b/support-bundle-collection/build.rs similarity index 50% rename from nexus/src/app/background/tasks/support_bundle/mod.rs rename to support-bundle-collection/build.rs index ba8fb9e4cab..1ba9acd41c9 100644 --- a/nexus/src/app/background/tasks/support_bundle/mod.rs +++ b/support-bundle-collection/build.rs @@ -2,10 +2,9 @@ // 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/. -//! Support bundle related types and utilities - -mod cache; -pub mod collection; -pub mod perfetto; -mod step; -mod steps; +// See omicron-rpaths for documentation. +// NOTE: This file MUST be kept in sync with the other build.rs files in this +// repository. +fn main() { + omicron_rpaths::configure_default_omicron_rpaths(); +} diff --git a/nexus/src/app/background/tasks/support_bundle/cache.rs b/support-bundle-collection/src/cache.rs similarity index 97% rename from nexus/src/app/background/tasks/support_bundle/cache.rs rename to support-bundle-collection/src/cache.rs index 314345c64b7..60f87919a5d 100644 --- a/nexus/src/app/background/tasks/support_bundle/cache.rs +++ b/support-bundle-collection/src/cache.rs @@ -7,7 +7,7 @@ //! This is used to share data which may be used by multiple //! otherwise independent steps. -use crate::app::background::tasks::support_bundle::collection::BundleCollection; +use crate::collection::BundleCollection; use gateway_client::Client as MgsClient; use internal_dns_types::names::ServiceName; diff --git a/nexus/src/app/background/tasks/support_bundle/collection.rs b/support-bundle-collection/src/collection.rs similarity index 97% rename from nexus/src/app/background/tasks/support_bundle/collection.rs rename to support-bundle-collection/src/collection.rs index 2326f05e677..f3a3fb9e8a7 100644 --- a/nexus/src/app/background/tasks/support_bundle/collection.rs +++ b/support-bundle-collection/src/collection.rs @@ -13,10 +13,10 @@ //! data to a sled-agent's bundle storage endpoints, and never polls //! bundle state. Those responsibilities belong to the caller. -use crate::app::background::tasks::support_bundle::cache::Cache; -use crate::app::background::tasks::support_bundle::perfetto; -use crate::app::background::tasks::support_bundle::step::CollectionStep; -use crate::app::background::tasks::support_bundle::steps; +use crate::cache::Cache; +use crate::perfetto; +use crate::step::CollectionStep; +use crate::steps; use nexus_types::support_bundle::BundleDataSelection; use anyhow::Context; diff --git a/support-bundle-collection/src/lib.rs b/support-bundle-collection/src/lib.rs new file mode 100644 index 00000000000..6a604b5fb21 --- /dev/null +++ b/support-bundle-collection/src/lib.rs @@ -0,0 +1,26 @@ +// 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/. + +//! The mechanism layer of support bundle collection. +//! +//! This crate provides the data-gathering primitives used to assemble a +//! support bundle. It is consumed by both the Nexus background task +//! (which manages bundle state in the `support_bundle` table) and by +//! `omdb` (which collects bundles ad-hoc, including when Nexus is down). +//! +//! See `README.md` in this crate for a developer-oriented overview of +//! the step framework. + +#[macro_use] +extern crate slog; + +mod cache; +pub mod collection; +pub mod perfetto; +mod step; +mod steps; +pub mod zip; + +pub use collection::BundleCollection; +pub use collection::BundleInfo; diff --git a/nexus/src/app/background/tasks/support_bundle/perfetto.rs b/support-bundle-collection/src/perfetto.rs similarity index 100% rename from nexus/src/app/background/tasks/support_bundle/perfetto.rs rename to support-bundle-collection/src/perfetto.rs diff --git a/nexus/src/app/background/tasks/support_bundle/step.rs b/support-bundle-collection/src/step.rs similarity index 98% rename from nexus/src/app/background/tasks/support_bundle/step.rs rename to support-bundle-collection/src/step.rs index 5909265b976..df222127403 100644 --- a/nexus/src/app/background/tasks/support_bundle/step.rs +++ b/support-bundle-collection/src/step.rs @@ -4,7 +4,7 @@ //! Support bundle collection step execution framework -use crate::app::background::tasks::support_bundle::collection::BundleCollection; +use crate::collection::BundleCollection; use camino::Utf8Path; use chrono::DateTime; diff --git a/nexus/src/app/background/tasks/support_bundle/steps/ereports.rs b/support-bundle-collection/src/steps/ereports.rs similarity index 96% rename from nexus/src/app/background/tasks/support_bundle/steps/ereports.rs rename to support-bundle-collection/src/steps/ereports.rs index ff4a58cbbad..d8f4aa69b4d 100644 --- a/nexus/src/app/background/tasks/support_bundle/steps/ereports.rs +++ b/support-bundle-collection/src/steps/ereports.rs @@ -4,13 +4,14 @@ //! Collect ereports for support bundles -use crate::app::background::tasks::support_bundle::collection::BundleCollection; -use crate::app::background::tasks::support_bundle::step::CollectionStepOutput; +use crate::collection::BundleCollection; +use crate::step::CollectionStepOutput; use nexus_types::fm::ereport::EreportFilters; use anyhow::Context; use camino::Utf8Path; use camino::Utf8PathBuf; +use dropshot::PaginationOrder; use nexus_db_queries::context::OpContext; use nexus_db_queries::db::DataStore; use nexus_db_queries::db::datastore; @@ -75,10 +76,8 @@ async fn save_ereports( dir: Utf8PathBuf, status: &mut SupportBundleEreportStatus, ) -> anyhow::Result<()> { - let mut paginator = Paginator::new( - datastore::SQL_BATCH_SIZE, - dropshot::PaginationOrder::Ascending, - ); + let mut paginator = + Paginator::new(datastore::SQL_BATCH_SIZE, PaginationOrder::Ascending); while let Some(p) = paginator.next() { let pagparams = p.current_pagparams(); let ereports = tokio::select! { diff --git a/nexus/src/app/background/tasks/support_bundle/steps/host_info.rs b/support-bundle-collection/src/steps/host_info.rs similarity index 97% rename from nexus/src/app/background/tasks/support_bundle/steps/host_info.rs rename to support-bundle-collection/src/steps/host_info.rs index 4aa0dc7fdb4..41cfadc58ad 100644 --- a/nexus/src/app/background/tasks/support_bundle/steps/host_info.rs +++ b/support-bundle-collection/src/steps/host_info.rs @@ -4,10 +4,10 @@ //! Collect host information from sleds for support bundles -use crate::app::background::tasks::support_bundle::cache::Cache; -use crate::app::background::tasks::support_bundle::collection::BundleCollection; -use crate::app::background::tasks::support_bundle::step::CollectionStep; -use crate::app::background::tasks::support_bundle::step::CollectionStepOutput; +use crate::cache::Cache; +use crate::collection::BundleCollection; +use crate::step::CollectionStep; +use crate::step::CollectionStepOutput; use anyhow::Context; use anyhow::bail; diff --git a/nexus/src/app/background/tasks/support_bundle/steps/metadata.rs b/support-bundle-collection/src/steps/metadata.rs similarity index 86% rename from nexus/src/app/background/tasks/support_bundle/steps/metadata.rs rename to support-bundle-collection/src/steps/metadata.rs index 726e3387d43..fcf7946b1f4 100644 --- a/nexus/src/app/background/tasks/support_bundle/steps/metadata.rs +++ b/support-bundle-collection/src/steps/metadata.rs @@ -4,8 +4,8 @@ //! Collects metadata about the bundle itself -use crate::app::background::tasks::support_bundle::collection::BundleCollection; -use crate::app::background::tasks::support_bundle::step::CollectionStepOutput; +use crate::collection::BundleCollection; +use crate::step::CollectionStepOutput; use camino::Utf8Path; /// Writes the bundle ID to a file diff --git a/nexus/src/app/background/tasks/support_bundle/steps/mod.rs b/support-bundle-collection/src/steps/mod.rs similarity index 95% rename from nexus/src/app/background/tasks/support_bundle/steps/mod.rs rename to support-bundle-collection/src/steps/mod.rs index 93952861b2c..57ddda584ed 100644 --- a/nexus/src/app/background/tasks/support_bundle/steps/mod.rs +++ b/support-bundle-collection/src/steps/mod.rs @@ -4,8 +4,8 @@ //! Individual support bundle collection steps -use crate::app::background::tasks::support_bundle::cache::Cache; -use crate::app::background::tasks::support_bundle::step::CollectionStep; +use crate::cache::Cache; +use crate::step::CollectionStep; use futures::FutureExt; use nexus_types::internal_api::background::SupportBundleCollectionStep; diff --git a/nexus/src/app/background/tasks/support_bundle/steps/reconfigurator.rs b/support-bundle-collection/src/steps/reconfigurator.rs similarity index 93% rename from nexus/src/app/background/tasks/support_bundle/steps/reconfigurator.rs rename to support-bundle-collection/src/steps/reconfigurator.rs index d1c8d79c4f8..d8ad62ee6ed 100644 --- a/nexus/src/app/background/tasks/support_bundle/steps/reconfigurator.rs +++ b/support-bundle-collection/src/steps/reconfigurator.rs @@ -4,8 +4,8 @@ //! Collect reconfigurator state for support bundles -use crate::app::background::tasks::support_bundle::collection::BundleCollection; -use crate::app::background::tasks::support_bundle::step::CollectionStepOutput; +use crate::collection::BundleCollection; +use crate::step::CollectionStepOutput; use anyhow::Context; use camino::Utf8Path; diff --git a/nexus/src/app/background/tasks/support_bundle/steps/sled_cubby.rs b/support-bundle-collection/src/steps/sled_cubby.rs similarity index 95% rename from nexus/src/app/background/tasks/support_bundle/steps/sled_cubby.rs rename to support-bundle-collection/src/steps/sled_cubby.rs index 1bd6dc59a36..91ce615e101 100644 --- a/nexus/src/app/background/tasks/support_bundle/steps/sled_cubby.rs +++ b/support-bundle-collection/src/steps/sled_cubby.rs @@ -4,9 +4,9 @@ //! Collect sled cubby information for support bundles -use crate::app::background::tasks::support_bundle::cache::Cache; -use crate::app::background::tasks::support_bundle::collection::BundleCollection; -use crate::app::background::tasks::support_bundle::step::CollectionStepOutput; +use crate::cache::Cache; +use crate::collection::BundleCollection; +use crate::step::CollectionStepOutput; use anyhow::Context; use anyhow::bail; diff --git a/nexus/src/app/background/tasks/support_bundle/steps/sp_dumps.rs b/support-bundle-collection/src/steps/sp_dumps.rs similarity index 92% rename from nexus/src/app/background/tasks/support_bundle/steps/sp_dumps.rs rename to support-bundle-collection/src/steps/sp_dumps.rs index 07027abcc5a..496be1eb065 100644 --- a/nexus/src/app/background/tasks/support_bundle/steps/sp_dumps.rs +++ b/support-bundle-collection/src/steps/sp_dumps.rs @@ -4,11 +4,11 @@ //! Collect SP task dumps for support bundles -use crate::app::background::tasks::support_bundle::cache::Cache; -use crate::app::background::tasks::support_bundle::collection::BundleCollection; -use crate::app::background::tasks::support_bundle::step::CollectionStep; -use crate::app::background::tasks::support_bundle::step::CollectionStepOutput; -use crate::app::background::tasks::support_bundle::steps; +use crate::cache::Cache; +use crate::collection::BundleCollection; +use crate::step::CollectionStep; +use crate::step::CollectionStepOutput; +use crate::steps; use anyhow::Context; use anyhow::bail; diff --git a/support-bundle-collection/src/zip.rs b/support-bundle-collection/src/zip.rs new file mode 100644 index 00000000000..b0065e8b326 --- /dev/null +++ b/support-bundle-collection/src/zip.rs @@ -0,0 +1,113 @@ +// 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/. + +//! Helpers for converting a collected bundle directory into a zipfile. +//! +//! These are used by callers that need to produce a single archive from +//! the directory of collected data — both Nexus (for storing on a sled +//! agent) and omdb (for writing to local storage). + +use ::zip::ZipWriter; +use ::zip::write::FullFileOptions; +use anyhow::Result; +use camino::Utf8DirEntry; +use camino::Utf8Path; +use camino_tempfile::Utf8TempDir; +use camino_tempfile::tempfile_in; + +/// Takes the contents of `dir`, and zips them into a single zipfile +/// stored as a tempfile under `tempdir`. +pub fn bundle_to_zipfile( + dir: &Utf8TempDir, + tempdir: &Utf8Path, +) -> Result { + let tempfile = tempfile_in(tempdir)?; + let mut zip = ZipWriter::new(tempfile); + + recursively_add_directory_to_zipfile(&mut zip, dir.path(), dir.path())?; + + Ok(zip.finish()?) +} + +fn recursively_add_directory_to_zipfile( + zip: &mut ZipWriter, + root_path: &Utf8Path, + dir_path: &Utf8Path, +) -> Result<()> { + // Readdir might return entries in a non-deterministic order. + // Let's sort it for the zipfile, to be nice. + let mut entries = dir_path + .read_dir_utf8()? + .filter_map(Result::ok) + .collect::>(); + entries.sort_by(|a, b| a.file_name().cmp(&b.file_name())); + + for entry in &entries { + // Strip the tempdir prefix when storing the path in the zipfile. + let dst = entry.path().strip_prefix(root_path)?; + + let file_type = entry.file_type()?; + if file_type.is_file() { + let src = entry.path(); + + let zip_time = entry + .path() + .metadata() + .and_then(|m| m.modified()) + .ok() + .and_then(|sys_time| jiff::Zoned::try_from(sys_time).ok()) + .and_then(|zoned| { + ::zip::DateTime::try_from(zoned.datetime()).ok() + }) + .unwrap_or_else(::zip::DateTime::default); + + let opts = FullFileOptions::default() + .last_modified_time(zip_time) + .compression_method(::zip::CompressionMethod::Deflated) + .large_file(true); + + zip.start_file_from_path(dst, opts)?; + let mut file = std::fs::File::open(&src)?; + std::io::copy(&mut file, zip)?; + } + if file_type.is_dir() { + let opts = FullFileOptions::default(); + zip.add_directory_from_path(dst, opts)?; + recursively_add_directory_to_zipfile(zip, root_path, entry.path())?; + } + } + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + + use camino_tempfile::tempdir; + + // Ensure that we can convert a temporary directory into a zipfile + #[test] + fn test_zipfile_creation() { + let dir = tempdir().unwrap(); + let tempdir_for_zip = tempdir().unwrap(); + + std::fs::create_dir_all(dir.path().join("dir-a")).unwrap(); + std::fs::create_dir_all(dir.path().join("dir-b")).unwrap(); + std::fs::write(dir.path().join("dir-a").join("file-a"), "some data") + .unwrap(); + std::fs::write(dir.path().join("file-b"), "more data").unwrap(); + + let zipfile = bundle_to_zipfile(&dir, tempdir_for_zip.path()) + .expect("Should have been able to bundle zipfile"); + let archive = ::zip::read::ZipArchive::new(zipfile).unwrap(); + + // We expect the order to be deterministically alphabetical + let mut names = archive.file_names(); + assert_eq!(names.next(), Some("dir-a/")); + assert_eq!(names.next(), Some("dir-a/file-a")); + assert_eq!(names.next(), Some("dir-b/")); + assert_eq!(names.next(), Some("file-b")); + assert_eq!(names.next(), None); + } +}