From 742c81f1e7fd858dc60c33564b74a17fbadf3fd8 Mon Sep 17 00:00:00 2001 From: cxymds Date: Tue, 27 Oct 2026 18:52:02 +0800 Subject: [PATCH] fix(admin): show pool availability diagnostics --- crates/cli/src/commands/admin/pool.rs | 20 ++++++++++++++++- crates/cli/tests/admin_pool.rs | 6 ++++++ crates/core/src/admin/cluster.rs | 31 ++++++++++++++++++++++++++- schemas/output_v2.json | 27 +++++++++++++++++++++++ 4 files changed, 82 insertions(+), 2 deletions(-) diff --git a/crates/cli/src/commands/admin/pool.rs b/crates/cli/src/commands/admin/pool.rs index 53c66744..9c76b405 100644 --- a/crates/cli/src/commands/admin/pool.rs +++ b/crates/cli/src/commands/admin/pool.rs @@ -132,6 +132,10 @@ pub(super) fn print_pool_status(pool: &PoolStatus, formatter: &Formatter) { let lifecycle_state = pool_lifecycle_state(pool); let decommission_state = pool_decommission_state(pool); let rebalance_state = status_or_default(&pool.rebalance_status, "none"); + let availability = status_or_default(&pool.availability, "unknown"); + let pool_metadata = status_or_default(&pool.pool_metadata_status, "unknown"); + let rebalance_metadata = status_or_default(&pool.rebalance_metadata_status, "unknown"); + let capacity_status = status_or_default(&pool.capacity_status, "unknown"); let used = decommission .map(|info| info.total_size.saturating_sub(info.current_size)) .or_else(|| (pool.total_size > 0).then_some(pool.used_size)) @@ -150,6 +154,19 @@ pub(super) fn print_pool_status(pool: &PoolStatus, formatter: &Formatter) { " Status: {}", style_state(lifecycle_state, formatter) )); + formatter.println(&format!( + " Availability: {}", + style_state(availability, formatter) + )); + formatter.println(&format!( + " Metadata: pool={}, rebalance={}", + style_state(pool_metadata, formatter), + style_state(rebalance_metadata, formatter) + )); + formatter.println(&format!( + " Capacity: {}", + style_state(capacity_status, formatter) + )); formatter.println(&format!( " Decommission: {}", style_state(decommission_state, formatter) @@ -227,7 +244,8 @@ pub(super) fn style_state(state: &str, formatter: &Formatter) -> String { match state { "active" | "complete" | "completed" | "decommissioned" => formatter.style_size(state), "blocked" | "failed" => formatter.theme().error.apply_to(state).to_string(), - "canceled" | "queued" | "stopping" | "stopped" => { + "offline" => formatter.theme().error.apply_to(state).to_string(), + "degraded" | "unknown" | "canceled" | "queued" | "stopping" | "stopped" => { formatter.theme().warning.apply_to(state).to_string() } "decommissioning" | "running" | "started" => formatter.style_name(state), diff --git a/crates/cli/tests/admin_pool.rs b/crates/cli/tests/admin_pool.rs index c2def37f..82059042 100644 --- a/crates/cli/tests/admin_pool.rs +++ b/crates/cli/tests/admin_pool.rs @@ -37,6 +37,10 @@ fn pool_list_dispatches_to_pool_list_json() { assert_eq!(pools[0]["decommissionStatus"], "none"); assert_eq!(pools[0]["rebalanceStatus"], "completed"); assert_eq!(pools[0]["usedSize"], 20); + assert_eq!(pools[0]["availability"], "unknown"); + assert_eq!(pools[0]["poolMetadataStatus"], "unknown"); + assert_eq!(pools[0]["rebalanceMetadataStatus"], "unknown"); + assert_eq!(pools[0]["capacityStatus"], "unknown"); let request = receiver .recv_timeout(Duration::from_secs(5)) @@ -78,6 +82,7 @@ fn pool_status_without_target_dispatches_to_pool_list_json() { assert_eq!(pools[0]["rebalanceStatus"], "none"); assert_eq!(pools[0]["decommissionInfo"]["totalSize"], 100); assert_eq!(pools[0]["decommissionInfo"]["currentSize"], 90); + assert_eq!(pools[0]["availability"], "unknown"); let request = receiver .recv_timeout(Duration::from_secs(5)) @@ -117,6 +122,7 @@ fn pool_status_dispatches_by_id_pool_json() { assert_eq!(payload["status"], "active"); assert_eq!(payload["decommissionStatus"], "none"); assert_eq!(payload["rebalanceStatus"], "failed"); + assert_eq!(payload["availability"], "unknown"); let request = receiver .recv_timeout(Duration::from_secs(5)) diff --git a/crates/core/src/admin/cluster.rs b/crates/core/src/admin/cluster.rs index 3a5c93b5..628b96c8 100644 --- a/crates/core/src/admin/cluster.rs +++ b/crates/core/src/admin/cluster.rs @@ -739,6 +739,30 @@ pub struct PoolStatus { /// Decommission status and progress for this pool. #[serde(default, rename = "decommissionInfo")] pub decommission: Option, + + /// Current storage availability derived from runtime disk/set observations. + #[serde(default = "unknown_pool_state")] + pub availability: String, + + /// Freshness/health of the pool metadata used for lifecycle fields. + #[serde(default = "unknown_pool_state", rename = "poolMetadataStatus")] + pub pool_metadata_status: String, + + /// Freshness/health of the rebalance metadata used for rebalance fields. + #[serde(default = "unknown_pool_state", rename = "rebalanceMetadataStatus")] + pub rebalance_metadata_status: String, + + /// Source quality of the capacity observation. + #[serde(default = "unknown_pool_state", rename = "capacityStatus")] + pub capacity_status: String, + + /// Time at which the diagnostic observation was captured. + #[serde(default, rename = "observedAt")] + pub observed_at: Option, +} + +fn unknown_pool_state() -> String { + "unknown".to_string() } /// Decommission status response. @@ -1112,7 +1136,7 @@ mod tests { #[test] fn test_pool_status_deserialization() { - let json = r#"{"id":1,"cmdline":"/data/pool1/disk{1...4}","lastUpdate":"2026-05-06T00:00:00Z","status":"decommissioning","decommissionStatus":"running","rebalanceStatus":"none","totalSize":1000,"currentSize":600,"usedSize":400,"used":0.4,"decommissionInfo":{"startTime":"2026-05-06T00:00:01Z","startSize":100,"totalSize":1000,"currentSize":600,"complete":false,"failed":false,"canceled":false,"queued":true,"queuedBuckets":["bucket-a"],"decommissionedBuckets":["bucket-b"],"bucket":"bucket-a","prefix":"","object":"object.txt","stage":"migrate_object","objectsDecommissioned":2,"objectsDecommissionedFailed":1,"bytesDecommissioned":128,"bytesDecommissionedFailed":64,"waitingReason":"queued"}}"#; + let json = r#"{"id":1,"cmdline":"/data/pool1/disk{1...4}","lastUpdate":"2026-05-06T00:00:00Z","status":"decommissioning","decommissionStatus":"running","rebalanceStatus":"none","totalSize":1000,"currentSize":600,"usedSize":400,"used":0.4,"decommissionInfo":{"startTime":"2026-05-06T00:00:01Z","startSize":100,"totalSize":1000,"currentSize":600,"complete":false,"failed":false,"canceled":false,"queued":true,"queuedBuckets":["bucket-a"],"decommissionedBuckets":["bucket-b"],"bucket":"bucket-a","prefix":"","object":"object.txt","stage":"migrate_object","objectsDecommissioned":2,"objectsDecommissionedFailed":1,"bytesDecommissioned":128,"bytesDecommissionedFailed":64,"waitingReason":"queued"},"availability":"offline","poolMetadataStatus":"stale","rebalanceMetadataStatus":"unknown","capacityStatus":"cached","observedAt":"2026-05-06T00:00:02Z"}"#; let status: PoolStatus = serde_json::from_str(json).unwrap(); @@ -1122,6 +1146,11 @@ mod tests { assert_eq!(status.decommission_status, "running"); assert_eq!(status.rebalance_status, "none"); assert_eq!(status.used_size, 400); + assert_eq!(status.availability, "offline"); + assert_eq!(status.pool_metadata_status, "stale"); + assert_eq!(status.rebalance_metadata_status, "unknown"); + assert_eq!(status.capacity_status, "cached"); + assert_eq!(status.observed_at.as_deref(), Some("2026-05-06T00:00:02Z")); let info = status.decommission.expect("decommission info exists"); assert!(info.queued); assert_eq!(info.queued_buckets, vec!["bucket-a"]); diff --git a/schemas/output_v2.json b/schemas/output_v2.json index 226fdab4..228df623 100644 --- a/schemas/output_v2.json +++ b/schemas/output_v2.json @@ -534,6 +534,33 @@ } ], "description": "Decommission state and progress for this pool" + }, + "availability": { + "type": "string", + "description": "Current storage availability: online, degraded, offline, or unknown" + }, + "poolMetadataStatus": { + "type": "string", + "description": "Freshness or recovery state of pool metadata" + }, + "rebalanceMetadataStatus": { + "type": "string", + "description": "Freshness or recovery state of rebalance metadata" + }, + "capacityStatus": { + "type": "string", + "description": "Source quality of the capacity observation" + }, + "observedAt": { + "oneOf": [ + { + "$ref": "#/definitions/timestamp" + }, + { + "type": "null" + } + ], + "description": "Time at which the diagnostic observation was captured" } } },