mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-19 10:03:13 +08:00
feat(connect): expose object inspect CLI (#7821)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user