Skip to main content

rc_core/admin/
cluster.rs

1//! Cluster management type definitions
2//!
3//! This module contains data structures for cluster management operations
4//! including server information, disk status, and heal operations.
5
6use serde::{Deserialize, Serialize};
7use std::collections::{BTreeMap, HashMap};
8
9/// Server information representing a RustFS node
10#[derive(Debug, Clone, Serialize, Deserialize, Default)]
11#[serde(rename_all = "camelCase")]
12pub struct ServerInfo {
13    /// Server state (online, offline, initializing)
14    #[serde(default)]
15    pub state: String,
16
17    /// Server endpoint URL
18    #[serde(default)]
19    pub endpoint: String,
20
21    /// Connection scheme (http/https)
22    #[serde(default)]
23    pub scheme: String,
24
25    /// Uptime in seconds
26    #[serde(default)]
27    pub uptime: u64,
28
29    /// Server version
30    #[serde(default)]
31    pub version: String,
32
33    /// Git commit ID
34    #[serde(default, rename = "commitID")]
35    pub commit_id: String,
36
37    /// Network interfaces
38    #[serde(default)]
39    pub network: HashMap<String, String>,
40
41    /// Attached drives
42    #[serde(default, rename = "drives")]
43    pub disks: Vec<DiskInfo>,
44
45    /// Pool number
46    #[serde(default, rename = "poolNumber")]
47    pub pool_number: i32,
48
49    /// Memory statistics
50    #[serde(default, rename = "mem_stats")]
51    pub mem_stats: MemStats,
52}
53
54/// Disk information
55#[derive(Debug, Clone, Serialize, Deserialize, Default)]
56#[serde(rename_all = "camelCase")]
57pub struct DiskInfo {
58    /// Disk endpoint
59    #[serde(default)]
60    pub endpoint: String,
61
62    /// Whether this is a root disk
63    #[serde(default, rename = "rootDisk")]
64    pub root_disk: bool,
65
66    /// Drive path
67    #[serde(default, rename = "path")]
68    pub drive_path: String,
69
70    /// Whether healing is in progress
71    #[serde(default)]
72    pub healing: bool,
73
74    /// Whether scanning is in progress
75    #[serde(default)]
76    pub scanning: bool,
77
78    /// Disk state (online, offline)
79    #[serde(default)]
80    pub state: String,
81
82    /// Disk UUID
83    #[serde(default)]
84    pub uuid: String,
85
86    /// Total space in bytes
87    #[serde(default, rename = "totalspace")]
88    pub total_space: u64,
89
90    /// Used space in bytes
91    #[serde(default, rename = "usedspace")]
92    pub used_space: u64,
93
94    /// Available space in bytes
95    #[serde(default, rename = "availspace")]
96    pub available_space: u64,
97
98    /// Pool index
99    #[serde(default, alias = "pool_index")]
100    pub pool_index: i32,
101
102    /// Set index
103    #[serde(default, alias = "set_index")]
104    pub set_index: i32,
105
106    /// Disk index within set
107    #[serde(default, alias = "disk_index")]
108    pub disk_index: i32,
109
110    /// Healing info if disk is being healed
111    #[serde(default, skip_serializing_if = "Option::is_none")]
112    pub heal_info: Option<HealingDiskInfo>,
113}
114
115/// Healing disk information
116#[derive(Debug, Clone, Serialize, Deserialize, Default)]
117#[serde(rename_all = "camelCase")]
118pub struct HealingDiskInfo {
119    /// Heal ID
120    #[serde(default)]
121    pub id: String,
122
123    /// Heal session ID
124    #[serde(default)]
125    pub heal_id: String,
126
127    /// Pool index
128    #[serde(default)]
129    pub pool_index: Option<usize>,
130
131    /// Set index
132    #[serde(default)]
133    pub set_index: Option<usize>,
134
135    /// Disk index
136    #[serde(default)]
137    pub disk_index: Option<usize>,
138
139    /// Endpoint being healed
140    #[serde(default)]
141    pub endpoint: String,
142
143    /// Path being healed
144    #[serde(default)]
145    pub path: String,
146
147    /// Objects total count
148    #[serde(default)]
149    pub objects_total_count: u64,
150
151    /// Objects total size
152    #[serde(default)]
153    pub objects_total_size: u64,
154
155    /// Items healed count
156    #[serde(default)]
157    pub items_healed: u64,
158
159    /// Items failed count
160    #[serde(default)]
161    pub items_failed: u64,
162
163    /// Bytes done
164    #[serde(default)]
165    pub bytes_done: u64,
166
167    /// Whether healing is finished
168    #[serde(default)]
169    pub finished: bool,
170
171    /// Current bucket being healed
172    #[serde(default)]
173    pub bucket: String,
174
175    /// Current object being healed
176    #[serde(default)]
177    pub object: String,
178}
179
180/// Memory statistics
181#[derive(Debug, Clone, Serialize, Deserialize, Default)]
182pub struct MemStats {
183    /// Current allocated memory
184    #[serde(default)]
185    pub alloc: u64,
186
187    /// Total allocated memory over lifetime
188    #[serde(default)]
189    pub total_alloc: u64,
190
191    /// Heap allocated memory
192    #[serde(default)]
193    pub heap_alloc: u64,
194}
195
196/// Storage backend type
197#[derive(Debug, Clone, Serialize, Deserialize, Default)]
198#[serde(rename_all = "lowercase")]
199pub enum BackendType {
200    /// Filesystem backend (single drive)
201    #[default]
202    #[serde(rename = "FS")]
203    Fs,
204    /// Erasure coding backend (distributed)
205    #[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/// Backend information
219#[derive(Debug, Clone, Serialize, Deserialize, Default)]
220#[serde(rename_all = "camelCase")]
221pub struct BackendInfo {
222    /// Backend type
223    #[serde(default, rename = "backendType")]
224    pub backend_type: BackendType,
225
226    /// Number of online disks
227    #[serde(default, rename = "onlineDisks")]
228    pub online_disks: usize,
229
230    /// Number of offline disks
231    #[serde(default, rename = "offlineDisks")]
232    pub offline_disks: usize,
233
234    /// Standard storage class parity
235    #[serde(default, rename = "standardSCParity")]
236    pub standard_sc_parity: Option<usize>,
237
238    /// Reduced redundancy storage class parity
239    #[serde(default, rename = "rrSCParity")]
240    pub rr_sc_parity: Option<usize>,
241
242    /// Total erasure sets
243    #[serde(default, rename = "totalSets")]
244    pub total_sets: Vec<usize>,
245
246    /// Drives per erasure set
247    #[serde(default, rename = "totalDrivesPerSet")]
248    pub drives_per_set: Vec<usize>,
249}
250
251/// Cluster usage statistics
252#[derive(Debug, Clone, Serialize, Deserialize, Default)]
253pub struct UsageInfo {
254    /// Total storage size in bytes
255    #[serde(default)]
256    pub size: u64,
257
258    /// Error message if any
259    #[serde(default, skip_serializing_if = "Option::is_none")]
260    pub error: Option<String>,
261}
262
263/// Bucket count information
264#[derive(Debug, Clone, Serialize, Deserialize, Default)]
265pub struct BucketsInfo {
266    /// Number of buckets
267    #[serde(default)]
268    pub count: u64,
269
270    /// Error message if any
271    #[serde(default, skip_serializing_if = "Option::is_none")]
272    pub error: Option<String>,
273}
274
275/// Object count information
276#[derive(Debug, Clone, Serialize, Deserialize, Default)]
277pub struct ObjectsInfo {
278    /// Number of objects
279    #[serde(default)]
280    pub count: u64,
281
282    /// Error message if any
283    #[serde(default, skip_serializing_if = "Option::is_none")]
284    pub error: Option<String>,
285}
286
287/// Pool erasure set metrics returned by cluster information.
288#[derive(Debug, Clone, Serialize, Deserialize, Default)]
289#[serde(rename_all = "camelCase")]
290pub struct PoolErasureSetInfo {
291    /// Erasure set ID within the pool.
292    #[serde(default)]
293    pub id: i32,
294
295    /// Raw used capacity in bytes.
296    #[serde(default, rename = "rawUsage")]
297    pub raw_usage: u64,
298
299    /// Raw total capacity in bytes.
300    #[serde(default, rename = "rawCapacity")]
301    pub raw_capacity: u64,
302
303    /// Object data usage in bytes.
304    #[serde(default)]
305    pub usage: u64,
306
307    /// Number of objects in the set.
308    #[serde(default, rename = "objectsCount")]
309    pub objects_count: u64,
310
311    /// Number of versions in the set.
312    #[serde(default, rename = "versionsCount")]
313    pub versions_count: u64,
314
315    /// Number of delete markers in the set.
316    #[serde(default, rename = "deleteMarkersCount")]
317    pub delete_markers_count: u64,
318
319    /// Number of healing disks in the set.
320    #[serde(default, rename = "healDisks")]
321    pub heal_disks: i32,
322}
323
324/// Complete cluster information response
325#[derive(Debug, Clone, Serialize, Deserialize, Default)]
326#[serde(rename_all = "camelCase")]
327pub struct ClusterInfo {
328    /// Deployment mode (distributed, standalone)
329    #[serde(default)]
330    pub mode: Option<String>,
331
332    /// Domain names
333    #[serde(default)]
334    pub domain: Option<Vec<String>>,
335
336    /// Region
337    #[serde(default)]
338    pub region: Option<String>,
339
340    /// Deployment ID
341    #[serde(default, rename = "deploymentID")]
342    pub deployment_id: Option<String>,
343
344    /// Bucket information
345    #[serde(default)]
346    pub buckets: Option<BucketsInfo>,
347
348    /// Object information
349    #[serde(default)]
350    pub objects: Option<ObjectsInfo>,
351
352    /// Storage usage
353    #[serde(default)]
354    pub usage: Option<UsageInfo>,
355
356    /// Backend information
357    #[serde(default)]
358    pub backend: Option<BackendInfo>,
359
360    /// Server information
361    #[serde(default)]
362    pub servers: Option<Vec<ServerInfo>>,
363
364    /// Pool metrics keyed by pool and erasure set index.
365    #[serde(default)]
366    pub pools: Option<BTreeMap<i32, BTreeMap<i32, PoolErasureSetInfo>>>,
367}
368
369impl ClusterInfo {
370    /// Get the total number of online disks across all servers
371    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    /// Get the total number of offline disks across all servers
385    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    /// Get total storage capacity in bytes
399    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    /// Get used storage in bytes
413    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/// Heal operation mode
428#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
429#[serde(rename_all = "lowercase")]
430pub enum HealScanMode {
431    /// Normal scan (default)
432    #[default]
433    Normal,
434    /// Deep scan (slower but more thorough)
435    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/// Node coverage of a background heal snapshot; missing counts are unknown.
472#[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/// Request to start a heal operation
485#[derive(Debug, Clone, Serialize, Deserialize, Default)]
486#[serde(rename_all = "camelCase")]
487pub struct HealStartRequest {
488    /// Bucket to heal (empty for all buckets)
489    #[serde(default, skip_serializing_if = "Option::is_none")]
490    pub bucket: Option<String>,
491
492    /// Object prefix to heal
493    #[serde(default, skip_serializing_if = "Option::is_none")]
494    pub prefix: Option<String>,
495
496    /// Scan mode
497    #[serde(default)]
498    pub scan_mode: HealScanMode,
499
500    /// Whether to remove dangling objects
501    #[serde(default)]
502    pub remove: bool,
503
504    /// Whether to recreate missing data
505    #[serde(default)]
506    pub recreate: bool,
507
508    /// Dry run mode (don't actually heal)
509    #[serde(default)]
510    pub dry_run: bool,
511}
512
513/// Request to inspect or stop a token-scoped heal task
514#[derive(Debug, Clone, Serialize, Deserialize)]
515#[serde(rename_all = "camelCase")]
516pub struct HealTaskRequest {
517    /// Bucket being healed
518    pub bucket: String,
519
520    /// Object prefix being healed
521    #[serde(default, skip_serializing_if = "Option::is_none")]
522    pub prefix: Option<String>,
523
524    /// Client token returned by the heal start request
525    pub client_token: String,
526}
527
528/// Information about a single heal drive
529#[derive(Debug, Clone, Serialize, Deserialize, Default)]
530pub struct HealDriveInfo {
531    /// Drive UUID
532    #[serde(default)]
533    pub uuid: String,
534
535    /// Drive endpoint
536    #[serde(default)]
537    pub endpoint: String,
538
539    /// Drive state
540    #[serde(default)]
541    pub state: String,
542}
543
544/// Result of a heal operation on a single item
545#[derive(Debug, Clone, Serialize, Deserialize, Default)]
546#[serde(rename_all = "camelCase")]
547pub struct HealResultItem {
548    /// Result index
549    #[serde(default, rename = "resultId")]
550    pub result_index: usize,
551
552    /// Type of item healed (bucket, object, metadata)
553    #[serde(default, rename = "type")]
554    pub item_type: String,
555
556    /// Bucket name
557    #[serde(default)]
558    pub bucket: String,
559
560    /// Object key
561    #[serde(default)]
562    pub object: String,
563
564    /// Version ID
565    #[serde(default, rename = "versionId")]
566    pub version_id: String,
567
568    /// Detail message
569    #[serde(default)]
570    pub detail: String,
571
572    /// Number of parity blocks
573    #[serde(default, rename = "parityBlocks")]
574    pub parity_blocks: usize,
575
576    /// Number of data blocks
577    #[serde(default, rename = "dataBlocks")]
578    pub data_blocks: usize,
579
580    /// Object size
581    #[serde(default, rename = "objectSize")]
582    pub object_size: u64,
583
584    /// Drive info before healing
585    #[serde(default)]
586    pub before: HealDriveInfos,
587
588    /// Drive info after healing
589    #[serde(default)]
590    pub after: HealDriveInfos,
591}
592
593/// Collection of heal drive infos
594#[derive(Debug, Clone, Serialize, Deserialize, Default)]
595pub struct HealDriveInfos {
596    /// Drive information
597    #[serde(default)]
598    pub drives: Vec<HealDriveInfo>,
599}
600
601/// Status of a heal operation
602#[derive(Debug, Clone, Serialize, Deserialize, Default)]
603#[serde(rename_all = "camelCase")]
604pub struct HealStatus {
605    /// Heal ID
606    #[serde(default)]
607    pub heal_id: String,
608
609    /// Whether healing is in progress
610    #[serde(default)]
611    pub healing: bool,
612
613    #[serde(default, skip_serializing_if = "Option::is_none")]
614    pub state: Option<HealRuntimeState>,
615
616    /// Absent on legacy servers and token-scoped task responses.
617    #[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    /// Task summary for token-scoped manual heal status
624    #[serde(default, skip_serializing_if = "Option::is_none")]
625    pub summary: Option<String>,
626
627    /// Task detail for token-scoped manual heal status
628    #[serde(default, skip_serializing_if = "Option::is_none")]
629    pub detail: Option<String>,
630
631    /// Current bucket being healed
632    #[serde(default)]
633    pub bucket: String,
634
635    /// Current object being healed
636    #[serde(default)]
637    pub object: String,
638
639    /// Current scan mode reported by background healing
640    #[serde(default, skip_serializing_if = "Option::is_none")]
641    pub scan_mode: Option<HealScanMode>,
642
643    /// Background heal scan cycle
644    #[serde(default)]
645    pub scan_cycle: u64,
646
647    /// Number of queued heal tasks
648    #[serde(default)]
649    pub heal_queue_length: u64,
650
651    /// Number of active heal tasks
652    #[serde(default)]
653    pub heal_active_tasks: u64,
654
655    /// Number of items scanned
656    #[serde(default)]
657    pub items_scanned: u64,
658
659    /// Number of items healed
660    #[serde(default)]
661    pub items_healed: u64,
662
663    /// Number of items failed
664    #[serde(default)]
665    pub items_failed: u64,
666
667    /// Bytes scanned
668    #[serde(default)]
669    pub bytes_scanned: u64,
670
671    /// Bytes healed
672    #[serde(default)]
673    pub bytes_healed: u64,
674
675    /// Start time
676    #[serde(default)]
677    pub started: Option<String>,
678
679    /// Last update time
680    #[serde(default)]
681    pub last_update: Option<String>,
682}
683
684/// Request targeting a storage pool by command line or numeric ID.
685#[derive(Debug, Clone, Serialize, Deserialize, Default)]
686#[serde(rename_all = "camelCase")]
687pub struct PoolTarget {
688    /// Pool command line, or zero-based pool ID when `by_id` is true.
689    pub pool: String,
690
691    /// Interpret `pool` as a zero-based pool ID.
692    #[serde(default)]
693    pub by_id: bool,
694}
695
696/// Status of a server pool.
697#[derive(Debug, Clone, Serialize, Deserialize, Default)]
698pub struct PoolStatus {
699    /// Zero-based pool ID.
700    #[serde(default)]
701    pub id: usize,
702
703    /// Pool command line used by the server process.
704    #[serde(default, rename = "cmdline")]
705    pub cmd_line: String,
706
707    /// Last pool metadata update timestamp.
708    #[serde(default, rename = "lastUpdate")]
709    pub last_update: String,
710
711    /// Pool lifecycle status.
712    #[serde(default)]
713    pub status: String,
714
715    /// Decommission operation status for this pool.
716    #[serde(default, rename = "decommissionStatus")]
717    pub decommission_status: String,
718
719    /// Rebalance operation status for this pool.
720    #[serde(default, rename = "rebalanceStatus")]
721    pub rebalance_status: String,
722
723    /// Total pool size in bytes.
724    #[serde(default, rename = "totalSize")]
725    pub total_size: u64,
726
727    /// Current free size in bytes.
728    #[serde(default, rename = "currentSize")]
729    pub current_size: u64,
730
731    /// Used pool size in bytes.
732    #[serde(default, rename = "usedSize")]
733    pub used_size: u64,
734
735    /// Used capacity ratio in the range 0.0..=1.0.
736    #[serde(default)]
737    pub used: f64,
738
739    /// Decommission status and progress for this pool.
740    #[serde(default, rename = "decommissionInfo")]
741    pub decommission: Option<PoolDecommissionInfo>,
742}
743
744/// Decommission status response.
745#[derive(Debug, Clone, Serialize, Deserialize, Default)]
746pub struct DecommissionStatus {
747    /// Per-pool decommission status.
748    #[serde(default)]
749    pub pools: Vec<DecommissionPoolStatus>,
750}
751
752/// Decommission operation status for a single pool.
753#[derive(Debug, Clone, Serialize, Deserialize, Default)]
754pub struct DecommissionPoolStatus {
755    /// Zero-based pool ID.
756    #[serde(default)]
757    pub id: usize,
758
759    /// Pool command line used by the server process.
760    #[serde(default, rename = "cmdline")]
761    pub cmd_line: String,
762
763    /// Decommission operation status for this pool.
764    #[serde(default)]
765    pub status: String,
766
767    /// Pool lifecycle status.
768    #[serde(default, rename = "poolStatus")]
769    pub pool_status: String,
770
771    /// Decommission state and progress for this pool.
772    #[serde(default, rename = "decommissionInfo")]
773    pub decommission: Option<PoolDecommissionInfo>,
774}
775
776/// Decommission state and progress for a server pool.
777#[derive(Debug, Clone, Serialize, Deserialize, Default)]
778pub struct PoolDecommissionInfo {
779    /// Decommission start timestamp.
780    #[serde(default, rename = "startTime")]
781    pub start_time: Option<String>,
782
783    /// Free bytes when decommission started.
784    #[serde(default, rename = "startSize")]
785    pub start_size: u64,
786
787    /// Total pool size in bytes.
788    #[serde(default, rename = "totalSize")]
789    pub total_size: u64,
790
791    /// Current free size in bytes.
792    #[serde(default, rename = "currentSize")]
793    pub current_size: u64,
794
795    /// Whether decommission completed.
796    #[serde(default)]
797    pub complete: bool,
798
799    /// Whether decommission failed.
800    #[serde(default)]
801    pub failed: bool,
802
803    /// Whether decommission was canceled.
804    #[serde(default)]
805    pub canceled: bool,
806
807    /// Whether decommission is queued.
808    #[serde(default)]
809    pub queued: bool,
810
811    /// Buckets waiting to be decommissioned.
812    #[serde(default, rename = "queuedBuckets")]
813    pub queued_buckets: Vec<String>,
814
815    /// Buckets already decommissioned.
816    #[serde(default, rename = "decommissionedBuckets")]
817    pub decommissioned_buckets: Vec<String>,
818
819    /// Current bucket.
820    #[serde(default)]
821    pub bucket: String,
822
823    /// Current prefix.
824    #[serde(default)]
825    pub prefix: String,
826
827    /// Current object.
828    #[serde(default)]
829    pub object: String,
830
831    /// Current decommission stage.
832    #[serde(default)]
833    pub stage: String,
834
835    /// Number of successfully decommissioned objects.
836    #[serde(default, rename = "objectsDecommissioned")]
837    pub objects_decommissioned: u64,
838
839    /// Number of objects that failed to decommission.
840    #[serde(default, rename = "objectsDecommissionedFailed")]
841    pub objects_decommissioned_failed: u64,
842
843    /// Bytes successfully moved off the pool.
844    #[serde(default, rename = "bytesDecommissioned")]
845    pub bytes_decommissioned: u64,
846
847    /// Bytes that failed to move off the pool.
848    #[serde(default, rename = "bytesDecommissionedFailed")]
849    pub bytes_decommissioned_failed: u64,
850
851    /// Reason why decommission is waiting.
852    #[serde(default, rename = "waitingReason")]
853    pub waiting_reason: Option<String>,
854}
855
856/// Response from starting a rebalance operation.
857#[derive(Debug, Clone, Serialize, Deserialize, Default)]
858pub struct RebalanceStartResult {
859    /// Rebalance operation ID.
860    #[serde(default)]
861    pub id: String,
862}
863
864/// Cluster-wide rebalance status.
865#[derive(Debug, Clone, Serialize, Deserialize, Default)]
866pub struct RebalanceStatus {
867    /// Rebalance operation ID.
868    #[serde(default)]
869    pub id: String,
870
871    /// Per-pool rebalance status.
872    #[serde(default)]
873    pub pools: Vec<RebalancePoolStatus>,
874
875    /// Timestamp when rebalance was stopped.
876    #[serde(default, rename = "stoppedAt")]
877    pub stopped_at: Option<String>,
878}
879
880/// Rebalance status for a single pool.
881#[derive(Debug, Clone, Serialize, Deserialize, Default)]
882pub struct RebalancePoolStatus {
883    /// Zero-based pool ID.
884    #[serde(default)]
885    pub id: usize,
886
887    /// Rebalance status for this pool.
888    #[serde(default)]
889    pub status: String,
890
891    /// Used capacity ratio in the range 0.0..=1.0.
892    #[serde(default)]
893    pub used: f64,
894
895    /// Last rebalance error, if any.
896    #[serde(default, rename = "lastError")]
897    pub last_error: Option<String>,
898
899    /// Cleanup warnings observed after this pool finishes rebalance.
900    #[serde(default, rename = "cleanupWarnings")]
901    pub cleanup_warnings: RebalanceCleanupWarnings,
902
903    /// Rebalance progress, if this pool is active.
904    #[serde(default)]
905    pub progress: Option<RebalancePoolProgress>,
906}
907
908/// Cleanup warnings recorded for a rebalanced pool.
909#[derive(Debug, Clone, Serialize, Deserialize, Default)]
910pub struct RebalanceCleanupWarnings {
911    /// Number of cleanup warnings observed.
912    #[serde(default)]
913    pub count: u64,
914
915    /// Last cleanup warning message.
916    #[serde(default, rename = "lastMsg")]
917    pub last_message: Option<String>,
918
919    /// Bucket associated with the last cleanup warning.
920    #[serde(default, rename = "lastBucket")]
921    pub last_bucket: Option<String>,
922
923    /// Object associated with the last cleanup warning.
924    #[serde(default, rename = "lastObject")]
925    pub last_object: Option<String>,
926
927    /// Timestamp of the last cleanup warning.
928    #[serde(default, rename = "lastAt")]
929    pub last_at: Option<String>,
930}
931
932/// Rebalance progress for a single pool.
933#[derive(Debug, Clone, Serialize, Deserialize, Default)]
934pub struct RebalancePoolProgress {
935    /// Number of objects moved.
936    #[serde(default, rename = "objects")]
937    pub num_objects: u64,
938
939    /// Number of object versions moved.
940    #[serde(default, rename = "versions")]
941    pub num_versions: u64,
942
943    /// Number of bytes moved.
944    #[serde(default)]
945    pub bytes: u64,
946
947    /// Number of buckets remaining.
948    #[serde(default, rename = "remainingBuckets")]
949    pub remaining_buckets: usize,
950
951    /// Current bucket.
952    #[serde(default)]
953    pub bucket: String,
954
955    /// Current object.
956    #[serde(default)]
957    pub object: String,
958
959    /// Elapsed seconds.
960    #[serde(default)]
961    pub elapsed: u64,
962
963    /// Estimated seconds remaining.
964    #[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}