From 03e6020c6bc147a1f3f16137377730ddc767a391 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Fri, 6 Mar 2026 09:49:00 +0100 Subject: [PATCH] admin api: report avilable space numerically in GetClusterStatistics --- doc/api/garage-admin-v2.json | 21 +++++++++++-- src/api/admin/api.rs | 14 +++++++++ src/api/admin/cluster.rs | 58 ++++++++++++++++++++---------------- 3 files changed, 66 insertions(+), 27 deletions(-) diff --git a/doc/api/garage-admin-v2.json b/doc/api/garage-admin-v2.json index 15059ce4..82796e3a 100644 --- a/doc/api/garage-admin-v2.json +++ b/doc/api/garage-admin-v2.json @@ -2581,8 +2581,25 @@ "freeform" ], "properties": { + "dataAvail": { + "type": "integer", + "format": "int64", + "description": "available storage space for object data in the entire cluster, in bytes", + "minimum": 0 + }, "freeform": { - "type": "string" + "type": "string", + "description": "cluster statistics as a free-form string, kept for compatibility with nodes\nrunning older v2.x versions of garage" + }, + "incompleteInfo": { + "type": "boolean", + "description": "true if the available storage space statistics are imprecise due to missing\ninformation of disconnected nodes. When this is the case, the actual\nspace available in the cluster might be lower than the reported values." + }, + "metadataAvail": { + "type": "integer", + "format": "int64", + "description": "available storage space for object metadata in the entire cluster, in bytes", + "minimum": 0 } } }, @@ -4117,7 +4134,7 @@ "items": { "type": "string" }, - "description": "Scope of the admin API token, a list of admin endpoint names (such as\n`GetClusterStatus`, etc), or the special value `*` to allow all\nadmin endpoints. **WARNING:** Granting a scope of `CreateAdminToken` or\n`UpdateAdminToken` trivially allows for privilege escalation, and is thus\nfunctionnally equivalent to granting a scope of `*`." + "description": "Scope of the admin API token, a list of admin endpoint names (such as\n`GetClusterStatus`, etc), or the special value `*` to allow all\nadmin endpoints. **WARNING:** Granting a scope of `CreateAdminToken` or\n`UpdateAdminToken` trivially allows for privilege escalation, and is thus\nfunctionally equivalent to granting a scope of `*`." } } }, diff --git a/src/api/admin/api.rs b/src/api/admin/api.rs index 83d456a4..fb91354c 100644 --- a/src/api/admin/api.rs +++ b/src/api/admin/api.rs @@ -282,8 +282,22 @@ pub struct GetClusterHealthResponse { pub struct GetClusterStatisticsRequest; #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] pub struct GetClusterStatisticsResponse { + /// cluster statistics as a free-form string, kept for compatibility with nodes + /// running older v2.x versions of garage pub freeform: String, + /// available storage space for object data in the entire cluster, in bytes + #[serde(default)] + pub data_avail: u64, + /// available storage space for object metadata in the entire cluster, in bytes + #[serde(default)] + pub metadata_avail: u64, + /// true if the available storage space statistics are imprecise due to missing + /// information of disconnected nodes. When this is the case, the actual + /// space available in the cluster might be lower than the reported values. + #[serde(default)] + pub incomplete_info: bool, } // ---- ConnectClusterNodes ---- diff --git a/src/api/admin/cluster.rs b/src/api/admin/cluster.rs index 6a97c471..96efa7f3 100644 --- a/src/api/admin/cluster.rs +++ b/src/api/admin/cluster.rs @@ -231,33 +231,41 @@ impl RequestHandler for GetClusterStatisticsRequest { .map(|c| c.0 / *parts) }) .collect::>(); - if !meta_part_avail.is_empty() && !data_part_avail.is_empty() { - let meta_avail = - bytesize::ByteSize(meta_part_avail.iter().min().unwrap() * (1 << PARTITION_BITS)); - let data_avail = - bytesize::ByteSize(data_part_avail.iter().min().unwrap() * (1 << PARTITION_BITS)); - writeln!( - &mut ret, - "\nEstimated available storage space cluster-wide (might be lower in practice):" - ) - .unwrap(); - if meta_part_avail.len() < node_partition_count.len() - || data_part_avail.len() < node_partition_count.len() - { - 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 { - ret += &format_table_to_string(vec![ - format!(" data: {}", data_avail), - format!(" metadata: {}", meta_avail), - ]); - } + + let metadata_avail: u64 = + meta_part_avail.iter().min().unwrap_or(&0) * (1 << PARTITION_BITS); + let data_avail: u64 = data_part_avail.iter().min().unwrap_or(&0) * (1 << PARTITION_BITS); + + let metadata_avail_str = bytesize::ByteSize(metadata_avail); + let data_avail_str = bytesize::ByteSize(data_avail); + + let incomplete_info = meta_part_avail.len() < node_partition_count.len() + || data_part_avail.len() < node_partition_count.len(); + + writeln!( + &mut ret, + "\nEstimated available storage space cluster-wide (might be lower in practice):" + ) + .unwrap(); + if incomplete_info { + ret += &format_table_to_string(vec![ + format!(" data: < {}", data_avail_str), + format!(" metadata: < {}", metadata_avail_str), + ]); + writeln!(&mut ret, "A precise estimate could not be given as information is missing for some storage nodes.").unwrap(); + } else { + ret += &format_table_to_string(vec![ + format!(" data: {}", data_avail_str), + format!(" metadata: {}", metadata_avail_str), + ]); } - Ok(GetClusterStatisticsResponse { freeform: ret }) + Ok(GetClusterStatisticsResponse { + freeform: ret, + metadata_avail, + data_avail, + incomplete_info, + }) } }