1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Serialize, Deserialize, Default)]
11#[serde(rename_all = "camelCase")]
12pub struct ServerInfo {
13 #[serde(default)]
15 pub state: String,
16
17 #[serde(default)]
19 pub endpoint: String,
20
21 #[serde(default)]
23 pub scheme: String,
24
25 #[serde(default)]
27 pub uptime: u64,
28
29 #[serde(default)]
31 pub version: String,
32
33 #[serde(default, rename = "commitID")]
35 pub commit_id: String,
36
37 #[serde(default)]
39 pub network: HashMap<String, String>,
40
41 #[serde(default, rename = "drives")]
43 pub disks: Vec<DiskInfo>,
44
45 #[serde(default, rename = "poolNumber")]
47 pub pool_number: i32,
48
49 #[serde(default, rename = "mem_stats")]
51 pub mem_stats: MemStats,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, Default)]
56#[serde(rename_all = "camelCase")]
57pub struct DiskInfo {
58 #[serde(default)]
60 pub endpoint: String,
61
62 #[serde(default, rename = "rootDisk")]
64 pub root_disk: bool,
65
66 #[serde(default, rename = "path")]
68 pub drive_path: String,
69
70 #[serde(default)]
72 pub healing: bool,
73
74 #[serde(default)]
76 pub scanning: bool,
77
78 #[serde(default)]
80 pub state: String,
81
82 #[serde(default)]
84 pub uuid: String,
85
86 #[serde(default, rename = "totalspace")]
88 pub total_space: u64,
89
90 #[serde(default, rename = "usedspace")]
92 pub used_space: u64,
93
94 #[serde(default, rename = "availspace")]
96 pub available_space: u64,
97
98 #[serde(default)]
100 pub pool_index: i32,
101
102 #[serde(default)]
104 pub set_index: i32,
105
106 #[serde(default)]
108 pub disk_index: i32,
109
110 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub heal_info: Option<HealingDiskInfo>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize, Default)]
117#[serde(rename_all = "camelCase")]
118pub struct HealingDiskInfo {
119 #[serde(default)]
121 pub id: String,
122
123 #[serde(default)]
125 pub heal_id: String,
126
127 #[serde(default)]
129 pub pool_index: Option<usize>,
130
131 #[serde(default)]
133 pub set_index: Option<usize>,
134
135 #[serde(default)]
137 pub disk_index: Option<usize>,
138
139 #[serde(default)]
141 pub endpoint: String,
142
143 #[serde(default)]
145 pub path: String,
146
147 #[serde(default)]
149 pub objects_total_count: u64,
150
151 #[serde(default)]
153 pub objects_total_size: u64,
154
155 #[serde(default)]
157 pub items_healed: u64,
158
159 #[serde(default)]
161 pub items_failed: u64,
162
163 #[serde(default)]
165 pub bytes_done: u64,
166
167 #[serde(default)]
169 pub finished: bool,
170
171 #[serde(default)]
173 pub bucket: String,
174
175 #[serde(default)]
177 pub object: String,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize, Default)]
182pub struct MemStats {
183 #[serde(default)]
185 pub alloc: u64,
186
187 #[serde(default)]
189 pub total_alloc: u64,
190
191 #[serde(default)]
193 pub heap_alloc: u64,
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize, Default)]
198#[serde(rename_all = "lowercase")]
199pub enum BackendType {
200 #[default]
202 #[serde(rename = "FS")]
203 Fs,
204 #[serde(rename = "Erasure")]
206 Erasure,
207}
208
209impl std::fmt::Display for BackendType {
210 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211 match self {
212 BackendType::Fs => write!(f, "FS"),
213 BackendType::Erasure => write!(f, "Erasure"),
214 }
215 }
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize, Default)]
220#[serde(rename_all = "camelCase")]
221pub struct BackendInfo {
222 #[serde(default, rename = "backendType")]
224 pub backend_type: BackendType,
225
226 #[serde(default, rename = "onlineDisks")]
228 pub online_disks: usize,
229
230 #[serde(default, rename = "offlineDisks")]
232 pub offline_disks: usize,
233
234 #[serde(default, rename = "standardSCParity")]
236 pub standard_sc_parity: Option<usize>,
237
238 #[serde(default, rename = "rrSCParity")]
240 pub rr_sc_parity: Option<usize>,
241
242 #[serde(default, rename = "totalSets")]
244 pub total_sets: Vec<usize>,
245
246 #[serde(default, rename = "totalDrivesPerSet")]
248 pub drives_per_set: Vec<usize>,
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize, Default)]
253pub struct UsageInfo {
254 #[serde(default)]
256 pub size: u64,
257
258 #[serde(default, skip_serializing_if = "Option::is_none")]
260 pub error: Option<String>,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize, Default)]
265pub struct BucketsInfo {
266 #[serde(default)]
268 pub count: u64,
269
270 #[serde(default, skip_serializing_if = "Option::is_none")]
272 pub error: Option<String>,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize, Default)]
277pub struct ObjectsInfo {
278 #[serde(default)]
280 pub count: u64,
281
282 #[serde(default, skip_serializing_if = "Option::is_none")]
284 pub error: Option<String>,
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize, Default)]
289#[serde(rename_all = "camelCase")]
290pub struct ClusterInfo {
291 #[serde(default)]
293 pub mode: Option<String>,
294
295 #[serde(default)]
297 pub domain: Option<Vec<String>>,
298
299 #[serde(default)]
301 pub region: Option<String>,
302
303 #[serde(default, rename = "deploymentID")]
305 pub deployment_id: Option<String>,
306
307 #[serde(default)]
309 pub buckets: Option<BucketsInfo>,
310
311 #[serde(default)]
313 pub objects: Option<ObjectsInfo>,
314
315 #[serde(default)]
317 pub usage: Option<UsageInfo>,
318
319 #[serde(default)]
321 pub backend: Option<BackendInfo>,
322
323 #[serde(default)]
325 pub servers: Option<Vec<ServerInfo>>,
326}
327
328impl ClusterInfo {
329 pub fn online_disks(&self) -> usize {
331 self.servers
332 .as_ref()
333 .map(|servers| {
334 servers
335 .iter()
336 .flat_map(|s| &s.disks)
337 .filter(|d| d.state == "online" || d.state == "ok")
338 .count()
339 })
340 .unwrap_or(0)
341 }
342
343 pub fn offline_disks(&self) -> usize {
345 self.servers
346 .as_ref()
347 .map(|servers| {
348 servers
349 .iter()
350 .flat_map(|s| &s.disks)
351 .filter(|d| d.state == "offline")
352 .count()
353 })
354 .unwrap_or(0)
355 }
356
357 pub fn total_capacity(&self) -> u64 {
359 self.servers
360 .as_ref()
361 .map(|servers| {
362 servers
363 .iter()
364 .flat_map(|s| &s.disks)
365 .map(|d| d.total_space)
366 .sum()
367 })
368 .unwrap_or(0)
369 }
370
371 pub fn used_capacity(&self) -> u64 {
373 self.servers
374 .as_ref()
375 .map(|servers| {
376 servers
377 .iter()
378 .flat_map(|s| &s.disks)
379 .map(|d| d.used_space)
380 .sum()
381 })
382 .unwrap_or(0)
383 }
384}
385
386#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
388#[serde(rename_all = "lowercase")]
389pub enum HealScanMode {
390 #[default]
392 Normal,
393 Deep,
395}
396
397impl std::fmt::Display for HealScanMode {
398 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
399 match self {
400 HealScanMode::Normal => write!(f, "normal"),
401 HealScanMode::Deep => write!(f, "deep"),
402 }
403 }
404}
405
406impl std::str::FromStr for HealScanMode {
407 type Err = String;
408
409 fn from_str(s: &str) -> Result<Self, Self::Err> {
410 match s.to_lowercase().as_str() {
411 "normal" => Ok(HealScanMode::Normal),
412 "deep" => Ok(HealScanMode::Deep),
413 _ => Err(format!("Invalid heal scan mode: {s}")),
414 }
415 }
416}
417
418#[derive(Debug, Clone, Serialize, Deserialize, Default)]
420#[serde(rename_all = "camelCase")]
421pub struct HealStartRequest {
422 #[serde(default, skip_serializing_if = "Option::is_none")]
424 pub bucket: Option<String>,
425
426 #[serde(default, skip_serializing_if = "Option::is_none")]
428 pub prefix: Option<String>,
429
430 #[serde(default)]
432 pub scan_mode: HealScanMode,
433
434 #[serde(default)]
436 pub remove: bool,
437
438 #[serde(default)]
440 pub recreate: bool,
441
442 #[serde(default)]
444 pub dry_run: bool,
445}
446
447#[derive(Debug, Clone, Serialize, Deserialize, Default)]
449pub struct HealDriveInfo {
450 #[serde(default)]
452 pub uuid: String,
453
454 #[serde(default)]
456 pub endpoint: String,
457
458 #[serde(default)]
460 pub state: String,
461}
462
463#[derive(Debug, Clone, Serialize, Deserialize, Default)]
465#[serde(rename_all = "camelCase")]
466pub struct HealResultItem {
467 #[serde(default, rename = "resultId")]
469 pub result_index: usize,
470
471 #[serde(default, rename = "type")]
473 pub item_type: String,
474
475 #[serde(default)]
477 pub bucket: String,
478
479 #[serde(default)]
481 pub object: String,
482
483 #[serde(default, rename = "versionId")]
485 pub version_id: String,
486
487 #[serde(default)]
489 pub detail: String,
490
491 #[serde(default, rename = "parityBlocks")]
493 pub parity_blocks: usize,
494
495 #[serde(default, rename = "dataBlocks")]
497 pub data_blocks: usize,
498
499 #[serde(default, rename = "objectSize")]
501 pub object_size: u64,
502
503 #[serde(default)]
505 pub before: HealDriveInfos,
506
507 #[serde(default)]
509 pub after: HealDriveInfos,
510}
511
512#[derive(Debug, Clone, Serialize, Deserialize, Default)]
514pub struct HealDriveInfos {
515 #[serde(default)]
517 pub drives: Vec<HealDriveInfo>,
518}
519
520#[derive(Debug, Clone, Serialize, Deserialize, Default)]
522#[serde(rename_all = "camelCase")]
523pub struct HealStatus {
524 #[serde(default)]
526 pub heal_id: String,
527
528 #[serde(default)]
530 pub healing: bool,
531
532 #[serde(default)]
534 pub bucket: String,
535
536 #[serde(default)]
538 pub object: String,
539
540 #[serde(default)]
542 pub items_scanned: u64,
543
544 #[serde(default)]
546 pub items_healed: u64,
547
548 #[serde(default)]
550 pub items_failed: u64,
551
552 #[serde(default)]
554 pub bytes_scanned: u64,
555
556 #[serde(default)]
558 pub bytes_healed: u64,
559
560 #[serde(default)]
562 pub started: Option<String>,
563
564 #[serde(default)]
566 pub last_update: Option<String>,
567}
568
569#[derive(Debug, Clone, Serialize, Deserialize, Default)]
571#[serde(rename_all = "camelCase")]
572pub struct PoolTarget {
573 pub pool: String,
575
576 #[serde(default)]
578 pub by_id: bool,
579}
580
581#[derive(Debug, Clone, Serialize, Deserialize, Default)]
583pub struct PoolStatus {
584 #[serde(default)]
586 pub id: usize,
587
588 #[serde(default, rename = "cmdline")]
590 pub cmd_line: String,
591
592 #[serde(default, rename = "lastUpdate")]
594 pub last_update: String,
595
596 #[serde(default, rename = "decommissionInfo")]
598 pub decommission: Option<PoolDecommissionInfo>,
599}
600
601#[derive(Debug, Clone, Serialize, Deserialize, Default)]
603pub struct PoolDecommissionInfo {
604 #[serde(default, rename = "startTime")]
606 pub start_time: Option<String>,
607
608 #[serde(default, rename = "startSize")]
610 pub start_size: u64,
611
612 #[serde(default, rename = "totalSize")]
614 pub total_size: u64,
615
616 #[serde(default, rename = "currentSize")]
618 pub current_size: u64,
619
620 #[serde(default)]
622 pub complete: bool,
623
624 #[serde(default)]
626 pub failed: bool,
627
628 #[serde(default)]
630 pub canceled: bool,
631
632 #[serde(default, rename = "objectsDecommissioned")]
634 pub objects_decommissioned: u64,
635
636 #[serde(default, rename = "objectsDecommissionedFailed")]
638 pub objects_decommissioned_failed: u64,
639
640 #[serde(default, rename = "bytesDecommissioned")]
642 pub bytes_decommissioned: u64,
643
644 #[serde(default, rename = "bytesDecommissionedFailed")]
646 pub bytes_decommissioned_failed: u64,
647}
648
649#[derive(Debug, Clone, Serialize, Deserialize, Default)]
651pub struct RebalanceStartResult {
652 #[serde(default)]
654 pub id: String,
655}
656
657#[derive(Debug, Clone, Serialize, Deserialize, Default)]
659pub struct RebalanceStatus {
660 #[serde(default)]
662 pub id: String,
663
664 #[serde(default)]
666 pub pools: Vec<RebalancePoolStatus>,
667
668 #[serde(default, rename = "stoppedAt")]
670 pub stopped_at: Option<String>,
671}
672
673#[derive(Debug, Clone, Serialize, Deserialize, Default)]
675pub struct RebalancePoolStatus {
676 #[serde(default)]
678 pub id: usize,
679
680 #[serde(default)]
682 pub status: String,
683
684 #[serde(default)]
686 pub used: f64,
687
688 #[serde(default, rename = "lastError")]
690 pub last_error: Option<String>,
691
692 #[serde(default)]
694 pub progress: Option<RebalancePoolProgress>,
695}
696
697#[derive(Debug, Clone, Serialize, Deserialize, Default)]
699pub struct RebalancePoolProgress {
700 #[serde(default, rename = "objects")]
702 pub num_objects: u64,
703
704 #[serde(default, rename = "versions")]
706 pub num_versions: u64,
707
708 #[serde(default)]
710 pub bytes: u64,
711
712 #[serde(default, rename = "remainingBuckets")]
714 pub remaining_buckets: usize,
715
716 #[serde(default)]
718 pub bucket: String,
719
720 #[serde(default)]
722 pub object: String,
723
724 #[serde(default)]
726 pub elapsed: u64,
727
728 #[serde(default)]
730 pub eta: u64,
731}
732
733#[cfg(test)]
734mod tests {
735 use super::*;
736
737 #[test]
738 fn test_backend_type_display() {
739 assert_eq!(BackendType::Fs.to_string(), "FS");
740 assert_eq!(BackendType::Erasure.to_string(), "Erasure");
741 }
742
743 #[test]
744 fn test_heal_scan_mode_display() {
745 assert_eq!(HealScanMode::Normal.to_string(), "normal");
746 assert_eq!(HealScanMode::Deep.to_string(), "deep");
747 }
748
749 #[test]
750 fn test_heal_scan_mode_from_str() {
751 assert_eq!(
752 "normal".parse::<HealScanMode>().unwrap(),
753 HealScanMode::Normal
754 );
755 assert_eq!("deep".parse::<HealScanMode>().unwrap(), HealScanMode::Deep);
756 assert!("invalid".parse::<HealScanMode>().is_err());
757 }
758
759 #[test]
760 fn test_cluster_info_default() {
761 let info = ClusterInfo::default();
762 assert!(info.mode.is_none());
763 assert!(info.servers.is_none());
764 assert_eq!(info.online_disks(), 0);
765 assert_eq!(info.offline_disks(), 0);
766 }
767
768 #[test]
769 fn test_cluster_info_disk_counts() {
770 let info = ClusterInfo {
771 servers: Some(vec![ServerInfo {
772 disks: vec![
773 DiskInfo {
774 state: "online".to_string(),
775 ..Default::default()
776 },
777 DiskInfo {
778 state: "online".to_string(),
779 ..Default::default()
780 },
781 DiskInfo {
782 state: "offline".to_string(),
783 ..Default::default()
784 },
785 ],
786 ..Default::default()
787 }]),
788 ..Default::default()
789 };
790
791 assert_eq!(info.online_disks(), 2);
792 assert_eq!(info.offline_disks(), 1);
793 }
794
795 #[test]
796 fn test_cluster_info_capacity() {
797 let info = ClusterInfo {
798 servers: Some(vec![ServerInfo {
799 disks: vec![
800 DiskInfo {
801 total_space: 1000,
802 used_space: 300,
803 ..Default::default()
804 },
805 DiskInfo {
806 total_space: 2000,
807 used_space: 500,
808 ..Default::default()
809 },
810 ],
811 ..Default::default()
812 }]),
813 ..Default::default()
814 };
815
816 assert_eq!(info.total_capacity(), 3000);
817 assert_eq!(info.used_capacity(), 800);
818 }
819
820 #[test]
821 fn test_disk_info_default() {
822 let disk = DiskInfo::default();
823 assert!(disk.endpoint.is_empty());
824 assert!(!disk.healing);
825 assert!(!disk.scanning);
826 assert_eq!(disk.total_space, 0);
827 }
828
829 #[test]
830 fn test_server_info_default() {
831 let server = ServerInfo::default();
832 assert!(server.state.is_empty());
833 assert!(server.endpoint.is_empty());
834 assert_eq!(server.uptime, 0);
835 }
836
837 #[test]
838 fn test_heal_start_request_default() {
839 let req = HealStartRequest::default();
840 assert!(req.bucket.is_none());
841 assert!(req.prefix.is_none());
842 assert_eq!(req.scan_mode, HealScanMode::Normal);
843 assert!(!req.remove);
844 assert!(!req.dry_run);
845 }
846
847 #[test]
848 fn test_heal_status_default() {
849 let status = HealStatus::default();
850 assert!(status.heal_id.is_empty());
851 assert!(!status.healing);
852 assert_eq!(status.items_scanned, 0);
853 }
854
855 #[test]
856 fn test_pool_status_deserialization() {
857 let json = r#"{"id":1,"cmdline":"/data/pool1/disk{1...4}","lastUpdate":"2026-05-06T00:00:00Z","decommissionInfo":{"startTime":"2026-05-06T00:00:01Z","startSize":100,"totalSize":1000,"currentSize":600,"complete":false,"failed":false,"canceled":false,"objectsDecommissioned":2,"objectsDecommissionedFailed":1,"bytesDecommissioned":128,"bytesDecommissionedFailed":64}}"#;
858
859 let status: PoolStatus = serde_json::from_str(json).unwrap();
860
861 assert_eq!(status.id, 1);
862 assert_eq!(status.cmd_line, "/data/pool1/disk{1...4}");
863 let info = status.decommission.expect("decommission info exists");
864 assert_eq!(info.objects_decommissioned, 2);
865 assert_eq!(info.bytes_decommissioned_failed, 64);
866 }
867
868 #[test]
869 fn test_rebalance_status_deserialization() {
870 let json = r#"{"id":"rebalance-1","pools":[{"id":0,"status":"Started","used":0.5,"lastError":null,"progress":{"objects":3,"versions":4,"bytes":1024,"remainingBuckets":2,"bucket":"bucket","object":"object","elapsed":10,"eta":20}}],"stoppedAt":null}"#;
871
872 let status: RebalanceStatus = serde_json::from_str(json).unwrap();
873
874 assert_eq!(status.id, "rebalance-1");
875 assert_eq!(status.pools.len(), 1);
876 assert_eq!(status.pools[0].used, 0.5);
877 let progress = status.pools[0]
878 .progress
879 .as_ref()
880 .expect("progress should exist");
881 assert_eq!(progress.num_objects, 3);
882 assert_eq!(progress.remaining_buckets, 2);
883 }
884
885 #[test]
886 fn test_serialization() {
887 let info = ClusterInfo {
888 mode: Some("distributed".to_string()),
889 deployment_id: Some("test-123".to_string()),
890 ..Default::default()
891 };
892
893 let json = serde_json::to_string(&info).unwrap();
894 assert!(json.contains("distributed"));
895 assert!(json.contains("test-123"));
896
897 let deserialized: ClusterInfo = serde_json::from_str(&json).unwrap();
898 assert_eq!(deserialized.mode, Some("distributed".to_string()));
899 }
900}