From e6862c5d3dc15bbf8c7b8213c434758716c13d8b Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 12 Mar 2025 15:01:39 +0100 Subject: [PATCH] cli: uniformize output and add some infos --- doc/api/garage-admin-v2.json | 17 +- src/api/admin/Cargo.toml | 1 + src/api/admin/api.rs | 9 +- src/api/admin/block.rs | 6 +- src/api/admin/cluster.rs | 13 +- src/api/admin/node.rs | 79 +++--- src/garage/Cargo.toml | 2 +- src/garage/cli/local/init.rs | 10 - src/garage/cli/remote/admin_token.rs | 22 +- src/garage/cli/remote/block.rs | 42 ++- src/garage/cli/remote/bucket.rs | 379 ++++++++++++--------------- src/garage/cli/remote/cluster.rs | 8 +- src/garage/cli/remote/key.rs | 57 ++-- src/garage/cli/remote/layout.rs | 6 +- src/garage/cli/remote/mod.rs | 12 + src/garage/cli/remote/node.rs | 25 +- src/rpc/layout/version.rs | 2 +- src/rpc/system.rs | 9 + src/table/data.rs | 5 + src/table/metrics.rs | 16 ++ 20 files changed, 391 insertions(+), 329 deletions(-) diff --git a/doc/api/garage-admin-v2.json b/doc/api/garage-admin-v2.json index fbb1f6c5..31aaa915 100644 --- a/doc/api/garage-admin-v2.json +++ b/doc/api/garage-admin-v2.json @@ -1752,7 +1752,8 @@ "type": "object", "required": [ "versionId", - "deleted", + "refDeleted", + "versionDeleted", "garbageCollected" ], "properties": { @@ -1766,10 +1767,13 @@ } ] }, - "deleted": { + "garbageCollected": { "type": "boolean" }, - "garbageCollected": { + "refDeleted": { + "type": "boolean" + }, + "versionDeleted": { "type": "boolean" }, "versionId": { @@ -3516,6 +3520,13 @@ "type": "boolean", "description": "Whether this node is part of an older layout version and is draining data." }, + "garageVersion": { + "type": [ + "string", + "null" + ], + "description": "Garage version" + }, "hostname": { "type": [ "string", diff --git a/src/api/admin/Cargo.toml b/src/api/admin/Cargo.toml index 65d9fda9..92d041cc 100644 --- a/src/api/admin/Cargo.toml +++ b/src/api/admin/Cargo.toml @@ -47,3 +47,4 @@ prometheus = { workspace = true, optional = true } [features] metrics = [ "opentelemetry-prometheus", "prometheus" ] +k2v = [ "garage_model/k2v" ] diff --git a/src/api/admin/api.rs b/src/api/admin/api.rs index 3694fd67..b865ac88 100644 --- a/src/api/admin/api.rs +++ b/src/api/admin/api.rs @@ -188,8 +188,8 @@ pub struct GetClusterStatusResponse { pub struct NodeResp { /// Full-length node identifier pub id: String, - /// Role assigned to this node in the current cluster layout - pub role: Option, + /// Garage version + pub garage_version: Option, /// Socket address used by other nodes to connect to this node for RPC #[schema(value_type = Option)] pub addr: Option, @@ -200,6 +200,8 @@ pub struct NodeResp { /// For disconnected nodes, the number of seconds since last contact, /// or `null` if no contact was established since Garage restarted. pub last_seen_secs_ago: Option, + /// Role assigned to this node in the current cluster layout + pub role: Option, /// Whether this node is part of an older layout version and is draining data. pub draining: bool, /// Total and available space on the disk partition(s) containing the data @@ -1174,7 +1176,8 @@ pub struct LocalGetBlockInfoResponse { #[serde(rename_all = "camelCase")] pub struct BlockVersion { pub version_id: String, - pub deleted: bool, + pub ref_deleted: bool, + pub version_deleted: bool, pub garbage_collected: bool, pub backlink: Option, } diff --git a/src/api/admin/block.rs b/src/api/admin/block.rs index 73d186a6..4b8edc63 100644 --- a/src/api/admin/block.rs +++ b/src/api/admin/block.rs @@ -84,14 +84,16 @@ impl RequestHandler for LocalGetBlockInfoRequest { }; versions.push(BlockVersion { version_id: hex::encode(&br.version), - deleted: v.deleted.get(), + ref_deleted: br.deleted.get(), + version_deleted: v.deleted.get(), garbage_collected: false, backlink: Some(bl), }); } else { versions.push(BlockVersion { version_id: hex::encode(&br.version), - deleted: true, + ref_deleted: br.deleted.get(), + version_deleted: true, garbage_collected: true, backlink: None, }); diff --git a/src/api/admin/cluster.rs b/src/api/admin/cluster.rs index 6a555d04..09f59d63 100644 --- a/src/api/admin/cluster.rs +++ b/src/api/admin/cluster.rs @@ -33,6 +33,7 @@ impl RequestHandler for GetClusterStatusRequest { i.id, NodeResp { id: hex::encode(i.id), + garage_version: i.status.garage_version, addr: i.addr, hostname: i.status.hostname, is_up: i.is_up, @@ -231,12 +232,16 @@ impl RequestHandler for GetClusterStatisticsRequest { if meta_part_avail.len() < node_partition_count.len() || data_part_avail.len() < node_partition_count.len() { - writeln!(&mut ret, " data: < {}", data_avail).unwrap(); - writeln!(&mut ret, " metadata: < {}", meta_avail).unwrap(); + ret += &format_table_to_string(vec![ + format!(" data: < {}", data_avail), + format!(" metadata: < {}", meta_avail), + ]); writeln!(&mut ret, "A precise estimate could not be given as information is missing for some storage nodes.").unwrap(); } else { - writeln!(&mut ret, " data: {}", data_avail).unwrap(); - writeln!(&mut ret, " metadata: {}", meta_avail).unwrap(); + ret += &format_table_to_string(vec![ + format!(" data: {}", data_avail), + format!(" metadata: {}", meta_avail), + ]); } } diff --git a/src/api/admin/node.rs b/src/api/admin/node.rs index 9994cfd0..fcc7e4d3 100644 --- a/src/api/admin/node.rs +++ b/src/api/admin/node.rs @@ -55,27 +55,48 @@ impl RequestHandler for LocalGetNodeStatisticsRequest { garage: &Arc, _admin: &Admin, ) -> Result { - let mut ret = String::new(); - writeln!( - &mut ret, - "Garage version: {} [features: {}]\nRust compiler version: {}", - garage_util::version::garage_version(), - garage_util::version::garage_features() - .map(|list| list.join(", ")) - .unwrap_or_else(|| "(unknown)".into()), - garage_util::version::rust_version(), - ) - .unwrap(); + let sys_status = garage.system.local_status(); - writeln!(&mut ret, "\nDatabase engine: {}", garage.db.engine()).unwrap(); + let mut ret = format_table_to_string(vec![ + format!("Node ID:\t{:?}", garage.system.id), + format!("Hostname:\t{}", sys_status.hostname.unwrap_or_default(),), + format!( + "Garage version:\t{}", + garage_util::version::garage_version(), + ), + format!( + "Garage features:\t{}", + garage_util::version::garage_features() + .map(|list| list.join(", ")) + .unwrap_or_else(|| "(unknown)".into()), + ), + format!( + "Rust compiler version:\t{}", + garage_util::version::rust_version(), + ), + format!("Database engine:\t{}", garage.db.engine()), + ]); // Gather table statistics - let mut table = vec![" Table\tItems\tMklItems\tMklTodo\tGcTodo".into()]; + let mut table = vec![" Table\tItems\tMklItems\tMklTodo\tInsQueue\tGcTodo".into()]; + table.push(gather_table_stats(&garage.admin_token_table)?); table.push(gather_table_stats(&garage.bucket_table)?); + table.push(gather_table_stats(&garage.bucket_alias_table)?); table.push(gather_table_stats(&garage.key_table)?); + table.push(gather_table_stats(&garage.object_table)?); + table.push(gather_table_stats(&garage.object_counter_table.table)?); + table.push(gather_table_stats(&garage.mpu_table)?); + table.push(gather_table_stats(&garage.mpu_counter_table.table)?); table.push(gather_table_stats(&garage.version_table)?); table.push(gather_table_stats(&garage.block_ref_table)?); + + #[cfg(feature = "k2v")] + { + table.push(gather_table_stats(&garage.k2v.item_table)?); + table.push(gather_table_stats(&garage.k2v.counter_table.table)?); + } + write!( &mut ret, "\nTable stats:\n{}", @@ -87,24 +108,17 @@ impl RequestHandler for LocalGetNodeStatisticsRequest { writeln!(&mut ret, "\nBlock manager stats:").unwrap(); let rc_len = garage.block_manager.rc_len()?.to_string(); - writeln!( - &mut ret, - " number of RC entries (~= number of blocks): {}", - rc_len - ) - .unwrap(); - writeln!( - &mut ret, - " resync queue length: {}", - garage.block_manager.resync.queue_len()? - ) - .unwrap(); - writeln!( - &mut ret, - " blocks with resync errors: {}", - garage.block_manager.resync.errors_len()? - ) - .unwrap(); + ret += &format_table_to_string(vec![ + format!(" number of RC entries:\t{} (~= number of blocks)", rc_len), + format!( + " resync queue length:\t{}", + garage.block_manager.resync.queue_len()? + ), + format!( + " blocks with resync errors:\t{}", + garage.block_manager.resync.errors_len()? + ), + ]); Ok(LocalGetNodeStatisticsResponse { freeform: ret }) } @@ -119,11 +133,12 @@ where let mkl_len = t.merkle_updater.merkle_tree_len()?.to_string(); Ok(format!( - " {}\t{}\t{}\t{}\t{}", + " {}\t{}\t{}\t{}\t{}\t{}", F::TABLE_NAME, data_len, mkl_len, t.merkle_updater.todo_len()?, + t.data.insert_queue_len()?, t.data.gc_todo_len()? )) } diff --git a/src/garage/Cargo.toml b/src/garage/Cargo.toml index 045a6174..2ce4fe52 100644 --- a/src/garage/Cargo.toml +++ b/src/garage/Cargo.toml @@ -85,7 +85,7 @@ k2v-client.workspace = true [features] default = [ "bundled-libs", "metrics", "lmdb", "sqlite", "k2v" ] -k2v = [ "garage_util/k2v", "garage_api_k2v" ] +k2v = [ "garage_util/k2v", "garage_api_k2v", "garage_api_admin/k2v" ] # Database engines lmdb = [ "garage_model/lmdb" ] diff --git a/src/garage/cli/local/init.rs b/src/garage/cli/local/init.rs index 43ca5c09..683930ca 100644 --- a/src/garage/cli/local/init.rs +++ b/src/garage/cli/local/init.rs @@ -36,16 +36,6 @@ pub fn node_id_command(config_file: PathBuf, quiet: bool) -> Result<(), Error> { ); eprintln!(" garage [-c ] node connect {}", idstr); eprintln!(); - eprintln!("Or instruct them to connect from here by running:"); - eprintln!( - " garage -c {} -h node connect {}", - config_file.to_string_lossy(), - idstr - ); - eprintln!( - "where is their own node identifier in the format: @:" - ); - eprintln!(); eprintln!("This node identifier can also be added as a bootstrap node in other node's garage.toml files:"); eprintln!(" bootstrap_peers = ["); eprintln!(" \"{}\",", idstr); diff --git a/src/garage/cli/remote/admin_token.rs b/src/garage/cli/remote/admin_token.rs index 78286dc4..09699ad7 100644 --- a/src/garage/cli/remote/admin_token.rs +++ b/src/garage/cli/remote/admin_token.rs @@ -34,14 +34,12 @@ impl Cli { list.0.sort_by_key(|x| x.created); - let mut table = vec!["ID\tCREATED\tNAME\tEXPIRATION\tSCOPE".to_string()]; + let mut table = vec!["ID\tCreated\tName\tExpiration\tScope".to_string()]; for tok in list.0.iter() { let scope = if tok.expired { String::new() - } else if tok.scope.len() > 1 { - format!("[{}]", tok.scope.len()) } else { - tok.scope.get(0).cloned().unwrap_or_default() + table_list_abbr(&tok.scope) }; let exp = if tok.expired { "expired".to_string() @@ -233,7 +231,7 @@ impl Cli { } fn print_token_info(token: &GetAdminTokenInfoResponse) { - format_table(vec![ + let mut table = vec![ format!("ID:\t{}", token.id.as_ref().unwrap()), format!("Name:\t{}", token.name), format!("Created:\t{}", token.created.unwrap().with_timezone(&Local)), @@ -248,6 +246,16 @@ fn print_token_info(token: &GetAdminTokenInfoResponse) { .map(|x| x.with_timezone(&Local).to_string()) .unwrap_or("never".into()) ), - format!("Scope:\t{}", token.scope.to_vec().join(", ")), - ]); + String::new(), + ]; + + for (i, scope) in token.scope.iter().enumerate() { + if i == 0 { + table.push(format!("Scope:\t{}", scope)); + } else { + table.push(format!("\t{}", scope)); + } + } + + format_table(table); } diff --git a/src/garage/cli/remote/block.rs b/src/garage/cli/remote/block.rs index 933dcbdb..f70decd7 100644 --- a/src/garage/cli/remote/block.rs +++ b/src/garage/cli/remote/block.rs @@ -51,46 +51,70 @@ impl Cli { .local_api_request(LocalGetBlockInfoRequest { block_hash: hash }) .await?; - println!("Block hash: {}", info.block_hash); - println!("Refcount: {}", info.refcount); + println!("==== BLOCK INFORMATION ===="); + format_table(vec![ + format!("Block hash:\t{}", info.block_hash), + format!("Refcount:\t{}", info.refcount), + ]); println!(); - let mut table = vec!["Version\tBucket\tKey\tMPU\tDeleted".into()]; + println!("==== REFERENCES TO THIS BLOCK ===="); + let mut table = vec!["Status\tVersion\tBucket\tKey\tMPU".into()]; let mut nondeleted_count = 0; + let mut inconsistent_refs = false; for ver in info.versions.iter() { match &ver.backlink { Some(BlockVersionBacklink::Object { bucket_id, key }) => { table.push(format!( - "{:.16}\t{:.16}\t{}\t\t{:?}", - ver.version_id, bucket_id, key, ver.deleted + "{}\t{:.16}{}\t{:.16}\t{}", + ver.ref_deleted.then_some("deleted").unwrap_or("active"), + ver.version_id, + ver.version_deleted + .then_some(" (deleted)") + .unwrap_or_default(), + bucket_id, + key )); } Some(BlockVersionBacklink::Upload { upload_id, - upload_deleted: _, + upload_deleted, upload_garbage_collected: _, bucket_id, key, }) => { table.push(format!( - "{:.16}\t{:.16}\t{}\t{:.16}\t{:.16}", + "{}\t{:.16}{}\t{:.16}\t{}\t{:.16}{}", + ver.ref_deleted.then_some("deleted").unwrap_or("active"), ver.version_id, + ver.version_deleted + .then_some(" (deleted)") + .unwrap_or_default(), bucket_id.as_deref().unwrap_or(""), key.as_deref().unwrap_or(""), upload_id, - ver.deleted + upload_deleted.then_some(" (deleted)").unwrap_or_default(), )); } None => { table.push(format!("{:.16}\t\t\tyes", ver.version_id)); } } - if !ver.deleted { + if ver.ref_deleted != ver.version_deleted { + inconsistent_refs = true; + } + if !ver.ref_deleted { nondeleted_count += 1; } } format_table(table); + if inconsistent_refs { + println!(); + println!("There are inconsistencies between the block_ref and the version tables."); + println!("Fix them by running `garage repair block-refs`"); + } + if info.refcount != nondeleted_count { println!(); println!( diff --git a/src/garage/cli/remote/bucket.rs b/src/garage/cli/remote/bucket.rs index 9adcdbe5..09e3de64 100644 --- a/src/garage/cli/remote/bucket.rs +++ b/src/garage/cli/remote/bucket.rs @@ -30,21 +30,18 @@ impl Cli { pub async fn cmd_list_buckets(&self) -> Result<(), Error> { let buckets = self.api_request(ListBucketsRequest).await?; - println!("List of buckets:"); - - let mut table = vec![]; + let mut table = vec!["ID\tGlobal aliases\tLocal aliases".to_string()]; for bucket in buckets.0.iter() { - let local_aliases_n = match &bucket.local_aliases[..] { - [] => "".into(), - [alias] => format!("{}:{}", alias.access_key_id, alias.alias), - s => format!("[{} local aliases]", s.len()), - }; - table.push(format!( - "\t{}\t{}\t{}", - bucket.global_aliases.join(","), - local_aliases_n, + "{:.16}\t{}\t{}", bucket.id, + table_list_abbr(&bucket.global_aliases), + table_list_abbr( + bucket + .local_aliases + .iter() + .map(|x| format!("{}:{}", x.access_key_id, x.alias)) + ), )); } format_table(table); @@ -61,88 +58,20 @@ impl Cli { }) .await?; - println!("Bucket: {}", bucket.id); - - let size = bytesize::ByteSize::b(bucket.bytes as u64); - println!( - "\nSize: {} ({})", - size.to_string_as(true), - size.to_string_as(false) - ); - println!("Objects: {}", bucket.objects); - println!( - "Unfinished uploads (multipart and non-multipart): {}", - bucket.unfinished_uploads, - ); - println!( - "Unfinished multipart uploads: {}", - bucket.unfinished_multipart_uploads - ); - let mpu_size = bytesize::ByteSize::b(bucket.unfinished_multipart_uploads as u64); - println!( - "Size of unfinished multipart uploads: {} ({})", - mpu_size.to_string_as(true), - mpu_size.to_string_as(false), - ); - - println!("\nWebsite access: {}", bucket.website_access); - - if bucket.quotas.max_size.is_some() || bucket.quotas.max_objects.is_some() { - println!("\nQuotas:"); - if let Some(ms) = bucket.quotas.max_size { - let ms = bytesize::ByteSize::b(ms); - println!( - " maximum size: {} ({})", - ms.to_string_as(true), - ms.to_string_as(false) - ); - } - if let Some(mo) = bucket.quotas.max_objects { - println!(" maximum number of objects: {}", mo); - } - } - - println!("\nGlobal aliases:"); - for alias in bucket.global_aliases { - println!(" {}", alias); - } - - println!("\nKey-specific aliases:"); - let mut table = vec![]; - for key in bucket.keys.iter() { - for alias in key.bucket_local_aliases.iter() { - table.push(format!("\t{} ({})\t{}", key.access_key_id, key.name, alias)); - } - } - format_table(table); - - println!("\nAuthorized keys:"); - let mut table = vec![]; - for key in bucket.keys.iter() { - if !(key.permissions.read || key.permissions.write || key.permissions.owner) { - continue; - } - let rflag = if key.permissions.read { "R" } else { " " }; - let wflag = if key.permissions.write { "W" } else { " " }; - let oflag = if key.permissions.owner { "O" } else { " " }; - table.push(format!( - "\t{}{}{}\t{}\t{}", - rflag, wflag, oflag, key.access_key_id, key.name - )); - } - format_table(table); + print_bucket_info(&bucket); Ok(()) } pub async fn cmd_create_bucket(&self, opt: BucketOpt) -> Result<(), Error> { - self.api_request(CreateBucketRequest { - global_alias: Some(opt.name.clone()), - local_alias: None, - }) - .await?; + let bucket = self + .api_request(CreateBucketRequest { + global_alias: Some(opt.name.clone()), + local_alias: None, + }) + .await?; - println!("Bucket {} was created.", opt.name); + print_bucket_info(&bucket.0); Ok(()) } @@ -200,7 +129,7 @@ impl Cli { }) .await?; - if let Some(key_pat) = &opt.local { + let res = if let Some(key_pat) = &opt.local { let key = self .api_request(GetKeyInfoRequest { search: Some(key_pat.clone()), @@ -216,12 +145,7 @@ impl Cli { access_key_id: key.access_key_id.clone(), }, }) - .await?; - - println!( - "Alias {} now points to bucket {:.16} in namespace of key {}", - opt.new_name, bucket.id, key.access_key_id - ) + .await? } else { self.api_request(AddBucketAliasRequest { bucket_id: bucket.id.clone(), @@ -229,19 +153,16 @@ impl Cli { global_alias: opt.new_name.clone(), }, }) - .await?; + .await? + }; - println!( - "Alias {} now points to bucket {:.16}", - opt.new_name, bucket.id - ) - } + print_bucket_info(&res.0); Ok(()) } pub async fn cmd_unalias_bucket(&self, opt: UnaliasBucketOpt) -> Result<(), Error> { - if let Some(key_pat) = &opt.local { + let res = if let Some(key_pat) = &opt.local { let key = self .api_request(GetKeyInfoRequest { search: Some(key_pat.clone()), @@ -266,12 +187,7 @@ impl Cli { local_alias: opt.name.clone(), }, }) - .await?; - - println!( - "Alias {} no longer points to bucket {:.16} in namespace of key {}", - &opt.name, bucket.id, key.access_key_id - ) + .await? } else { let bucket = self .api_request(GetBucketInfoRequest { @@ -287,13 +203,10 @@ impl Cli { global_alias: opt.name.clone(), }, }) - .await?; + .await? + }; - println!( - "Alias {} no longer points to bucket {:.16}", - opt.name, bucket.id - ) - } + print_bucket_info(&res.0); Ok(()) } @@ -315,44 +228,19 @@ impl Cli { }) .await?; - self.api_request(AllowBucketKeyRequest(BucketKeyPermChangeRequest { - bucket_id: bucket.id.clone(), - access_key_id: key.access_key_id.clone(), - permissions: ApiBucketKeyPerm { - read: opt.read, - write: opt.write, - owner: opt.owner, - }, - })) - .await?; - - let new_bucket = self - .api_request(GetBucketInfoRequest { - id: Some(bucket.id), - global_alias: None, - search: None, - }) + let res = self + .api_request(AllowBucketKeyRequest(BucketKeyPermChangeRequest { + bucket_id: bucket.id.clone(), + access_key_id: key.access_key_id.clone(), + permissions: ApiBucketKeyPerm { + read: opt.read, + write: opt.write, + owner: opt.owner, + }, + })) .await?; - if let Some(new_key) = new_bucket - .keys - .iter() - .find(|k| k.access_key_id == key.access_key_id) - { - println!( - "New permissions for key {} on bucket {:.16}:\n read {}\n write {}\n owner {}", - key.access_key_id, - new_bucket.id, - new_key.permissions.read, - new_key.permissions.write, - new_key.permissions.owner - ); - } else { - println!( - "Access key {} has no permissions on bucket {:.16}", - key.access_key_id, new_bucket.id - ); - } + print_bucket_info(&res.0); Ok(()) } @@ -374,44 +262,19 @@ impl Cli { }) .await?; - self.api_request(DenyBucketKeyRequest(BucketKeyPermChangeRequest { - bucket_id: bucket.id.clone(), - access_key_id: key.access_key_id.clone(), - permissions: ApiBucketKeyPerm { - read: opt.read, - write: opt.write, - owner: opt.owner, - }, - })) - .await?; - - let new_bucket = self - .api_request(GetBucketInfoRequest { - id: Some(bucket.id), - global_alias: None, - search: None, - }) + let res = self + .api_request(DenyBucketKeyRequest(BucketKeyPermChangeRequest { + bucket_id: bucket.id.clone(), + access_key_id: key.access_key_id.clone(), + permissions: ApiBucketKeyPerm { + read: opt.read, + write: opt.write, + owner: opt.owner, + }, + })) .await?; - if let Some(new_key) = new_bucket - .keys - .iter() - .find(|k| k.access_key_id == key.access_key_id) - { - println!( - "New permissions for key {} on bucket {:.16}:\n read {}\n write {}\n owner {}", - key.access_key_id, - new_bucket.id, - new_key.permissions.read, - new_key.permissions.write, - new_key.permissions.owner - ); - } else { - println!( - "Access key {} no longer has permissions on bucket {:.16}", - key.access_key_id, new_bucket.id - ); - } + print_bucket_info(&res.0); Ok(()) } @@ -447,20 +310,17 @@ impl Cli { } }; - self.api_request(UpdateBucketRequest { - id: bucket.id, - body: UpdateBucketRequestBody { - website_access: Some(wa), - quotas: None, - }, - }) - .await?; + let res = self + .api_request(UpdateBucketRequest { + id: bucket.id, + body: UpdateBucketRequestBody { + website_access: Some(wa), + quotas: None, + }, + }) + .await?; - if opt.allow { - println!("Website access allowed for {}", &opt.bucket); - } else { - println!("Website access denied for {}", &opt.bucket); - } + print_bucket_info(&res.0); Ok(()) } @@ -500,16 +360,17 @@ impl Cli { }, }; - self.api_request(UpdateBucketRequest { - id: bucket.id.clone(), - body: UpdateBucketRequestBody { - website_access: None, - quotas: Some(new_quotas), - }, - }) - .await?; + let res = self + .api_request(UpdateBucketRequest { + id: bucket.id.clone(), + body: UpdateBucketRequestBody { + website_access: None, + quotas: Some(new_quotas), + }, + }) + .await?; - println!("Quotas updated for bucket {:.16}", bucket.id); + print_bucket_info(&res.0); Ok(()) } @@ -547,3 +408,105 @@ impl Cli { Ok(()) } } + +fn print_bucket_info(bucket: &GetBucketInfoResponse) { + println!("==== BUCKET INFORMATION ===="); + + let mut info = vec![ + format!("Bucket:\t{}", bucket.id), + String::new(), + { + let size = bytesize::ByteSize::b(bucket.bytes as u64); + format!( + "Size:\t{} ({})", + size.to_string_as(true), + size.to_string_as(false) + ) + }, + format!("Objects:\t{}", bucket.objects), + ]; + + if bucket.unfinished_uploads > 0 { + info.extend([ + format!( + "Unfinished uploads:\t{} multipart uploads", + bucket.unfinished_multipart_uploads + ), + format!("\t{} including regular uploads", bucket.unfinished_uploads), + { + let mpu_size = + bytesize::ByteSize::b(bucket.unfinished_multipart_upload_bytes as u64); + format!( + "Size of unfinished multipart uploads:\t{} ({})", + mpu_size.to_string_as(true), + mpu_size.to_string_as(false), + ) + }, + ]); + } + + info.extend([ + String::new(), + format!("Website access:\t{}", bucket.website_access), + ]); + + if let Some(wc) = &bucket.website_config { + info.extend([ + format!(" index document:\t{}", wc.index_document), + format!( + " error document:\t{}", + wc.error_document.as_deref().unwrap_or("(not defined)") + ), + ]); + } + + if bucket.quotas.max_size.is_some() || bucket.quotas.max_objects.is_some() { + info.push(String::new()); + info.push("Quotas:\tenabled".into()); + if let Some(ms) = bucket.quotas.max_size { + let ms = bytesize::ByteSize::b(ms); + info.push(format!( + " maximum size:\t{} ({})", + ms.to_string_as(true), + ms.to_string_as(false) + )); + } + if let Some(mo) = bucket.quotas.max_objects { + info.push(format!(" maximum number of objects:\t{}", mo)); + } + } + + if !bucket.global_aliases.is_empty() { + info.push(String::new()); + for (i, alias) in bucket.global_aliases.iter().enumerate() { + if i == 0 && bucket.global_aliases.len() > 1 { + info.push(format!("Global aliases:\t{}", alias)); + } else if i == 0 { + info.push(format!("Global alias:\t{}", alias)); + } else { + info.push(format!("\t{}", alias)); + } + } + } + + format_table(info); + + println!(""); + println!("==== KEYS FOR THIS BUCKET ===="); + let mut key_info = vec!["Permissions\tAccess key\t\tLocal aliases".to_string()]; + key_info.extend(bucket.keys.iter().map(|key| { + let rflag = if key.permissions.read { "R" } else { " " }; + let wflag = if key.permissions.write { "W" } else { " " }; + let oflag = if key.permissions.owner { "O" } else { " " }; + format!( + "{}{}{}\t{}\t{}\t{}", + rflag, + wflag, + oflag, + key.access_key_id, + key.name, + key.bucket_local_aliases.to_vec().join(","), + ) + })); + format_table(key_info); +} diff --git a/src/garage/cli/remote/cluster.rs b/src/garage/cli/remote/cluster.rs index 9639df8b..78d24245 100644 --- a/src/garage/cli/remote/cluster.rs +++ b/src/garage/cli/remote/cluster.rs @@ -16,7 +16,7 @@ impl Cli { println!("==== HEALTHY NODES ===="); let mut healthy_nodes = - vec!["ID\tHostname\tAddress\tTags\tZone\tCapacity\tDataAvail".to_string()]; + vec!["ID\tHostname\tAddress\tTags\tZone\tCapacity\tDataAvail\tVersion".to_string()]; for adv in status.nodes.iter().filter(|adv| adv.is_up) { let host = adv.hostname.as_deref().unwrap_or("?"); @@ -35,7 +35,7 @@ impl Cli { None => "?".into(), }; healthy_nodes.push(format!( - "{id:.16}\t{host}\t{addr}\t[{tags}]\t{zone}\t{capacity}\t{data_avail}", + "{id:.16}\t{host}\t{addr}\t[{tags}]\t{zone}\t{capacity}\t{data_avail}\t{version}", id = adv.id, host = host, addr = addr, @@ -43,6 +43,7 @@ impl Cli { zone = cfg.zone, capacity = capacity_string(cfg.capacity), data_avail = data_avail, + version = adv.garage_version.as_deref().unwrap_or_default(), )); } else { let status = match layout.staged_role_changes.iter().find(|x| x.id == adv.id) { @@ -54,11 +55,12 @@ impl Cli { _ => "NO ROLE ASSIGNED", }; healthy_nodes.push(format!( - "{id:.16}\t{h}\t{addr}\t\t\t{status}", + "{id:.16}\t{h}\t{addr}\t\t\t{status}\t\t{version}", id = adv.id, h = host, addr = addr, status = status, + version = adv.garage_version.as_deref().unwrap_or_default(), )); } } diff --git a/src/garage/cli/remote/key.rs b/src/garage/cli/remote/key.rs index 67843a83..2c6981b6 100644 --- a/src/garage/cli/remote/key.rs +++ b/src/garage/cli/remote/key.rs @@ -24,10 +24,9 @@ impl Cli { pub async fn cmd_list_keys(&self) -> Result<(), Error> { let keys = self.api_request(ListKeysRequest).await?; - println!("List of keys:"); - let mut table = vec![]; + let mut table = vec!["ID\tName".to_string()]; for key in keys.0.iter() { - table.push(format!("\t{}\t{}", key.id, key.name)); + table.push(format!("{}\t{}", key.id, key.name)); } format_table(table); @@ -185,43 +184,35 @@ impl Cli { } fn print_key_info(key: &GetKeyInfoResponse) { - println!("Key name: {}", key.name); - println!("Key ID: {}", key.access_key_id); - println!( - "Secret key: {}", - key.secret_access_key.as_deref().unwrap_or("(redacted)") - ); - println!("Can create buckets: {}", key.permissions.create_bucket); + println!("==== ACCESS KEY INFORMATION ===="); - println!("\nKey-specific bucket aliases:"); - let mut table = vec![]; - for bucket in key.buckets.iter() { - for la in bucket.local_aliases.iter() { - table.push(format!( - "\t{}\t{}\t{}", - la, - bucket.global_aliases.join(","), - bucket.id - )); - } - } - format_table(table); + format_table(vec![ + format!("Key name:\t{}", key.name), + format!("Key ID:\t{}", key.access_key_id), + format!( + "Secret key:\t{}", + key.secret_access_key.as_deref().unwrap_or("(redacted)") + ), + format!("Can create buckets:\t{}", key.permissions.create_bucket), + ]); - println!("\nAuthorized buckets:"); - let mut table = vec![]; - for bucket in key.buckets.iter() { + println!(""); + println!("==== BUCKETS FOR THIS KEY ===="); + let mut bucket_info = vec!["Permissions\tID\tGlobal aliases\tLocal aliases".to_string()]; + bucket_info.extend(key.buckets.iter().map(|bucket| { let rflag = if bucket.permissions.read { "R" } else { " " }; let wflag = if bucket.permissions.write { "W" } else { " " }; let oflag = if bucket.permissions.owner { "O" } else { " " }; - table.push(format!( - "\t{}{}{}\t{}\t{}\t{:.16}", + format!( + "{}{}{}\t{:.16}\t{}\t{}", rflag, wflag, oflag, - bucket.global_aliases.join(","), + bucket.id, + table_list_abbr(&bucket.global_aliases), bucket.local_aliases.join(","), - bucket.id - )); - } - format_table(table); + ) + })); + + format_table(bucket_info); } diff --git a/src/garage/cli/remote/layout.rs b/src/garage/cli/remote/layout.rs index f350ab66..e243688b 100644 --- a/src/garage/cli/remote/layout.rs +++ b/src/garage/cli/remote/layout.rs @@ -378,7 +378,7 @@ pub fn print_cluster_layout(layout: &GetClusterLayoutResponse, empty_msg: &str) let tags = role.tags.join(","); if let (Some(capacity), Some(usable_capacity)) = (role.capacity, role.usable_capacity) { table.push(format!( - "{:.16}\t{}\t{}\t{}\t{} ({:.1}%)", + "{:.16}\t[{}]\t{}\t{}\t{} ({:.1}%)", role.id, tags, role.zone, @@ -388,7 +388,7 @@ pub fn print_cluster_layout(layout: &GetClusterLayoutResponse, empty_msg: &str) )); } else { table.push(format!( - "{:.16}\t{}\t{}\t{}", + "{:.16}\t[{}]\t{}\t{}", role.id, tags, role.zone, @@ -427,7 +427,7 @@ pub fn print_staging_role_changes(layout: &GetClusterLayoutResponse) -> bool { }) => { let tags = tags.join(","); table.push(format!( - "{:.16}\t{}\t{}\t{}", + "{:.16}\t[{}]\t{}\t{}", change.id, tags, zone, diff --git a/src/garage/cli/remote/mod.rs b/src/garage/cli/remote/mod.rs index 237b6db9..f2516427 100644 --- a/src/garage/cli/remote/mod.rs +++ b/src/garage/cli/remote/mod.rs @@ -106,3 +106,15 @@ impl Cli { Ok(resp.success.into_iter().next().unwrap().1) } } + +pub fn table_list_abbr, S: AsRef>(values: T) -> String { + let mut iter = values.into_iter(); + + match iter.next() { + Some(first) => match iter.count() { + 0 => first.as_ref().to_string(), + n => format!("{}, ... ({})", first.as_ref(), n + 1), + }, + None => String::new(), + } +} diff --git a/src/garage/cli/remote/node.rs b/src/garage/cli/remote/node.rs index 419d6bf7..d3017da7 100644 --- a/src/garage/cli/remote/node.rs +++ b/src/garage/cli/remote/node.rs @@ -22,15 +22,22 @@ impl Cli { }) .await?; - let mut table = vec![]; - for (node, err) in res.error.iter() { - table.push(format!("{:.16}\tError: {}", node, err)); - } + let mut table = vec!["Node\tResult".to_string()]; for (node, _) in res.success.iter() { table.push(format!("{:.16}\tSnapshot created", node)); } + for (node, err) in res.error.iter() { + table.push(format!("{:.16}\tError: {}", node, err)); + } format_table(table); + if !res.error.is_empty() { + return Err(Error::Message(format!( + "{} nodes returned an error", + res.error.len() + ))); + } + Ok(()) } @@ -47,19 +54,17 @@ impl Cli { .await?; for (node, res) in res.success.iter() { - println!("======================"); - println!("Stats for node {:.16}:\n", node); + println!("==== NODE [{:.16}] ====", node); println!("{}\n", res.freeform); } for (node, err) in res.error.iter() { - println!("======================"); - println!("Node {:.16}: error: {}\n", node, err); + println!("==== NODE [{:.16}] ====", node); + println!("Error: {}\n", err); } let res = self.api_request(GetClusterStatisticsRequest).await?; - println!("======================"); - println!("Cluster statistics:\n"); + println!("==== CLUSTER STATISTICS ===="); println!("{}\n", res.freeform); Ok(()) diff --git a/src/rpc/layout/version.rs b/src/rpc/layout/version.rs index b7902898..90a51de7 100644 --- a/src/rpc/layout/version.rs +++ b/src/rpc/layout/version.rs @@ -823,7 +823,7 @@ impl LayoutVersion { let total_cap_n = self.expect_get_node_capacity(&self.node_id_vec[*n]); let tags_n = (self.node_role(&self.node_id_vec[*n]).ok_or(""))?.tags_string(); table.push(format!( - " {:?}\t{}\t{} ({} new)\t{}\t{} ({:.1}%)", + " {:?}\t[{}]\t{} ({} new)\t{}\t{} ({:.1}%)", self.node_id_vec[*n], tags_n, stored_partitions[*n], diff --git a/src/rpc/system.rs b/src/rpc/system.rs index 2a52ae5d..198a5f6b 100644 --- a/src/rpc/system.rs +++ b/src/rpc/system.rs @@ -124,6 +124,9 @@ pub struct NodeStatus { /// Hostname of the node pub hostname: Option, + /// Garage version of the node + pub garage_version: Option, + /// Replication factor configured on the node pub replication_factor: usize, @@ -369,6 +372,10 @@ impl System { &self.layout_manager.rpc_helper } + pub fn local_status(&self) -> NodeStatus { + self.local_status.read().unwrap().clone() + } + // ---- Administrative operations (directly available and // also available through RPC) ---- @@ -786,6 +793,7 @@ impl NodeStatus { .into_string() .unwrap_or_else(|_| "".to_string()), ), + garage_version: Some(garage_util::version::garage_version().to_string()), replication_factor: replication_factor.into(), layout_digest: layout_manager.layout().digest(), meta_disk_avail: None, @@ -796,6 +804,7 @@ impl NodeStatus { fn unknown() -> Self { NodeStatus { hostname: None, + garage_version: None, replication_factor: 0, layout_digest: Default::default(), meta_disk_avail: None, diff --git a/src/table/data.rs b/src/table/data.rs index 09f4e008..c589c777 100644 --- a/src/table/data.rs +++ b/src/table/data.rs @@ -66,6 +66,7 @@ impl TableData { store.clone(), merkle_tree.clone(), merkle_todo.clone(), + insert_queue.clone(), gc_todo.clone(), ); @@ -367,6 +368,10 @@ impl TableData { } } + pub fn insert_queue_len(&self) -> Result { + Ok(self.insert_queue.len()?) + } + pub fn gc_todo_len(&self) -> Result { Ok(self.gc_todo.len()?) } diff --git a/src/table/metrics.rs b/src/table/metrics.rs index 7bb0959a..cbbb5bb9 100644 --- a/src/table/metrics.rs +++ b/src/table/metrics.rs @@ -7,6 +7,7 @@ pub struct TableMetrics { pub(crate) _table_size: ValueObserver, pub(crate) _merkle_tree_size: ValueObserver, pub(crate) _merkle_todo_len: ValueObserver, + pub(crate) _insert_queue_len: ValueObserver, pub(crate) _gc_todo_len: ValueObserver, pub(crate) get_request_counter: BoundCounter, @@ -26,6 +27,7 @@ impl TableMetrics { store: db::Tree, merkle_tree: db::Tree, merkle_todo: db::Tree, + insert_queue: db::Tree, gc_todo: db::Tree, ) -> Self { let meter = global::meter(table_name); @@ -72,6 +74,20 @@ impl TableMetrics { ) .with_description("Merkle tree updater TODO queue length") .init(), + _insert_queue_len: meter + .u64_value_observer( + "table.insert_queue_length", + move |observer| { + if let Ok(v) = insert_queue.len() { + observer.observe( + v as u64, + &[KeyValue::new("table_name", table_name)], + ); + } + }, + ) + .with_description("Table insert queue length") + .init(), _gc_todo_len: meter .u64_value_observer( "table.gc_todo_queue_length",