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, Copy, PartialEq, Eq, Serialize, Deserialize)]
460#[serde(rename_all = "lowercase")]
461pub enum HealRuntimeState {
462 Disabled,
463 Uninitialized,
464 Idle,
465 Active,
466 Degraded,
467 #[serde(other)]
468 Unknown,
469}
470
471#[derive(Debug, Clone, Serialize, Deserialize, Default)]
473pub struct BackgroundHealCoverage {
474 #[serde(default, skip_serializing_if = "Option::is_none")]
475 pub expected: Option<u64>,
476 #[serde(default, skip_serializing_if = "Option::is_none")]
477 pub responded: Option<u64>,
478 #[serde(default, skip_serializing_if = "Option::is_none")]
479 pub unknown: Option<u64>,
480 #[serde(default)]
481 pub reasons: Vec<String>,
482}
483
484#[derive(Debug, Clone, Serialize, Deserialize, Default)]
486#[serde(rename_all = "camelCase")]
487pub struct HealStartRequest {
488 #[serde(default, skip_serializing_if = "Option::is_none")]
490 pub bucket: Option<String>,
491
492 #[serde(default, skip_serializing_if = "Option::is_none")]
494 pub prefix: Option<String>,
495
496 #[serde(default)]
498 pub scan_mode: HealScanMode,
499
500 #[serde(default)]
502 pub remove: bool,
503
504 #[serde(default)]
506 pub recreate: bool,
507
508 #[serde(default)]
510 pub dry_run: bool,
511}
512
513#[derive(Debug, Clone, Serialize, Deserialize)]
515#[serde(rename_all = "camelCase")]
516pub struct HealTaskRequest {
517 pub bucket: String,
519
520 #[serde(default, skip_serializing_if = "Option::is_none")]
522 pub prefix: Option<String>,
523
524 pub client_token: String,
526}
527
528#[derive(Debug, Clone, Serialize, Deserialize, Default)]
530pub struct HealDriveInfo {
531 #[serde(default)]
533 pub uuid: String,
534
535 #[serde(default)]
537 pub endpoint: String,
538
539 #[serde(default)]
541 pub state: String,
542}
543
544#[derive(Debug, Clone, Serialize, Deserialize, Default)]
546#[serde(rename_all = "camelCase")]
547pub struct HealResultItem {
548 #[serde(default, rename = "resultId")]
550 pub result_index: usize,
551
552 #[serde(default, rename = "type")]
554 pub item_type: String,
555
556 #[serde(default)]
558 pub bucket: String,
559
560 #[serde(default)]
562 pub object: String,
563
564 #[serde(default, rename = "versionId")]
566 pub version_id: String,
567
568 #[serde(default)]
570 pub detail: String,
571
572 #[serde(default, rename = "parityBlocks")]
574 pub parity_blocks: usize,
575
576 #[serde(default, rename = "dataBlocks")]
578 pub data_blocks: usize,
579
580 #[serde(default, rename = "objectSize")]
582 pub object_size: u64,
583
584 #[serde(default)]
586 pub before: HealDriveInfos,
587
588 #[serde(default)]
590 pub after: HealDriveInfos,
591}
592
593#[derive(Debug, Clone, Serialize, Deserialize, Default)]
595pub struct HealDriveInfos {
596 #[serde(default)]
598 pub drives: Vec<HealDriveInfo>,
599}
600
601#[derive(Debug, Clone, Serialize, Deserialize, Default)]
603#[serde(rename_all = "camelCase")]
604pub struct HealStatus {
605 #[serde(default)]
607 pub heal_id: String,
608
609 #[serde(default)]
611 pub healing: bool,
612
613 #[serde(default, skip_serializing_if = "Option::is_none")]
614 pub state: Option<HealRuntimeState>,
615
616 #[serde(default, skip_serializing_if = "Option::is_none")]
618 pub cluster_status_complete: Option<bool>,
619
620 #[serde(default, skip_serializing_if = "Option::is_none")]
621 pub coverage: Option<BackgroundHealCoverage>,
622
623 #[serde(default, skip_serializing_if = "Option::is_none")]
625 pub summary: Option<String>,
626
627 #[serde(default, skip_serializing_if = "Option::is_none")]
629 pub detail: Option<String>,
630
631 #[serde(default)]
633 pub bucket: String,
634
635 #[serde(default)]
637 pub object: String,
638
639 #[serde(default, skip_serializing_if = "Option::is_none")]
641 pub scan_mode: Option<HealScanMode>,
642
643 #[serde(default)]
645 pub scan_cycle: u64,
646
647 #[serde(default)]
649 pub heal_queue_length: u64,
650
651 #[serde(default)]
653 pub heal_active_tasks: u64,
654
655 #[serde(default)]
657 pub items_scanned: u64,
658
659 #[serde(default)]
661 pub items_healed: u64,
662
663 #[serde(default)]
665 pub items_failed: u64,
666
667 #[serde(default)]
669 pub bytes_scanned: u64,
670
671 #[serde(default)]
673 pub bytes_healed: u64,
674
675 #[serde(default)]
677 pub started: Option<String>,
678
679 #[serde(default)]
681 pub last_update: Option<String>,
682}
683
684#[derive(Debug, Clone, Serialize, Deserialize, Default)]
686#[serde(rename_all = "camelCase")]
687pub struct PoolTarget {
688 pub pool: String,
690
691 #[serde(default)]
693 pub by_id: bool,
694}
695
696#[derive(Debug, Clone, Serialize, Deserialize, Default)]
698pub struct PoolStatus {
699 #[serde(default)]
701 pub id: usize,
702
703 #[serde(default, rename = "cmdline")]
705 pub cmd_line: String,
706
707 #[serde(default, rename = "lastUpdate")]
709 pub last_update: String,
710
711 #[serde(default)]
713 pub status: String,
714
715 #[serde(default, rename = "decommissionStatus")]
717 pub decommission_status: String,
718
719 #[serde(default, rename = "rebalanceStatus")]
721 pub rebalance_status: String,
722
723 #[serde(default, rename = "totalSize")]
725 pub total_size: u64,
726
727 #[serde(default, rename = "currentSize")]
729 pub current_size: u64,
730
731 #[serde(default, rename = "usedSize")]
733 pub used_size: u64,
734
735 #[serde(default)]
737 pub used: f64,
738
739 #[serde(default, rename = "decommissionInfo")]
741 pub decommission: Option<PoolDecommissionInfo>,
742}
743
744#[derive(Debug, Clone, Serialize, Deserialize, Default)]
746pub struct DecommissionStatus {
747 #[serde(default)]
749 pub pools: Vec<DecommissionPoolStatus>,
750}
751
752#[derive(Debug, Clone, Serialize, Deserialize, Default)]
754pub struct DecommissionPoolStatus {
755 #[serde(default)]
757 pub id: usize,
758
759 #[serde(default, rename = "cmdline")]
761 pub cmd_line: String,
762
763 #[serde(default)]
765 pub status: String,
766
767 #[serde(default, rename = "poolStatus")]
769 pub pool_status: String,
770
771 #[serde(default, rename = "decommissionInfo")]
773 pub decommission: Option<PoolDecommissionInfo>,
774}
775
776#[derive(Debug, Clone, Serialize, Deserialize, Default)]
778pub struct PoolDecommissionInfo {
779 #[serde(default, rename = "startTime")]
781 pub start_time: Option<String>,
782
783 #[serde(default, rename = "startSize")]
785 pub start_size: u64,
786
787 #[serde(default, rename = "totalSize")]
789 pub total_size: u64,
790
791 #[serde(default, rename = "currentSize")]
793 pub current_size: u64,
794
795 #[serde(default)]
797 pub complete: bool,
798
799 #[serde(default)]
801 pub failed: bool,
802
803 #[serde(default)]
805 pub canceled: bool,
806
807 #[serde(default)]
809 pub queued: bool,
810
811 #[serde(default, rename = "queuedBuckets")]
813 pub queued_buckets: Vec<String>,
814
815 #[serde(default, rename = "decommissionedBuckets")]
817 pub decommissioned_buckets: Vec<String>,
818
819 #[serde(default)]
821 pub bucket: String,
822
823 #[serde(default)]
825 pub prefix: String,
826
827 #[serde(default)]
829 pub object: String,
830
831 #[serde(default)]
833 pub stage: String,
834
835 #[serde(default, rename = "objectsDecommissioned")]
837 pub objects_decommissioned: u64,
838
839 #[serde(default, rename = "objectsDecommissionedFailed")]
841 pub objects_decommissioned_failed: u64,
842
843 #[serde(default, rename = "bytesDecommissioned")]
845 pub bytes_decommissioned: u64,
846
847 #[serde(default, rename = "bytesDecommissionedFailed")]
849 pub bytes_decommissioned_failed: u64,
850
851 #[serde(default, rename = "waitingReason")]
853 pub waiting_reason: Option<String>,
854}
855
856#[derive(Debug, Clone, Serialize, Deserialize, Default)]
858pub struct RebalanceStartResult {
859 #[serde(default)]
861 pub id: String,
862}
863
864#[derive(Debug, Clone, Serialize, Deserialize, Default)]
866pub struct RebalanceStatus {
867 #[serde(default)]
869 pub id: String,
870
871 #[serde(default)]
873 pub pools: Vec<RebalancePoolStatus>,
874
875 #[serde(default, rename = "stoppedAt")]
877 pub stopped_at: Option<String>,
878}
879
880#[derive(Debug, Clone, Serialize, Deserialize, Default)]
882pub struct RebalancePoolStatus {
883 #[serde(default)]
885 pub id: usize,
886
887 #[serde(default)]
889 pub status: String,
890
891 #[serde(default)]
893 pub used: f64,
894
895 #[serde(default, rename = "lastError")]
897 pub last_error: Option<String>,
898
899 #[serde(default, rename = "cleanupWarnings")]
901 pub cleanup_warnings: RebalanceCleanupWarnings,
902
903 #[serde(default)]
905 pub progress: Option<RebalancePoolProgress>,
906}
907
908#[derive(Debug, Clone, Serialize, Deserialize, Default)]
910pub struct RebalanceCleanupWarnings {
911 #[serde(default)]
913 pub count: u64,
914
915 #[serde(default, rename = "lastMsg")]
917 pub last_message: Option<String>,
918
919 #[serde(default, rename = "lastBucket")]
921 pub last_bucket: Option<String>,
922
923 #[serde(default, rename = "lastObject")]
925 pub last_object: Option<String>,
926
927 #[serde(default, rename = "lastAt")]
929 pub last_at: Option<String>,
930}
931
932#[derive(Debug, Clone, Serialize, Deserialize, Default)]
934pub struct RebalancePoolProgress {
935 #[serde(default, rename = "objects")]
937 pub num_objects: u64,
938
939 #[serde(default, rename = "versions")]
941 pub num_versions: u64,
942
943 #[serde(default)]
945 pub bytes: u64,
946
947 #[serde(default, rename = "remainingBuckets")]
949 pub remaining_buckets: usize,
950
951 #[serde(default)]
953 pub bucket: String,
954
955 #[serde(default)]
957 pub object: String,
958
959 #[serde(default)]
961 pub elapsed: u64,
962
963 #[serde(default)]
965 pub eta: u64,
966}
967
968#[cfg(test)]
969mod tests {
970 use super::*;
971
972 #[test]
973 fn test_backend_type_display() {
974 assert_eq!(BackendType::Fs.to_string(), "FS");
975 assert_eq!(BackendType::Erasure.to_string(), "Erasure");
976 }
977
978 #[test]
979 fn test_heal_scan_mode_display() {
980 assert_eq!(HealScanMode::Normal.to_string(), "normal");
981 assert_eq!(HealScanMode::Deep.to_string(), "deep");
982 }
983
984 #[test]
985 fn test_heal_scan_mode_from_str() {
986 assert_eq!(
987 "normal".parse::<HealScanMode>().unwrap(),
988 HealScanMode::Normal
989 );
990 assert_eq!("deep".parse::<HealScanMode>().unwrap(), HealScanMode::Deep);
991 assert!("invalid".parse::<HealScanMode>().is_err());
992 }
993
994 #[test]
995 fn test_cluster_info_default() {
996 let info = ClusterInfo::default();
997 assert!(info.mode.is_none());
998 assert!(info.servers.is_none());
999 assert_eq!(info.online_disks(), 0);
1000 assert_eq!(info.offline_disks(), 0);
1001 }
1002
1003 #[test]
1004 fn test_cluster_info_disk_counts() {
1005 let info = ClusterInfo {
1006 servers: Some(vec![ServerInfo {
1007 disks: vec![
1008 DiskInfo {
1009 state: "online".to_string(),
1010 ..Default::default()
1011 },
1012 DiskInfo {
1013 state: "online".to_string(),
1014 ..Default::default()
1015 },
1016 DiskInfo {
1017 state: "offline".to_string(),
1018 ..Default::default()
1019 },
1020 ],
1021 ..Default::default()
1022 }]),
1023 ..Default::default()
1024 };
1025
1026 assert_eq!(info.online_disks(), 2);
1027 assert_eq!(info.offline_disks(), 1);
1028 }
1029
1030 #[test]
1031 fn test_cluster_info_capacity() {
1032 let info = ClusterInfo {
1033 servers: Some(vec![ServerInfo {
1034 disks: vec![
1035 DiskInfo {
1036 total_space: 1000,
1037 used_space: 300,
1038 ..Default::default()
1039 },
1040 DiskInfo {
1041 total_space: 2000,
1042 used_space: 500,
1043 ..Default::default()
1044 },
1045 ],
1046 ..Default::default()
1047 }]),
1048 ..Default::default()
1049 };
1050
1051 assert_eq!(info.total_capacity(), 3000);
1052 assert_eq!(info.used_capacity(), 800);
1053 }
1054
1055 #[test]
1056 fn test_disk_info_default() {
1057 let disk = DiskInfo::default();
1058 assert!(disk.endpoint.is_empty());
1059 assert!(!disk.healing);
1060 assert!(!disk.scanning);
1061 assert_eq!(disk.total_space, 0);
1062 }
1063
1064 #[test]
1065 fn test_disk_info_deserializes_snake_case_location_indexes() {
1066 let json = r#"{"pool_index":1,"set_index":2,"disk_index":3}"#;
1067
1068 let disk: DiskInfo = serde_json::from_str(json).unwrap();
1069
1070 assert_eq!(disk.pool_index, 1);
1071 assert_eq!(disk.set_index, 2);
1072 assert_eq!(disk.disk_index, 3);
1073 }
1074
1075 #[test]
1076 fn test_server_info_default() {
1077 let server = ServerInfo::default();
1078 assert!(server.state.is_empty());
1079 assert!(server.endpoint.is_empty());
1080 assert_eq!(server.uptime, 0);
1081 }
1082
1083 #[test]
1084 fn test_heal_start_request_default() {
1085 let req = HealStartRequest::default();
1086 assert!(req.bucket.is_none());
1087 assert!(req.prefix.is_none());
1088 assert_eq!(req.scan_mode, HealScanMode::Normal);
1089 assert!(!req.remove);
1090 assert!(!req.dry_run);
1091 }
1092
1093 #[test]
1094 fn test_heal_status_default() {
1095 let status = HealStatus::default();
1096 assert!(status.heal_id.is_empty());
1097 assert!(!status.healing);
1098 assert!(status.state.is_none());
1099 assert!(status.scan_mode.is_none());
1100 assert_eq!(status.scan_cycle, 0);
1101 assert_eq!(status.heal_queue_length, 0);
1102 assert_eq!(status.heal_active_tasks, 0);
1103 assert_eq!(status.items_scanned, 0);
1104 }
1105
1106 #[test]
1107 fn test_heal_runtime_state_unknown_value_is_preserved() {
1108 let state: HealRuntimeState = serde_json::from_str(r#""future""#).unwrap();
1109
1110 assert_eq!(state, HealRuntimeState::Unknown);
1111 }
1112
1113 #[test]
1114 fn test_pool_status_deserialization() {
1115 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"}}"#;
1116
1117 let status: PoolStatus = serde_json::from_str(json).unwrap();
1118
1119 assert_eq!(status.id, 1);
1120 assert_eq!(status.cmd_line, "/data/pool1/disk{1...4}");
1121 assert_eq!(status.status, "decommissioning");
1122 assert_eq!(status.decommission_status, "running");
1123 assert_eq!(status.rebalance_status, "none");
1124 assert_eq!(status.used_size, 400);
1125 let info = status.decommission.expect("decommission info exists");
1126 assert!(info.queued);
1127 assert_eq!(info.queued_buckets, vec!["bucket-a"]);
1128 assert_eq!(info.bucket, "bucket-a");
1129 assert_eq!(info.object, "object.txt");
1130 assert_eq!(info.waiting_reason.as_deref(), Some("queued"));
1131 assert_eq!(info.objects_decommissioned, 2);
1132 assert_eq!(info.bytes_decommissioned_failed, 64);
1133 }
1134
1135 #[test]
1136 fn test_decommission_status_deserialization() {
1137 let json = r#"{"pools":[{"id":2,"cmdline":"/data/pool2/disk{1...4}","status":"failed","poolStatus":"blocked","decommissionInfo":{"failed":true,"totalSize":1000,"currentSize":900}}]}"#;
1138
1139 let status: DecommissionStatus = serde_json::from_str(json).unwrap();
1140
1141 assert_eq!(status.pools.len(), 1);
1142 assert_eq!(status.pools[0].id, 2);
1143 assert_eq!(status.pools[0].status, "failed");
1144 assert_eq!(status.pools[0].pool_status, "blocked");
1145 assert!(
1146 status.pools[0]
1147 .decommission
1148 .as_ref()
1149 .is_some_and(|info| info.failed)
1150 );
1151 }
1152
1153 #[test]
1154 fn test_rebalance_status_deserialization() {
1155 let json = r#"{"id":"rebalance-1","pools":[{"id":0,"status":"Started","used":0.5,"lastError":null,"cleanupWarnings":{"count":1,"lastMsg":"cleanup warning","lastBucket":"bucket","lastObject":"object","lastAt":"2026-06-12T00:00:00Z"},"progress":{"objects":3,"versions":4,"bytes":1024,"remainingBuckets":2,"bucket":"bucket","object":"object","elapsed":10,"eta":20}}],"stoppedAt":null}"#;
1156
1157 let status: RebalanceStatus = serde_json::from_str(json).unwrap();
1158
1159 assert_eq!(status.id, "rebalance-1");
1160 assert_eq!(status.pools.len(), 1);
1161 assert_eq!(status.pools[0].used, 0.5);
1162 assert_eq!(status.pools[0].cleanup_warnings.count, 1);
1163 assert_eq!(
1164 status.pools[0].cleanup_warnings.last_message.as_deref(),
1165 Some("cleanup warning")
1166 );
1167 let progress = status.pools[0]
1168 .progress
1169 .as_ref()
1170 .expect("progress should exist");
1171 assert_eq!(progress.num_objects, 3);
1172 assert_eq!(progress.remaining_buckets, 2);
1173 }
1174
1175 #[test]
1176 fn test_rebalance_status_defaults_cleanup_warnings() {
1177 let json = r#"{"id":"rebalance-1","pools":[{"id":0,"status":"Completed","used":0.5,"lastError":null,"progress":null}],"stoppedAt":null}"#;
1178
1179 let status: RebalanceStatus = serde_json::from_str(json).unwrap();
1180
1181 assert_eq!(status.pools[0].cleanup_warnings.count, 0);
1182 assert_eq!(status.pools[0].cleanup_warnings.last_message, None);
1183 }
1184
1185 #[test]
1186 fn test_serialization() {
1187 let info = ClusterInfo {
1188 mode: Some("distributed".to_string()),
1189 deployment_id: Some("test-123".to_string()),
1190 ..Default::default()
1191 };
1192
1193 let json = serde_json::to_string(&info).unwrap();
1194 assert!(json.contains("distributed"));
1195 assert!(json.contains("test-123"));
1196
1197 let deserialized: ClusterInfo = serde_json::from_str(&json).unwrap();
1198 assert_eq!(deserialized.mode, Some("distributed".to_string()));
1199 }
1200}