Skip to main content

talos_api_rs/resources/
reset.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Typed wrappers for reset operations.
4//!
5//! The Reset API is used to reset/wipe a Talos node. This is typically used
6//! for destroying clusters or removing nodes from a cluster.
7
8use crate::api::machine::{
9    reset_request::WipeMode as ProtoWipeMode, Reset as ProtoReset,
10    ResetPartitionSpec as ProtoPartitionSpec, ResetRequest as ProtoRequest,
11    ResetResponse as ProtoResponse,
12};
13
14/// Mode for wiping disks during reset.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum WipeMode {
17    /// Wipe all disks (system and user).
18    #[default]
19    All,
20    /// Wipe only the system disk.
21    SystemDisk,
22    /// Wipe only user disks.
23    UserDisks,
24}
25
26impl From<WipeMode> for i32 {
27    fn from(mode: WipeMode) -> Self {
28        match mode {
29            WipeMode::All => ProtoWipeMode::All as i32,
30            WipeMode::SystemDisk => ProtoWipeMode::SystemDisk as i32,
31            WipeMode::UserDisks => ProtoWipeMode::UserDisks as i32,
32        }
33    }
34}
35
36impl From<i32> for WipeMode {
37    fn from(value: i32) -> Self {
38        match value {
39            0 => WipeMode::All,
40            1 => WipeMode::SystemDisk,
41            2 => WipeMode::UserDisks,
42            _ => WipeMode::All,
43        }
44    }
45}
46
47impl std::fmt::Display for WipeMode {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        match self {
50            WipeMode::All => write!(f, "all"),
51            WipeMode::SystemDisk => write!(f, "system-disk"),
52            WipeMode::UserDisks => write!(f, "user-disks"),
53        }
54    }
55}
56
57/// Specification for a partition to wipe.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct ResetPartitionSpec {
60    /// Partition label.
61    pub label: String,
62    /// Whether to wipe the partition.
63    pub wipe: bool,
64}
65
66impl ResetPartitionSpec {
67    /// Create a new partition spec.
68    #[must_use]
69    pub fn new(label: impl Into<String>, wipe: bool) -> Self {
70        Self {
71            label: label.into(),
72            wipe,
73        }
74    }
75
76    /// Create a partition spec that will be wiped.
77    #[must_use]
78    pub fn wipe(label: impl Into<String>) -> Self {
79        Self::new(label, true)
80    }
81}
82
83impl From<ResetPartitionSpec> for ProtoPartitionSpec {
84    fn from(spec: ResetPartitionSpec) -> Self {
85        ProtoPartitionSpec {
86            label: spec.label,
87            wipe: spec.wipe,
88        }
89    }
90}
91
92/// Request to reset a Talos node.
93///
94/// # Example
95///
96/// ```no_run
97/// use talos_api_rs::resources::{ResetRequest, WipeMode};
98///
99/// // Graceful reset with reboot (recommended for cluster nodes)
100/// let request = ResetRequest::graceful();
101///
102/// // Quick reset without etcd leave (for standalone nodes)
103/// let request = ResetRequest::builder()
104///     .graceful(false)
105///     .reboot(true)
106///     .wipe_mode(WipeMode::SystemDisk)
107///     .build();
108/// ```
109#[derive(Debug, Clone, Default)]
110pub struct ResetRequest {
111    /// If true, node will gracefully leave etcd before reset.
112    pub graceful: bool,
113    /// If true, node will reboot after reset (otherwise halt).
114    pub reboot: bool,
115    /// Specific system partitions to wipe.
116    pub system_partitions_to_wipe: Vec<ResetPartitionSpec>,
117    /// Specific user disks to wipe.
118    pub user_disks_to_wipe: Vec<String>,
119    /// Wipe mode (all, system-disk, user-disks).
120    pub mode: WipeMode,
121}
122
123impl ResetRequest {
124    /// Create a new builder for customizing the reset request.
125    #[must_use]
126    pub fn builder() -> ResetRequestBuilder {
127        ResetRequestBuilder::default()
128    }
129
130    /// Create a graceful reset request.
131    ///
132    /// This will:
133    /// - Gracefully leave etcd (if control-plane)
134    /// - Reboot after reset
135    /// - Wipe all disks
136    #[must_use]
137    pub fn graceful() -> Self {
138        Self {
139            graceful: true,
140            reboot: true,
141            mode: WipeMode::All,
142            ..Default::default()
143        }
144    }
145
146    /// Create a forceful reset request (no etcd leave).
147    ///
148    /// This will:
149    /// - Skip etcd graceful leave
150    /// - Reboot after reset
151    /// - Wipe all disks
152    #[must_use]
153    pub fn force() -> Self {
154        Self {
155            graceful: false,
156            reboot: true,
157            mode: WipeMode::All,
158            ..Default::default()
159        }
160    }
161
162    /// Create a halt request (reset without reboot).
163    #[must_use]
164    pub fn halt() -> Self {
165        Self {
166            graceful: true,
167            reboot: false,
168            mode: WipeMode::All,
169            ..Default::default()
170        }
171    }
172}
173
174impl From<ResetRequest> for ProtoRequest {
175    fn from(req: ResetRequest) -> Self {
176        ProtoRequest {
177            graceful: req.graceful,
178            reboot: req.reboot,
179            system_partitions_to_wipe: req
180                .system_partitions_to_wipe
181                .into_iter()
182                .map(Into::into)
183                .collect(),
184            user_disks_to_wipe: req.user_disks_to_wipe,
185            mode: req.mode.into(),
186        }
187    }
188}
189
190/// Builder for `ResetRequest`.
191#[derive(Debug, Clone, Default)]
192pub struct ResetRequestBuilder {
193    graceful: bool,
194    reboot: bool,
195    system_partitions_to_wipe: Vec<ResetPartitionSpec>,
196    user_disks_to_wipe: Vec<String>,
197    mode: WipeMode,
198}
199
200impl ResetRequestBuilder {
201    /// Set whether to gracefully leave etcd.
202    #[must_use]
203    pub fn graceful(mut self, graceful: bool) -> Self {
204        self.graceful = graceful;
205        self
206    }
207
208    /// Set whether to reboot after reset.
209    #[must_use]
210    pub fn reboot(mut self, reboot: bool) -> Self {
211        self.reboot = reboot;
212        self
213    }
214
215    /// Set the wipe mode.
216    #[must_use]
217    pub fn wipe_mode(mut self, mode: WipeMode) -> Self {
218        self.mode = mode;
219        self
220    }
221
222    /// Add a system partition to wipe.
223    #[must_use]
224    pub fn wipe_partition(mut self, spec: ResetPartitionSpec) -> Self {
225        self.system_partitions_to_wipe.push(spec);
226        self
227    }
228
229    /// Add a user disk to wipe.
230    #[must_use]
231    pub fn wipe_user_disk(mut self, disk: impl Into<String>) -> Self {
232        self.user_disks_to_wipe.push(disk.into());
233        self
234    }
235
236    /// Build the reset request.
237    #[must_use]
238    pub fn build(self) -> ResetRequest {
239        ResetRequest {
240            graceful: self.graceful,
241            reboot: self.reboot,
242            system_partitions_to_wipe: self.system_partitions_to_wipe,
243            user_disks_to_wipe: self.user_disks_to_wipe,
244            mode: self.mode,
245        }
246    }
247}
248
249/// Result of a reset operation for a single node.
250#[derive(Debug, Clone)]
251pub struct ResetResult {
252    /// Node hostname (if available from metadata).
253    pub node: Option<String>,
254    /// Actor ID that initiated the reset.
255    pub actor_id: String,
256}
257
258impl From<ProtoReset> for ResetResult {
259    fn from(proto: ProtoReset) -> Self {
260        Self {
261            node: proto.metadata.map(|m| m.hostname),
262            actor_id: proto.actor_id,
263        }
264    }
265}
266
267/// Response from a reset operation.
268#[derive(Debug, Clone)]
269pub struct ResetResponse {
270    /// Results from each node.
271    pub results: Vec<ResetResult>,
272}
273
274impl From<ProtoResponse> for ResetResponse {
275    fn from(proto: ProtoResponse) -> Self {
276        Self {
277            results: proto.messages.into_iter().map(Into::into).collect(),
278        }
279    }
280}
281
282impl ResetResponse {
283    /// Check if the reset was initiated successfully.
284    #[must_use]
285    pub fn is_success(&self) -> bool {
286        !self.results.is_empty()
287    }
288
289    /// Get the first result (useful for single-node operations).
290    #[must_use]
291    pub fn first(&self) -> Option<&ResetResult> {
292        self.results.first()
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    #[test]
301    fn test_wipe_mode_conversion() {
302        assert_eq!(i32::from(WipeMode::All), 0);
303        assert_eq!(i32::from(WipeMode::SystemDisk), 1);
304        assert_eq!(i32::from(WipeMode::UserDisks), 2);
305
306        assert_eq!(WipeMode::from(0), WipeMode::All);
307        assert_eq!(WipeMode::from(1), WipeMode::SystemDisk);
308        assert_eq!(WipeMode::from(2), WipeMode::UserDisks);
309    }
310
311    #[test]
312    fn test_wipe_mode_display() {
313        assert_eq!(WipeMode::All.to_string(), "all");
314        assert_eq!(WipeMode::SystemDisk.to_string(), "system-disk");
315        assert_eq!(WipeMode::UserDisks.to_string(), "user-disks");
316    }
317
318    #[test]
319    fn test_reset_request_graceful() {
320        let request = ResetRequest::graceful();
321        assert!(request.graceful);
322        assert!(request.reboot);
323        assert_eq!(request.mode, WipeMode::All);
324    }
325
326    #[test]
327    fn test_reset_request_force() {
328        let request = ResetRequest::force();
329        assert!(!request.graceful);
330        assert!(request.reboot);
331        assert_eq!(request.mode, WipeMode::All);
332    }
333
334    #[test]
335    fn test_reset_request_halt() {
336        let request = ResetRequest::halt();
337        assert!(request.graceful);
338        assert!(!request.reboot);
339    }
340
341    #[test]
342    fn test_reset_request_builder() {
343        let request = ResetRequest::builder()
344            .graceful(true)
345            .reboot(false)
346            .wipe_mode(WipeMode::SystemDisk)
347            .wipe_partition(ResetPartitionSpec::wipe("STATE"))
348            .wipe_user_disk("/dev/sdb")
349            .build();
350
351        assert!(request.graceful);
352        assert!(!request.reboot);
353        assert_eq!(request.mode, WipeMode::SystemDisk);
354        assert_eq!(request.system_partitions_to_wipe.len(), 1);
355        assert_eq!(request.user_disks_to_wipe.len(), 1);
356    }
357
358    #[test]
359    fn test_proto_conversion() {
360        let request = ResetRequest::builder()
361            .graceful(true)
362            .reboot(true)
363            .wipe_mode(WipeMode::UserDisks)
364            .build();
365
366        let proto: ProtoRequest = request.into();
367        assert!(proto.graceful);
368        assert!(proto.reboot);
369        assert_eq!(proto.mode, ProtoWipeMode::UserDisks as i32);
370    }
371
372    #[test]
373    fn test_partition_spec() {
374        let spec = ResetPartitionSpec::wipe("STATE");
375        assert_eq!(spec.label, "STATE");
376        assert!(spec.wipe);
377
378        let spec2 = ResetPartitionSpec::new("EPHEMERAL", false);
379        assert_eq!(spec2.label, "EPHEMERAL");
380        assert!(!spec2.wipe);
381    }
382
383    #[test]
384    fn test_reset_response_is_success() {
385        let response = ResetResponse {
386            results: vec![ResetResult {
387                node: Some("node1".to_string()),
388                actor_id: "actor-123".to_string(),
389            }],
390        };
391        assert!(response.is_success());
392
393        let empty = ResetResponse { results: vec![] };
394        assert!(!empty.is_success());
395    }
396}