1use serde::{Deserialize, Serialize};
7use std::collections::{BTreeMap, 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, alias = "pool_index")]
100 pub pool_index: i32,
101
102 #[serde(default, alias = "set_index")]
104 pub set_index: i32,
105
106 #[serde(default, alias = "disk_index")]
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 PoolErasureSetInfo {
291 #[serde(default)]
293 pub id: i32,
294
295 #[serde(default, rename = "rawUsage")]
297 pub raw_usage: u64,
298
299 #[serde(default, rename = "rawCapacity")]
301 pub raw_capacity: u64,
302
303 #[serde(default)]
305 pub usage: u64,
306
307 #[serde(default, rename = "objectsCount")]
309 pub objects_count: u64,
310
311 #[serde(default, rename = "versionsCount")]
313 pub versions_count: u64,
314
315 #[serde(default, rename = "deleteMarkersCount")]
317 pub delete_markers_count: u64,
318
319 #[serde(default, rename = "healDisks")]
321 pub heal_disks: i32,
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize, Default)]
326#[serde(rename_all = "camelCase")]
327pub struct ClusterInfo {
328 #[serde(default)]
330 pub mode: Option<String>,
331
332 #[serde(default)]
334 pub domain: Option<Vec<String>>,
335
336 #[serde(default)]
338 pub region: Option<String>,
339
340 #[serde(default, rename = "deploymentID")]
342 pub deployment_id: Option<String>,
343
344 #[serde(default)]
346 pub buckets: Option<BucketsInfo>,
347
348 #[serde(default)]
350 pub objects: Option<ObjectsInfo>,
351
352 #[serde(default)]
354 pub usage: Option<UsageInfo>,
355
356 #[serde(default)]
358 pub backend: Option<BackendInfo>,
359
360 #[serde(default)]
362 pub servers: Option<Vec<ServerInfo>>,
363
364 #[serde(default)]
366 pub pools: Option<BTreeMap<i32, BTreeMap<i32, PoolErasureSetInfo>>>,
367}
368
369impl ClusterInfo {
370 pub fn online_disks(&self) -> usize {
372 self.servers
373 .as_ref()
374 .map(|servers| {
375 servers
376 .iter()
377 .flat_map(|s| &s.disks)
378 .filter(|d| d.state == "online" || d.state == "ok")
379 .count()
380 })
381 .unwrap_or(0)
382 }
383
384 pub fn offline_disks(&self) -> usize {
386 self.servers
387 .as_ref()
388 .map(|servers| {
389 servers
390 .iter()
391 .flat_map(|s| &s.disks)
392 .filter(|d| d.state == "offline")
393 .count()
394 })
395 .unwrap_or(0)
396 }
397
398 pub fn total_capacity(&self) -> u64 {
400 self.servers
401 .as_ref()
402 .map(|servers| {
403 servers
404 .iter()
405 .flat_map(|s| &s.disks)
406 .map(|d| d.total_space)
407 .sum()
408 })
409 .unwrap_or(0)
410 }
411
412 pub fn used_capacity(&self) -> u64 {
414 self.servers
415 .as_ref()
416 .map(|servers| {
417 servers
418 .iter()
419 .flat_map(|s| &s.disks)
420 .map(|d| d.used_space)
421 .sum()
422 })
423 .unwrap_or(0)
424 }
425}
426
427#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
429#[serde(rename_all = "lowercase")]
430pub enum HealScanMode {
431 #[default]
433 Normal,
434 Deep,
436}
437
438impl std::fmt::Display for HealScanMode {
439 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
440 match self {
441 HealScanMode::Normal => write!(f, "normal"),
442 HealScanMode::Deep => write!(f, "deep"),
443 }
444 }
445}
446
447impl std::str::FromStr for HealScanMode {
448 type Err = String;
449
450 fn from_str(s: &str) -> Result<Self, Self::Err> {
451 match s.to_lowercase().as_str() {
452 "normal" => Ok(HealScanMode::Normal),
453 "deep" => Ok(HealScanMode::Deep),
454 _ => Err(format!("Invalid heal scan mode: {s}")),
455 }
456 }
457}
458
459#[derive(Debug, Clone, Serialize, Deserialize, Default)]
461#[serde(rename_all = "camelCase")]
462pub struct HealStartRequest {
463 #[serde(default, skip_serializing_if = "Option::is_none")]
465 pub bucket: Option<String>,
466
467 #[serde(default, skip_serializing_if = "Option::is_none")]
469 pub prefix: Option<String>,
470
471 #[serde(default)]
473 pub scan_mode: HealScanMode,
474
475 #[serde(default)]
477 pub remove: bool,
478
479 #[serde(default)]
481 pub recreate: bool,
482
483 #[serde(default)]
485 pub dry_run: bool,
486}
487
488#[derive(Debug, Clone, Serialize, Deserialize, Default)]
490pub struct HealDriveInfo {
491 #[serde(default)]
493 pub uuid: String,
494
495 #[serde(default)]
497 pub endpoint: String,
498
499 #[serde(default)]
501 pub state: String,
502}
503
504#[derive(Debug, Clone, Serialize, Deserialize, Default)]
506#[serde(rename_all = "camelCase")]
507pub struct HealResultItem {
508 #[serde(default, rename = "resultId")]
510 pub result_index: usize,
511
512 #[serde(default, rename = "type")]
514 pub item_type: String,
515
516 #[serde(default)]
518 pub bucket: String,
519
520 #[serde(default)]
522 pub object: String,
523
524 #[serde(default, rename = "versionId")]
526 pub version_id: String,
527
528 #[serde(default)]
530 pub detail: String,
531
532 #[serde(default, rename = "parityBlocks")]
534 pub parity_blocks: usize,
535
536 #[serde(default, rename = "dataBlocks")]
538 pub data_blocks: usize,
539
540 #[serde(default, rename = "objectSize")]
542 pub object_size: u64,
543
544 #[serde(default)]
546 pub before: HealDriveInfos,
547
548 #[serde(default)]
550 pub after: HealDriveInfos,
551}
552
553#[derive(Debug, Clone, Serialize, Deserialize, Default)]
555pub struct HealDriveInfos {
556 #[serde(default)]
558 pub drives: Vec<HealDriveInfo>,
559}
560
561#[derive(Debug, Clone, Serialize, Deserialize, Default)]
563#[serde(rename_all = "camelCase")]
564pub struct HealStatus {
565 #[serde(default)]
567 pub heal_id: String,
568
569 #[serde(default)]
571 pub healing: bool,
572
573 #[serde(default)]
575 pub bucket: String,
576
577 #[serde(default)]
579 pub object: String,
580
581 #[serde(default)]
583 pub items_scanned: u64,
584
585 #[serde(default)]
587 pub items_healed: u64,
588
589 #[serde(default)]
591 pub items_failed: u64,
592
593 #[serde(default)]
595 pub bytes_scanned: u64,
596
597 #[serde(default)]
599 pub bytes_healed: u64,
600
601 #[serde(default)]
603 pub started: Option<String>,
604
605 #[serde(default)]
607 pub last_update: Option<String>,
608}
609
610#[derive(Debug, Clone, Serialize, Deserialize, Default)]
612#[serde(rename_all = "camelCase")]
613pub struct PoolTarget {
614 pub pool: String,
616
617 #[serde(default)]
619 pub by_id: bool,
620}
621
622#[derive(Debug, Clone, Serialize, Deserialize, Default)]
624pub struct PoolStatus {
625 #[serde(default)]
627 pub id: usize,
628
629 #[serde(default, rename = "cmdline")]
631 pub cmd_line: String,
632
633 #[serde(default, rename = "lastUpdate")]
635 pub last_update: String,
636
637 #[serde(default, rename = "decommissionInfo")]
639 pub decommission: Option<PoolDecommissionInfo>,
640}
641
642#[derive(Debug, Clone, Serialize, Deserialize, Default)]
644pub struct PoolDecommissionInfo {
645 #[serde(default, rename = "startTime")]
647 pub start_time: Option<String>,
648
649 #[serde(default, rename = "startSize")]
651 pub start_size: u64,
652
653 #[serde(default, rename = "totalSize")]
655 pub total_size: u64,
656
657 #[serde(default, rename = "currentSize")]
659 pub current_size: u64,
660
661 #[serde(default)]
663 pub complete: bool,
664
665 #[serde(default)]
667 pub failed: bool,
668
669 #[serde(default)]
671 pub canceled: bool,
672
673 #[serde(default, rename = "objectsDecommissioned")]
675 pub objects_decommissioned: u64,
676
677 #[serde(default, rename = "objectsDecommissionedFailed")]
679 pub objects_decommissioned_failed: u64,
680
681 #[serde(default, rename = "bytesDecommissioned")]
683 pub bytes_decommissioned: u64,
684
685 #[serde(default, rename = "bytesDecommissionedFailed")]
687 pub bytes_decommissioned_failed: u64,
688}
689
690#[derive(Debug, Clone, Serialize, Deserialize, Default)]
692pub struct RebalanceStartResult {
693 #[serde(default)]
695 pub id: String,
696}
697
698#[derive(Debug, Clone, Serialize, Deserialize, Default)]
700pub struct RebalanceStatus {
701 #[serde(default)]
703 pub id: String,
704
705 #[serde(default)]
707 pub pools: Vec<RebalancePoolStatus>,
708
709 #[serde(default, rename = "stoppedAt")]
711 pub stopped_at: Option<String>,
712}
713
714#[derive(Debug, Clone, Serialize, Deserialize, Default)]
716pub struct RebalancePoolStatus {
717 #[serde(default)]
719 pub id: usize,
720
721 #[serde(default)]
723 pub status: String,
724
725 #[serde(default)]
727 pub used: f64,
728
729 #[serde(default, rename = "lastError")]
731 pub last_error: Option<String>,
732
733 #[serde(default)]
735 pub progress: Option<RebalancePoolProgress>,
736}
737
738#[derive(Debug, Clone, Serialize, Deserialize, Default)]
740pub struct RebalancePoolProgress {
741 #[serde(default, rename = "objects")]
743 pub num_objects: u64,
744
745 #[serde(default, rename = "versions")]
747 pub num_versions: u64,
748
749 #[serde(default)]
751 pub bytes: u64,
752
753 #[serde(default, rename = "remainingBuckets")]
755 pub remaining_buckets: usize,
756
757 #[serde(default)]
759 pub bucket: String,
760
761 #[serde(default)]
763 pub object: String,
764
765 #[serde(default)]
767 pub elapsed: u64,
768
769 #[serde(default)]
771 pub eta: u64,
772}
773
774#[cfg(test)]
775mod tests {
776 use super::*;
777
778 #[test]
779 fn test_backend_type_display() {
780 assert_eq!(BackendType::Fs.to_string(), "FS");
781 assert_eq!(BackendType::Erasure.to_string(), "Erasure");
782 }
783
784 #[test]
785 fn test_heal_scan_mode_display() {
786 assert_eq!(HealScanMode::Normal.to_string(), "normal");
787 assert_eq!(HealScanMode::Deep.to_string(), "deep");
788 }
789
790 #[test]
791 fn test_heal_scan_mode_from_str() {
792 assert_eq!(
793 "normal".parse::<HealScanMode>().unwrap(),
794 HealScanMode::Normal
795 );
796 assert_eq!("deep".parse::<HealScanMode>().unwrap(), HealScanMode::Deep);
797 assert!("invalid".parse::<HealScanMode>().is_err());
798 }
799
800 #[test]
801 fn test_cluster_info_default() {
802 let info = ClusterInfo::default();
803 assert!(info.mode.is_none());
804 assert!(info.servers.is_none());
805 assert_eq!(info.online_disks(), 0);
806 assert_eq!(info.offline_disks(), 0);
807 }
808
809 #[test]
810 fn test_cluster_info_disk_counts() {
811 let info = ClusterInfo {
812 servers: Some(vec![ServerInfo {
813 disks: vec![
814 DiskInfo {
815 state: "online".to_string(),
816 ..Default::default()
817 },
818 DiskInfo {
819 state: "online".to_string(),
820 ..Default::default()
821 },
822 DiskInfo {
823 state: "offline".to_string(),
824 ..Default::default()
825 },
826 ],
827 ..Default::default()
828 }]),
829 ..Default::default()
830 };
831
832 assert_eq!(info.online_disks(), 2);
833 assert_eq!(info.offline_disks(), 1);
834 }
835
836 #[test]
837 fn test_cluster_info_capacity() {
838 let info = ClusterInfo {
839 servers: Some(vec![ServerInfo {
840 disks: vec![
841 DiskInfo {
842 total_space: 1000,
843 used_space: 300,
844 ..Default::default()
845 },
846 DiskInfo {
847 total_space: 2000,
848 used_space: 500,
849 ..Default::default()
850 },
851 ],
852 ..Default::default()
853 }]),
854 ..Default::default()
855 };
856
857 assert_eq!(info.total_capacity(), 3000);
858 assert_eq!(info.used_capacity(), 800);
859 }
860
861 #[test]
862 fn test_disk_info_default() {
863 let disk = DiskInfo::default();
864 assert!(disk.endpoint.is_empty());
865 assert!(!disk.healing);
866 assert!(!disk.scanning);
867 assert_eq!(disk.total_space, 0);
868 }
869
870 #[test]
871 fn test_disk_info_deserializes_snake_case_location_indexes() {
872 let json = r#"{"pool_index":1,"set_index":2,"disk_index":3}"#;
873
874 let disk: DiskInfo = serde_json::from_str(json).unwrap();
875
876 assert_eq!(disk.pool_index, 1);
877 assert_eq!(disk.set_index, 2);
878 assert_eq!(disk.disk_index, 3);
879 }
880
881 #[test]
882 fn test_server_info_default() {
883 let server = ServerInfo::default();
884 assert!(server.state.is_empty());
885 assert!(server.endpoint.is_empty());
886 assert_eq!(server.uptime, 0);
887 }
888
889 #[test]
890 fn test_heal_start_request_default() {
891 let req = HealStartRequest::default();
892 assert!(req.bucket.is_none());
893 assert!(req.prefix.is_none());
894 assert_eq!(req.scan_mode, HealScanMode::Normal);
895 assert!(!req.remove);
896 assert!(!req.dry_run);
897 }
898
899 #[test]
900 fn test_heal_status_default() {
901 let status = HealStatus::default();
902 assert!(status.heal_id.is_empty());
903 assert!(!status.healing);
904 assert_eq!(status.items_scanned, 0);
905 }
906
907 #[test]
908 fn test_pool_status_deserialization() {
909 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}}"#;
910
911 let status: PoolStatus = serde_json::from_str(json).unwrap();
912
913 assert_eq!(status.id, 1);
914 assert_eq!(status.cmd_line, "/data/pool1/disk{1...4}");
915 let info = status.decommission.expect("decommission info exists");
916 assert_eq!(info.objects_decommissioned, 2);
917 assert_eq!(info.bytes_decommissioned_failed, 64);
918 }
919
920 #[test]
921 fn test_rebalance_status_deserialization() {
922 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}"#;
923
924 let status: RebalanceStatus = serde_json::from_str(json).unwrap();
925
926 assert_eq!(status.id, "rebalance-1");
927 assert_eq!(status.pools.len(), 1);
928 assert_eq!(status.pools[0].used, 0.5);
929 let progress = status.pools[0]
930 .progress
931 .as_ref()
932 .expect("progress should exist");
933 assert_eq!(progress.num_objects, 3);
934 assert_eq!(progress.remaining_buckets, 2);
935 }
936
937 #[test]
938 fn test_serialization() {
939 let info = ClusterInfo {
940 mode: Some("distributed".to_string()),
941 deployment_id: Some("test-123".to_string()),
942 ..Default::default()
943 };
944
945 let json = serde_json::to_string(&info).unwrap();
946 assert!(json.contains("distributed"));
947 assert!(json.contains("test-123"));
948
949 let deserialized: ClusterInfo = serde_json::from_str(&json).unwrap();
950 assert_eq!(deserialized.mode, Some("distributed".to_string()));
951 }
952}