use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ServerInfo {
#[serde(default)]
pub state: String,
#[serde(default)]
pub endpoint: String,
#[serde(default)]
pub scheme: String,
#[serde(default)]
pub uptime: u64,
#[serde(default)]
pub version: String,
#[serde(default, rename = "commitID")]
pub commit_id: String,
#[serde(default)]
pub network: HashMap<String, String>,
#[serde(default, rename = "drives")]
pub disks: Vec<DiskInfo>,
#[serde(default, rename = "poolNumber")]
pub pool_number: i32,
#[serde(default, rename = "mem_stats")]
pub mem_stats: MemStats,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct DiskInfo {
#[serde(default)]
pub endpoint: String,
#[serde(default, rename = "rootDisk")]
pub root_disk: bool,
#[serde(default, rename = "path")]
pub drive_path: String,
#[serde(default)]
pub healing: bool,
#[serde(default)]
pub scanning: bool,
#[serde(default)]
pub state: String,
#[serde(default)]
pub uuid: String,
#[serde(default, rename = "totalspace")]
pub total_space: u64,
#[serde(default, rename = "usedspace")]
pub used_space: u64,
#[serde(default, rename = "availspace")]
pub available_space: u64,
#[serde(default)]
pub pool_index: i32,
#[serde(default)]
pub set_index: i32,
#[serde(default)]
pub disk_index: i32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub heal_info: Option<HealingDiskInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct HealingDiskInfo {
#[serde(default)]
pub id: String,
#[serde(default)]
pub heal_id: String,
#[serde(default)]
pub pool_index: Option<usize>,
#[serde(default)]
pub set_index: Option<usize>,
#[serde(default)]
pub disk_index: Option<usize>,
#[serde(default)]
pub endpoint: String,
#[serde(default)]
pub path: String,
#[serde(default)]
pub objects_total_count: u64,
#[serde(default)]
pub objects_total_size: u64,
#[serde(default)]
pub items_healed: u64,
#[serde(default)]
pub items_failed: u64,
#[serde(default)]
pub bytes_done: u64,
#[serde(default)]
pub finished: bool,
#[serde(default)]
pub bucket: String,
#[serde(default)]
pub object: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MemStats {
#[serde(default)]
pub alloc: u64,
#[serde(default)]
pub total_alloc: u64,
#[serde(default)]
pub heap_alloc: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum BackendType {
#[default]
#[serde(rename = "FS")]
Fs,
#[serde(rename = "Erasure")]
Erasure,
}
impl std::fmt::Display for BackendType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BackendType::Fs => write!(f, "FS"),
BackendType::Erasure => write!(f, "Erasure"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct BackendInfo {
#[serde(default, rename = "backendType")]
pub backend_type: BackendType,
#[serde(default, rename = "onlineDisks")]
pub online_disks: usize,
#[serde(default, rename = "offlineDisks")]
pub offline_disks: usize,
#[serde(default, rename = "standardSCParity")]
pub standard_sc_parity: Option<usize>,
#[serde(default, rename = "rrSCParity")]
pub rr_sc_parity: Option<usize>,
#[serde(default, rename = "totalSets")]
pub total_sets: Vec<usize>,
#[serde(default, rename = "totalDrivesPerSet")]
pub drives_per_set: Vec<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct UsageInfo {
#[serde(default)]
pub size: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BucketsInfo {
#[serde(default)]
pub count: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ObjectsInfo {
#[serde(default)]
pub count: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ClusterInfo {
#[serde(default)]
pub mode: Option<String>,
#[serde(default)]
pub domain: Option<Vec<String>>,
#[serde(default)]
pub region: Option<String>,
#[serde(default, rename = "deploymentID")]
pub deployment_id: Option<String>,
#[serde(default)]
pub buckets: Option<BucketsInfo>,
#[serde(default)]
pub objects: Option<ObjectsInfo>,
#[serde(default)]
pub usage: Option<UsageInfo>,
#[serde(default)]
pub backend: Option<BackendInfo>,
#[serde(default)]
pub servers: Option<Vec<ServerInfo>>,
}
impl ClusterInfo {
pub fn online_disks(&self) -> usize {
self.servers
.as_ref()
.map(|servers| {
servers
.iter()
.flat_map(|s| &s.disks)
.filter(|d| d.state == "online" || d.state == "ok")
.count()
})
.unwrap_or(0)
}
pub fn offline_disks(&self) -> usize {
self.servers
.as_ref()
.map(|servers| {
servers
.iter()
.flat_map(|s| &s.disks)
.filter(|d| d.state == "offline")
.count()
})
.unwrap_or(0)
}
pub fn total_capacity(&self) -> u64 {
self.servers
.as_ref()
.map(|servers| {
servers
.iter()
.flat_map(|s| &s.disks)
.map(|d| d.total_space)
.sum()
})
.unwrap_or(0)
}
pub fn used_capacity(&self) -> u64 {
self.servers
.as_ref()
.map(|servers| {
servers
.iter()
.flat_map(|s| &s.disks)
.map(|d| d.used_space)
.sum()
})
.unwrap_or(0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum HealScanMode {
#[default]
Normal,
Deep,
}
impl std::fmt::Display for HealScanMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HealScanMode::Normal => write!(f, "normal"),
HealScanMode::Deep => write!(f, "deep"),
}
}
}
impl std::str::FromStr for HealScanMode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"normal" => Ok(HealScanMode::Normal),
"deep" => Ok(HealScanMode::Deep),
_ => Err(format!("Invalid heal scan mode: {s}")),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct HealStartRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bucket: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prefix: Option<String>,
#[serde(default)]
pub scan_mode: HealScanMode,
#[serde(default)]
pub remove: bool,
#[serde(default)]
pub recreate: bool,
#[serde(default)]
pub dry_run: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct HealDriveInfo {
#[serde(default)]
pub uuid: String,
#[serde(default)]
pub endpoint: String,
#[serde(default)]
pub state: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct HealResultItem {
#[serde(default, rename = "resultId")]
pub result_index: usize,
#[serde(default, rename = "type")]
pub item_type: String,
#[serde(default)]
pub bucket: String,
#[serde(default)]
pub object: String,
#[serde(default, rename = "versionId")]
pub version_id: String,
#[serde(default)]
pub detail: String,
#[serde(default, rename = "parityBlocks")]
pub parity_blocks: usize,
#[serde(default, rename = "dataBlocks")]
pub data_blocks: usize,
#[serde(default, rename = "objectSize")]
pub object_size: u64,
#[serde(default)]
pub before: HealDriveInfos,
#[serde(default)]
pub after: HealDriveInfos,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct HealDriveInfos {
#[serde(default)]
pub drives: Vec<HealDriveInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct HealStatus {
#[serde(default)]
pub heal_id: String,
#[serde(default)]
pub healing: bool,
#[serde(default)]
pub bucket: String,
#[serde(default)]
pub object: String,
#[serde(default)]
pub items_scanned: u64,
#[serde(default)]
pub items_healed: u64,
#[serde(default)]
pub items_failed: u64,
#[serde(default)]
pub bytes_scanned: u64,
#[serde(default)]
pub bytes_healed: u64,
#[serde(default)]
pub started: Option<String>,
#[serde(default)]
pub last_update: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_backend_type_display() {
assert_eq!(BackendType::Fs.to_string(), "FS");
assert_eq!(BackendType::Erasure.to_string(), "Erasure");
}
#[test]
fn test_heal_scan_mode_display() {
assert_eq!(HealScanMode::Normal.to_string(), "normal");
assert_eq!(HealScanMode::Deep.to_string(), "deep");
}
#[test]
fn test_heal_scan_mode_from_str() {
assert_eq!(
"normal".parse::<HealScanMode>().unwrap(),
HealScanMode::Normal
);
assert_eq!("deep".parse::<HealScanMode>().unwrap(), HealScanMode::Deep);
assert!("invalid".parse::<HealScanMode>().is_err());
}
#[test]
fn test_cluster_info_default() {
let info = ClusterInfo::default();
assert!(info.mode.is_none());
assert!(info.servers.is_none());
assert_eq!(info.online_disks(), 0);
assert_eq!(info.offline_disks(), 0);
}
#[test]
fn test_cluster_info_disk_counts() {
let info = ClusterInfo {
servers: Some(vec![ServerInfo {
disks: vec![
DiskInfo {
state: "online".to_string(),
..Default::default()
},
DiskInfo {
state: "online".to_string(),
..Default::default()
},
DiskInfo {
state: "offline".to_string(),
..Default::default()
},
],
..Default::default()
}]),
..Default::default()
};
assert_eq!(info.online_disks(), 2);
assert_eq!(info.offline_disks(), 1);
}
#[test]
fn test_cluster_info_capacity() {
let info = ClusterInfo {
servers: Some(vec![ServerInfo {
disks: vec![
DiskInfo {
total_space: 1000,
used_space: 300,
..Default::default()
},
DiskInfo {
total_space: 2000,
used_space: 500,
..Default::default()
},
],
..Default::default()
}]),
..Default::default()
};
assert_eq!(info.total_capacity(), 3000);
assert_eq!(info.used_capacity(), 800);
}
#[test]
fn test_disk_info_default() {
let disk = DiskInfo::default();
assert!(disk.endpoint.is_empty());
assert!(!disk.healing);
assert!(!disk.scanning);
assert_eq!(disk.total_space, 0);
}
#[test]
fn test_server_info_default() {
let server = ServerInfo::default();
assert!(server.state.is_empty());
assert!(server.endpoint.is_empty());
assert_eq!(server.uptime, 0);
}
#[test]
fn test_heal_start_request_default() {
let req = HealStartRequest::default();
assert!(req.bucket.is_none());
assert!(req.prefix.is_none());
assert_eq!(req.scan_mode, HealScanMode::Normal);
assert!(!req.remove);
assert!(!req.dry_run);
}
#[test]
fn test_heal_status_default() {
let status = HealStatus::default();
assert!(status.heal_id.is_empty());
assert!(!status.healing);
assert_eq!(status.items_scanned, 0);
}
#[test]
fn test_serialization() {
let info = ClusterInfo {
mode: Some("distributed".to_string()),
deployment_id: Some("test-123".to_string()),
..Default::default()
};
let json = serde_json::to_string(&info).unwrap();
assert!(json.contains("distributed"));
assert!(json.contains("test-123"));
let deserialized: ClusterInfo = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.mode, Some("distributed".to_string()));
}
}