feat(connect): expose object inspect CLI (#7821)

This commit is contained in:
Chris
2026-09-14 10:54:51 +08:00
committed by GitHub
parent 9749447825
commit db9a420fd2
5 changed files with 223 additions and 6 deletions
+83
View File
@@ -143,6 +143,87 @@ pub enum ConnectCommands {
Telemetry(ConnectTelemetryOpts),
/// Capture a consent-bound local top snapshot
Top(ConnectTopOpts),
/// Inspect one local object and write a signed integrity summary
Inspect(ConnectInspectOpts),
}
#[derive(Args, Clone)]
pub struct ConnectInspectOpts {
#[command(subcommand)]
pub command: ConnectInspectCommands,
}
#[derive(Subcommand, Clone)]
pub enum ConnectInspectCommands {
/// Inspect one object's local erasure metadata and shards
Object(ConnectInspectObjectOpts),
}
#[derive(Args, Clone)]
pub struct ConnectInspectObjectOpts {
/// Directory containing an enrolled Connect device identity
#[arg(long = "state-dir")]
pub state_dir: PathBuf,
/// New local archive path; an existing file is never replaced
#[arg(long)]
pub output: PathBuf,
/// Organization resource name bound to the export
#[arg(long, value_parser = NonEmptyStringValueParser::new())]
pub organization: String,
/// Cluster resource name bound to the export
#[arg(long, value_parser = NonEmptyStringValueParser::new())]
pub cluster: String,
/// Cluster-device resource name bound to the export
#[arg(long, value_parser = NonEmptyStringValueParser::new())]
pub device: String,
/// UUIDv7 diagnostic run identifier issued by Connect
#[arg(long = "run-uid", value_parser = NonEmptyStringValueParser::new())]
pub run_uid: String,
/// UUIDv7 artifact identifier issued by Connect
#[arg(long = "artifact-uid", value_parser = NonEmptyStringValueParser::new())]
pub artifact_uid: String,
/// UUIDv7 consent identifier issued by Connect
#[arg(long = "consent-uid", value_parser = NonEmptyStringValueParser::new())]
pub consent_uid: String,
/// Consent policy revision bound to this inspection
#[arg(long = "policy-revision")]
pub policy_revision: u64,
/// Consent expiry as UTC Unix seconds
#[arg(long = "consent-expires-at")]
pub consent_expires_at_unix: i64,
/// Artifact expiry as UTC Unix seconds
#[arg(long = "expires-at")]
pub expires_at_unix: i64,
/// Drive root containing the object's local erasure state; repeat for every local drive
#[arg(long = "path", required = true, value_parser = NonEmptyStringValueParser::new())]
pub paths: Vec<PathBuf>,
/// Bucket containing the object
#[arg(long, value_parser = NonEmptyStringValueParser::new())]
pub bucket: String,
/// Object key to inspect
#[arg(long, value_parser = NonEmptyStringValueParser::new())]
pub object: String,
/// Optional exact object version UUID
#[arg(long = "version-id", value_parser = NonEmptyStringValueParser::new())]
pub version_id: Option<String>,
/// Negotiated producer schema version
#[arg(long = "schema-version", default_value_t = 1)]
pub schema_version: u16,
/// Negotiated producer capability
#[arg(long, default_value = "inspect.object@1", value_parser = NonEmptyStringValueParser::new())]
pub capability: String,
/// Maximum wall-clock duration in milliseconds
#[arg(long = "duration-millis", default_value_t = 30_000)]
pub duration_millis: u64,
/// Maximum bytes read from local metadata and shards
#[arg(long = "max-read-bytes", default_value_t = 268_435_456)]
pub max_read_bytes: u64,
/// Maximum working memory in bytes
#[arg(long = "max-memory-bytes", default_value_t = 67_108_864)]
pub max_memory_bytes: u64,
/// Confirm this explicit local L3 diagnostic operation
#[arg(long = "acknowledge-l3", required = true, action = clap::ArgAction::SetTrue)]
pub acknowledge_l3: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
@@ -1488,6 +1569,8 @@ pub enum CommandResult {
ConnectTelemetry(ConnectTelemetryCommands),
/// Consent-bound local Connect top operation
ConnectTop(ConnectTopCommands),
/// Consent-bound local object integrity export
ConnectInspect(ConnectInspectObjectOpts),
}
/// Create default ServerOpts from environment variables
+33
View File
@@ -126,6 +126,39 @@ mod tests {
}
}
#[test]
#[serial]
fn test_connect_inspect_object_parses() {
let result = Opt::parse_command([
"rustfs", "connect", "inspect", "object", "--state-dir", "/state", "--output", "/tmp/inspect.zip",
"--organization", "organizations/019e3ae0-0000-7000-8000-000000000021", "--cluster",
"organizations/019e3ae0-0000-7000-8000-000000000021/clusters/019e3ae0-0000-7000-8000-000000000022",
"--device",
"organizations/019e3ae0-0000-7000-8000-000000000021/clusters/019e3ae0-0000-7000-8000-000000000022/clusterDevices/019e3ae0-0000-7000-8000-000000000023",
"--run-uid", "019e3ae0-0000-7000-8000-000000000024", "--artifact-uid",
"019e3ae0-0000-7000-8000-000000000025", "--consent-uid",
"019e3ae0-0000-7000-8000-000000000026", "--policy-revision", "3", "--consent-expires-at",
"2000000000", "--expires-at", "2000000000", "--path", "/data/drive-1", "--path", "/data/drive-2",
"--bucket", "customer-bucket", "--object", "private/report.bin", "--acknowledge-l3",
])
.expect("connect inspect object should parse");
match result {
CommandResult::ConnectInspect(opts) => {
assert_eq!(
opts.paths,
[
std::path::PathBuf::from("/data/drive-1"),
std::path::PathBuf::from("/data/drive-2"),
]
);
assert_eq!(opts.bucket, "customer-bucket");
assert_eq!(opts.object, "private/report.bin");
}
_ => panic!("expected Connect inspect command result"),
}
}
#[test]
#[serial]
fn test_parse_from_non_server_commands_falls_back_without_panicking() {
+1
View File
@@ -56,6 +56,7 @@ pub use cli::{
ConnectClientPerformanceOperation, ConnectClientPerformanceOpts, ConnectDrivePerformanceOpts, ConnectPerformanceCommands,
};
pub use cli::{ConnectEnvironmentInventoryOpts, ConnectInventoryCommands};
pub use cli::{ConnectInspectCommands, ConnectInspectObjectOpts, ConnectInspectOpts};
pub use cli::{
ConnectLicenseArtifactOpts, ConnectLicenseCommands, ConnectLicenseRelayExportOpts, ConnectLicenseRelayImportOpts,
ConnectLicenseRenewOpts, ConnectLicenseScopeOpts,
+5 -2
View File
@@ -19,8 +19,8 @@
use super::Config;
use super::cli::{
Cli, CommandResult, Commands, ConnectCommands, ConnectInventoryCommands, ConnectPerformanceCommands, ConnectReportCommands,
ServerOpts, default_server_opts, preprocess_args_for_legacy,
Cli, CommandResult, Commands, ConnectCommands, ConnectInspectCommands, ConnectInventoryCommands, ConnectPerformanceCommands,
ConnectReportCommands, ServerOpts, default_server_opts, preprocess_args_for_legacy,
};
use crate::apply_external_env_compat;
use CommandResult::Server;
@@ -162,6 +162,9 @@ impl Opt {
ConnectCommands::Logs(opts) => Ok(CommandResult::ConnectLogs(opts)),
ConnectCommands::Telemetry(opts) => Ok(CommandResult::ConnectTelemetry(opts.command)),
ConnectCommands::Top(opts) => Ok(CommandResult::ConnectTop(opts.command)),
ConnectCommands::Inspect(opts) => match opts.command {
ConnectInspectCommands::Object(opts) => Ok(CommandResult::ConnectInspect(opts)),
},
},
Some(Commands::Server(opts)) => Self::server_command_result(Self::from_server_opts(*opts)),
None => {
+101 -4
View File
@@ -15,10 +15,11 @@
use crate::{
config::{
CommandResult, Config, ConnectClientPerformanceOperation, ConnectClientPerformanceOpts, ConnectDrivePerformanceOpts,
ConnectEnvironmentInventoryOpts, ConnectLicenseCommands, ConnectLicenseScopeOpts, ConnectLogsMode, ConnectLogsOpts,
ConnectObjectPerformanceOperation, ConnectObjectPerformanceOpts, ConnectProfileOpts, ConnectProfileTool,
ConnectRelayMaterialKind, ConnectRelayOpts, ConnectReportUploadOpts, ConnectSiteReplicationPerformanceOpts,
ConnectTelemetryArtifactOpts, ConnectTelemetryCommands, ConnectThreadProfileScope, ConnectTopCommands, Opt,
ConnectEnvironmentInventoryOpts, ConnectInspectObjectOpts, ConnectLicenseCommands, ConnectLicenseScopeOpts,
ConnectLogsMode, ConnectLogsOpts, ConnectObjectPerformanceOperation, ConnectObjectPerformanceOpts, ConnectProfileOpts,
ConnectProfileTool, ConnectRelayMaterialKind, ConnectRelayOpts, ConnectReportUploadOpts,
ConnectSiteReplicationPerformanceOpts, ConnectTelemetryArtifactOpts, ConnectTelemetryCommands, ConnectThreadProfileScope,
ConnectTopCommands, Opt,
},
startup_lifecycle::{StartupRuntimeLifecycle, run_startup_runtime_lifecycle},
startup_preflight::{StartupServerPreflightError, bootstrap_external_prefix_compat, init_startup_server_preflight},
@@ -151,6 +152,7 @@ async fn async_main() -> Result<()> {
CommandResult::ConnectLogs(options) => return execute_connect_logs(options).await,
CommandResult::ConnectTelemetry(command) => return execute_connect_telemetry(command).await,
CommandResult::ConnectTop(command) => return execute_connect_top(command).await,
CommandResult::ConnectInspect(options) => return execute_connect_inspect(options).await,
CommandResult::Server(config) => config,
};
@@ -180,6 +182,101 @@ async fn async_main() -> Result<()> {
}
}
async fn execute_connect_inspect(options: ConnectInspectObjectOpts) -> Result<()> {
use crate::connect::IdentityStore;
use crate::connect::diagnostics::{
InspectArtifactConsent, InspectProvenance, InspectRequest, InspectRule, InspectRun, export_inspect_summary,
save_signed_inspect_export,
};
use rand::{TryRng as _, rngs::SysRng};
let identity = IdentityStore::new(options.state_dir.join("identity"))
.load()
.map_err(Error::other)?
.ok_or_else(|| Error::other("connect inspect requires an enrolled device identity"))?;
let produced_at_unix = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(Error::other)
.and_then(|duration| i64::try_from(duration.as_secs()).map_err(Error::other))?;
let mut nonce = [0_u8; 32];
SysRng.try_fill_bytes(&mut nonce).map_err(Error::other)?;
let request = InspectRequest {
organization_name: options.organization,
cluster_name: options.cluster,
device_name: options.device,
run_uid: options.run_uid,
artifact_uid: options.artifact_uid,
schema_version: options.schema_version,
capability: options.capability,
consent: InspectArtifactConsent {
consent_uid: options.consent_uid,
policy_revision: options.policy_revision,
expires_at_unix: options.consent_expires_at_unix,
confirmed: options.acknowledge_l3,
},
produced_at_unix,
expires_at_unix: options.expires_at_unix,
nonce,
drive_roots: options.paths,
bucket: options.bucket,
object: options.object,
version_id: options.version_id,
rules: vec![
InspectRule::ShardBitrot,
InspectRule::ShardAvailability,
InspectRule::MetadataIdentity,
],
max_duration: Duration::from_millis(options.duration_millis),
max_read_bytes: options.max_read_bytes,
max_memory_bytes: options.max_memory_bytes,
provenance: InspectProvenance::new(
crate::version::build::COMMIT_HASH.to_string(),
hash_current_executable()?,
env!("CARGO_PKG_VERSION").to_string(),
enabled_build_features(),
),
};
let cancel = CancellationToken::new();
let worker_cancel = cancel.clone();
let worker_request = request.clone();
let mut worker = tokio::task::spawn_blocking(move || export_inspect_summary(&worker_request, &identity, &worker_cancel));
let run = tokio::select! {
biased;
signal = tokio::signal::ctrl_c() => {
signal.map_err(Error::other)?;
cancel.cancel();
worker.await.map_err(Error::other)?.map_err(Error::other)?
}
result = &mut worker => result.map_err(Error::other)?.map_err(Error::other)?,
};
match run {
InspectRun::Terminal(result) => {
println!("result={}", serde_json::to_string(&result).map_err(Error::other)?);
Err(Error::other("inspect collection did not produce an artifact"))
}
InspectRun::Signed(export) => {
let output = options.output;
let writer_cancel = cancel.clone();
let mut writer = tokio::task::spawn_blocking(move || save_signed_inspect_export(&output, &export, &writer_cancel));
let receipt = tokio::select! {
biased;
signal = tokio::signal::ctrl_c() => {
signal.map_err(Error::other)?;
cancel.cancel();
writer.await.map_err(Error::other)?.map_err(Error::other)?
}
result = &mut writer => result.map_err(Error::other)?.map_err(Error::other)?,
};
println!(
"artifact={} bytes={} sha256={}",
receipt.artifact_uid, receipt.archive_size_bytes, receipt.archive_sha256
);
println!("upload=not-performed");
Ok(())
}
}
}
async fn execute_connect_environment_inventory(options: ConnectEnvironmentInventoryOpts) -> Result<()> {
use crate::connect::environment::collect_environment;
use crate::connect::inventory::InventoryStateStore;