Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion crates/cli/src/commands/admin/pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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)
Expand Down Expand Up @@ -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),
Expand Down
6 changes: 6 additions & 0 deletions crates/cli/tests/admin_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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))
Expand Down
31 changes: 30 additions & 1 deletion crates/core/src/admin/cluster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,30 @@ pub struct PoolStatus {
/// Decommission status and progress for this pool.
#[serde(default, rename = "decommissionInfo")]
pub decommission: Option<PoolDecommissionInfo>,

/// 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<String>,
}

fn unknown_pool_state() -> String {
"unknown".to_string()
}

/// Decommission status response.
Expand Down Expand Up @@ -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();

Expand All @@ -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"]);
Expand Down
27 changes: 27 additions & 0 deletions schemas/output_v2.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
},
Expand Down
Loading