Skip to main content

talos_api_rs/resources/
advanced.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Typed wrappers for Advanced APIs.
4//!
5//! Provides Rollback, GenerateClientConfiguration, PacketCapture, and Netstat operations.
6
7use crate::api::generated::machine::{
8    ConnectRecord as ProtoConnectRecord, GenerateClientConfiguration as ProtoGenerateClientConfig,
9    GenerateClientConfigurationRequest as ProtoGenerateClientConfigRequest,
10    GenerateClientConfigurationResponse as ProtoGenerateClientConfigResponse,
11    Netstat as ProtoNetstat, NetstatRequest as ProtoNetstatRequest,
12    NetstatResponse as ProtoNetstatResponse, PacketCaptureRequest as ProtoPacketCaptureRequest,
13    RollbackResponse as ProtoRollbackResponse,
14};
15
16// =============================================================================
17// Rollback
18// =============================================================================
19
20/// Response from a rollback request.
21#[derive(Debug, Clone)]
22pub struct RollbackResponse {
23    /// Results from each node.
24    pub results: Vec<RollbackResult>,
25}
26
27/// Result of a rollback operation on a node.
28#[derive(Debug, Clone)]
29pub struct RollbackResult {
30    /// Node that returned this result.
31    pub node: Option<String>,
32}
33
34impl From<ProtoRollbackResponse> for RollbackResponse {
35    fn from(proto: ProtoRollbackResponse) -> Self {
36        Self {
37            results: proto
38                .messages
39                .into_iter()
40                .map(|m| RollbackResult {
41                    node: m.metadata.map(|meta| meta.hostname),
42                })
43                .collect(),
44        }
45    }
46}
47
48impl RollbackResponse {
49    /// Get the first result.
50    #[must_use]
51    pub fn first(&self) -> Option<&RollbackResult> {
52        self.results.first()
53    }
54
55    /// Check if rollback succeeded on all nodes.
56    #[must_use]
57    pub fn is_success(&self) -> bool {
58        !self.results.is_empty()
59    }
60}
61
62// =============================================================================
63// GenerateClientConfiguration
64// =============================================================================
65
66/// Request to generate client configuration (talosconfig).
67#[derive(Debug, Clone, Default)]
68pub struct GenerateClientConfigurationRequest {
69    /// Roles for the generated client certificate.
70    pub roles: Vec<String>,
71    /// Certificate TTL in seconds.
72    pub crt_ttl_seconds: Option<i64>,
73}
74
75impl GenerateClientConfigurationRequest {
76    /// Create a new request with default settings.
77    #[must_use]
78    pub fn new() -> Self {
79        Self::default()
80    }
81
82    /// Create a request with specific roles.
83    #[must_use]
84    pub fn with_roles(roles: Vec<String>) -> Self {
85        Self {
86            roles,
87            crt_ttl_seconds: None,
88        }
89    }
90
91    /// Create a builder.
92    #[must_use]
93    pub fn builder() -> GenerateClientConfigurationRequestBuilder {
94        GenerateClientConfigurationRequestBuilder::default()
95    }
96}
97
98impl From<GenerateClientConfigurationRequest> for ProtoGenerateClientConfigRequest {
99    fn from(req: GenerateClientConfigurationRequest) -> Self {
100        Self {
101            roles: req.roles,
102            crt_ttl: req.crt_ttl_seconds.map(|s| prost_types::Duration {
103                seconds: s,
104                nanos: 0,
105            }),
106        }
107    }
108}
109
110/// Builder for `GenerateClientConfigurationRequest`.
111#[derive(Debug, Clone, Default)]
112pub struct GenerateClientConfigurationRequestBuilder {
113    roles: Vec<String>,
114    crt_ttl_seconds: Option<i64>,
115}
116
117impl GenerateClientConfigurationRequestBuilder {
118    /// Add a role.
119    #[must_use]
120    pub fn role(mut self, role: impl Into<String>) -> Self {
121        self.roles.push(role.into());
122        self
123    }
124
125    /// Add multiple roles.
126    #[must_use]
127    pub fn roles(mut self, roles: Vec<String>) -> Self {
128        self.roles.extend(roles);
129        self
130    }
131
132    /// Set certificate TTL in seconds.
133    #[must_use]
134    pub fn crt_ttl_seconds(mut self, ttl: i64) -> Self {
135        self.crt_ttl_seconds = Some(ttl);
136        self
137    }
138
139    /// Set certificate TTL in hours.
140    #[must_use]
141    pub fn crt_ttl_hours(mut self, hours: i64) -> Self {
142        self.crt_ttl_seconds = Some(hours * 3600);
143        self
144    }
145
146    /// Set certificate TTL in days.
147    #[must_use]
148    pub fn crt_ttl_days(mut self, days: i64) -> Self {
149        self.crt_ttl_seconds = Some(days * 86400);
150        self
151    }
152
153    /// Build the request.
154    #[must_use]
155    pub fn build(self) -> GenerateClientConfigurationRequest {
156        GenerateClientConfigurationRequest {
157            roles: self.roles,
158            crt_ttl_seconds: self.crt_ttl_seconds,
159        }
160    }
161}
162
163/// Generated client configuration result for a node.
164#[derive(Debug, Clone)]
165pub struct GenerateClientConfigurationResult {
166    /// Node that returned this result.
167    pub node: Option<String>,
168    /// PEM-encoded CA certificate.
169    pub ca: Vec<u8>,
170    /// PEM-encoded client certificate.
171    pub crt: Vec<u8>,
172    /// PEM-encoded client key.
173    pub key: Vec<u8>,
174    /// Talosconfig file content.
175    pub talosconfig: Vec<u8>,
176}
177
178impl From<ProtoGenerateClientConfig> for GenerateClientConfigurationResult {
179    fn from(proto: ProtoGenerateClientConfig) -> Self {
180        Self {
181            node: proto.metadata.map(|m| m.hostname),
182            ca: proto.ca,
183            crt: proto.crt,
184            key: proto.key,
185            talosconfig: proto.talosconfig,
186        }
187    }
188}
189
190impl GenerateClientConfigurationResult {
191    /// Get CA as string.
192    #[must_use]
193    pub fn ca_as_str(&self) -> Option<&str> {
194        std::str::from_utf8(&self.ca).ok()
195    }
196
197    /// Get certificate as string.
198    #[must_use]
199    pub fn crt_as_str(&self) -> Option<&str> {
200        std::str::from_utf8(&self.crt).ok()
201    }
202
203    /// Get key as string.
204    #[must_use]
205    pub fn key_as_str(&self) -> Option<&str> {
206        std::str::from_utf8(&self.key).ok()
207    }
208
209    /// Get talosconfig as string.
210    #[must_use]
211    pub fn talosconfig_as_str(&self) -> Option<&str> {
212        std::str::from_utf8(&self.talosconfig).ok()
213    }
214}
215
216/// Response from generating client configuration.
217#[derive(Debug, Clone)]
218pub struct GenerateClientConfigurationResponse {
219    /// Results from each node.
220    pub results: Vec<GenerateClientConfigurationResult>,
221}
222
223impl From<ProtoGenerateClientConfigResponse> for GenerateClientConfigurationResponse {
224    fn from(proto: ProtoGenerateClientConfigResponse) -> Self {
225        Self {
226            results: proto
227                .messages
228                .into_iter()
229                .map(GenerateClientConfigurationResult::from)
230                .collect(),
231        }
232    }
233}
234
235impl GenerateClientConfigurationResponse {
236    /// Get the first result.
237    #[must_use]
238    pub fn first(&self) -> Option<&GenerateClientConfigurationResult> {
239        self.results.first()
240    }
241}
242
243// =============================================================================
244// PacketCapture
245// =============================================================================
246
247/// Request to capture packets.
248#[derive(Debug, Clone)]
249pub struct PacketCaptureRequest {
250    /// Network interface to capture on.
251    pub interface: String,
252    /// Enable promiscuous mode.
253    pub promiscuous: bool,
254    /// Snap length in bytes.
255    pub snap_len: u32,
256}
257
258impl PacketCaptureRequest {
259    /// Create a new packet capture request.
260    #[must_use]
261    pub fn new(interface: impl Into<String>) -> Self {
262        Self {
263            interface: interface.into(),
264            promiscuous: false,
265            snap_len: 65535,
266        }
267    }
268
269    /// Create a builder.
270    #[must_use]
271    pub fn builder(interface: impl Into<String>) -> PacketCaptureRequestBuilder {
272        PacketCaptureRequestBuilder::new(interface)
273    }
274}
275
276impl From<PacketCaptureRequest> for ProtoPacketCaptureRequest {
277    fn from(req: PacketCaptureRequest) -> Self {
278        Self {
279            interface: req.interface,
280            promiscuous: req.promiscuous,
281            snap_len: req.snap_len,
282            bpf_filter: Vec::new(), // BPF filters not exposed for simplicity
283        }
284    }
285}
286
287/// Builder for `PacketCaptureRequest`.
288#[derive(Debug, Clone)]
289pub struct PacketCaptureRequestBuilder {
290    interface: String,
291    promiscuous: bool,
292    snap_len: u32,
293}
294
295impl PacketCaptureRequestBuilder {
296    /// Create a new builder.
297    #[must_use]
298    pub fn new(interface: impl Into<String>) -> Self {
299        Self {
300            interface: interface.into(),
301            promiscuous: false,
302            snap_len: 65535,
303        }
304    }
305
306    /// Enable promiscuous mode.
307    #[must_use]
308    pub fn promiscuous(mut self, enabled: bool) -> Self {
309        self.promiscuous = enabled;
310        self
311    }
312
313    /// Set snap length.
314    #[must_use]
315    pub fn snap_len(mut self, len: u32) -> Self {
316        self.snap_len = len;
317        self
318    }
319
320    /// Build the request.
321    #[must_use]
322    pub fn build(self) -> PacketCaptureRequest {
323        PacketCaptureRequest {
324            interface: self.interface,
325            promiscuous: self.promiscuous,
326            snap_len: self.snap_len,
327        }
328    }
329}
330
331/// Response from packet capture (streaming pcap data).
332#[derive(Debug, Clone, Default)]
333pub struct PacketCaptureResponse {
334    /// PCAP data.
335    pub data: Vec<u8>,
336    /// Node that returned this data.
337    pub node: Option<String>,
338}
339
340impl PacketCaptureResponse {
341    /// Create a new response.
342    #[must_use]
343    pub fn new(data: Vec<u8>, node: Option<String>) -> Self {
344        Self { data, node }
345    }
346
347    /// Get data length.
348    #[must_use]
349    pub fn len(&self) -> usize {
350        self.data.len()
351    }
352
353    /// Check if empty.
354    #[must_use]
355    pub fn is_empty(&self) -> bool {
356        self.data.is_empty()
357    }
358}
359
360// =============================================================================
361// Netstat
362// =============================================================================
363
364/// Netstat filter type.
365#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
366pub enum NetstatFilter {
367    /// All connections.
368    #[default]
369    All,
370    /// Connected sockets.
371    Connected,
372    /// Listening sockets.
373    Listening,
374}
375
376impl From<NetstatFilter> for i32 {
377    fn from(filter: NetstatFilter) -> Self {
378        match filter {
379            NetstatFilter::All => 0,
380            NetstatFilter::Connected => 1,
381            NetstatFilter::Listening => 2,
382        }
383    }
384}
385
386/// Layer 4 protocol filter.
387#[derive(Debug, Clone, Default)]
388pub struct L4ProtoFilter {
389    /// Include TCP.
390    pub tcp: bool,
391    /// Include TCP6.
392    pub tcp6: bool,
393    /// Include UDP.
394    pub udp: bool,
395    /// Include UDP6.
396    pub udp6: bool,
397}
398
399impl L4ProtoFilter {
400    /// Create filter for all protocols.
401    #[must_use]
402    pub fn all() -> Self {
403        Self {
404            tcp: true,
405            tcp6: true,
406            udp: true,
407            udp6: true,
408        }
409    }
410
411    /// Create filter for TCP only.
412    #[must_use]
413    pub fn tcp_only() -> Self {
414        Self {
415            tcp: true,
416            tcp6: true,
417            ..Default::default()
418        }
419    }
420
421    /// Create filter for UDP only.
422    #[must_use]
423    pub fn udp_only() -> Self {
424        Self {
425            udp: true,
426            udp6: true,
427            ..Default::default()
428        }
429    }
430}
431
432/// Request for netstat information.
433#[derive(Debug, Clone, Default)]
434pub struct NetstatRequest {
435    /// Filter type.
436    pub filter: NetstatFilter,
437    /// Include process information.
438    pub include_pid: bool,
439    /// Layer 4 protocol filter.
440    pub l4proto: Option<L4ProtoFilter>,
441    /// Include host network namespace.
442    pub host_network: bool,
443    /// Include all network namespaces.
444    pub all_netns: bool,
445}
446
447impl NetstatRequest {
448    /// Create a new netstat request.
449    #[must_use]
450    pub fn new() -> Self {
451        Self::default()
452    }
453
454    /// Create a request for listening sockets.
455    #[must_use]
456    pub fn listening() -> Self {
457        Self {
458            filter: NetstatFilter::Listening,
459            ..Default::default()
460        }
461    }
462
463    /// Create a request for connected sockets.
464    #[must_use]
465    pub fn connected() -> Self {
466        Self {
467            filter: NetstatFilter::Connected,
468            ..Default::default()
469        }
470    }
471
472    /// Create a builder.
473    #[must_use]
474    pub fn builder() -> NetstatRequestBuilder {
475        NetstatRequestBuilder::default()
476    }
477}
478
479impl From<NetstatRequest> for ProtoNetstatRequest {
480    fn from(req: NetstatRequest) -> Self {
481        use crate::api::generated::machine::netstat_request::{Feature, L4proto, NetNs};
482
483        Self {
484            filter: req.filter.into(),
485            feature: if req.include_pid {
486                Some(Feature { pid: true })
487            } else {
488                None
489            },
490            l4proto: req.l4proto.map(|l4| L4proto {
491                tcp: l4.tcp,
492                tcp6: l4.tcp6,
493                udp: l4.udp,
494                udp6: l4.udp6,
495                udplite: false,
496                udplite6: false,
497                raw: false,
498                raw6: false,
499            }),
500            netns: Some(NetNs {
501                hostnetwork: req.host_network,
502                netns: Vec::new(),
503                allnetns: req.all_netns,
504            }),
505        }
506    }
507}
508
509/// Builder for `NetstatRequest`.
510#[derive(Debug, Clone, Default)]
511pub struct NetstatRequestBuilder {
512    filter: NetstatFilter,
513    include_pid: bool,
514    l4proto: Option<L4ProtoFilter>,
515    host_network: bool,
516    all_netns: bool,
517}
518
519impl NetstatRequestBuilder {
520    /// Set filter.
521    #[must_use]
522    pub fn filter(mut self, filter: NetstatFilter) -> Self {
523        self.filter = filter;
524        self
525    }
526
527    /// Include process information.
528    #[must_use]
529    pub fn include_pid(mut self, include: bool) -> Self {
530        self.include_pid = include;
531        self
532    }
533
534    /// Set L4 protocol filter.
535    #[must_use]
536    pub fn l4proto(mut self, l4proto: L4ProtoFilter) -> Self {
537        self.l4proto = Some(l4proto);
538        self
539    }
540
541    /// Include host network namespace.
542    #[must_use]
543    pub fn host_network(mut self, include: bool) -> Self {
544        self.host_network = include;
545        self
546    }
547
548    /// Include all network namespaces.
549    #[must_use]
550    pub fn all_netns(mut self, include: bool) -> Self {
551        self.all_netns = include;
552        self
553    }
554
555    /// Build the request.
556    #[must_use]
557    pub fn build(self) -> NetstatRequest {
558        NetstatRequest {
559            filter: self.filter,
560            include_pid: self.include_pid,
561            l4proto: self.l4proto,
562            host_network: self.host_network,
563            all_netns: self.all_netns,
564        }
565    }
566}
567
568/// Connection state.
569#[derive(Debug, Clone, Copy, PartialEq, Eq)]
570pub enum ConnectionState {
571    /// Reserved/unknown.
572    Reserved,
573    /// Established connection.
574    Established,
575    /// SYN sent.
576    SynSent,
577    /// SYN received.
578    SynRecv,
579    /// FIN wait 1.
580    FinWait1,
581    /// FIN wait 2.
582    FinWait2,
583    /// Time wait.
584    TimeWait,
585    /// Close.
586    Close,
587    /// Close wait.
588    CloseWait,
589    /// Last ACK.
590    LastAck,
591    /// Listen.
592    Listen,
593    /// Closing.
594    Closing,
595}
596
597impl From<i32> for ConnectionState {
598    fn from(state: i32) -> Self {
599        match state {
600            1 => Self::Established,
601            2 => Self::SynSent,
602            3 => Self::SynRecv,
603            4 => Self::FinWait1,
604            5 => Self::FinWait2,
605            6 => Self::TimeWait,
606            7 => Self::Close,
607            8 => Self::CloseWait,
608            9 => Self::LastAck,
609            10 => Self::Listen,
610            11 => Self::Closing,
611            _ => Self::Reserved,
612        }
613    }
614}
615
616/// Connection record.
617#[derive(Debug, Clone)]
618pub struct ConnectionRecord {
619    /// Layer 4 protocol.
620    pub l4proto: String,
621    /// Local IP address.
622    pub local_ip: String,
623    /// Local port.
624    pub local_port: u32,
625    /// Remote IP address.
626    pub remote_ip: String,
627    /// Remote port.
628    pub remote_port: u32,
629    /// Connection state.
630    pub state: ConnectionState,
631    /// TX queue size.
632    pub tx_queue: u64,
633    /// RX queue size.
634    pub rx_queue: u64,
635    /// Process ID (if available).
636    pub pid: Option<u32>,
637    /// Process name (if available).
638    pub process_name: Option<String>,
639    /// Network namespace.
640    pub netns: String,
641}
642
643impl From<ProtoConnectRecord> for ConnectionRecord {
644    fn from(proto: ProtoConnectRecord) -> Self {
645        let (pid, process_name) = proto
646            .process
647            .map(|p| (Some(p.pid), Some(p.name)))
648            .unwrap_or((None, None));
649
650        Self {
651            l4proto: proto.l4proto,
652            local_ip: proto.localip,
653            local_port: proto.localport,
654            remote_ip: proto.remoteip,
655            remote_port: proto.remoteport,
656            state: ConnectionState::from(proto.state),
657            tx_queue: proto.txqueue,
658            rx_queue: proto.rxqueue,
659            pid,
660            process_name,
661            netns: proto.netns,
662        }
663    }
664}
665
666/// Netstat result for a node.
667#[derive(Debug, Clone)]
668pub struct NetstatResult {
669    /// Node that returned this result.
670    pub node: Option<String>,
671    /// Connection records.
672    pub connections: Vec<ConnectionRecord>,
673}
674
675impl From<ProtoNetstat> for NetstatResult {
676    fn from(proto: ProtoNetstat) -> Self {
677        Self {
678            node: proto.metadata.map(|m| m.hostname),
679            connections: proto
680                .connectrecord
681                .into_iter()
682                .map(ConnectionRecord::from)
683                .collect(),
684        }
685    }
686}
687
688/// Response from netstat request.
689#[derive(Debug, Clone)]
690pub struct NetstatResponse {
691    /// Results from each node.
692    pub results: Vec<NetstatResult>,
693}
694
695impl From<ProtoNetstatResponse> for NetstatResponse {
696    fn from(proto: ProtoNetstatResponse) -> Self {
697        Self {
698            results: proto
699                .messages
700                .into_iter()
701                .map(NetstatResult::from)
702                .collect(),
703        }
704    }
705}
706
707impl NetstatResponse {
708    /// Get the first result.
709    #[must_use]
710    pub fn first(&self) -> Option<&NetstatResult> {
711        self.results.first()
712    }
713
714    /// Get total number of connections.
715    #[must_use]
716    pub fn total_connections(&self) -> usize {
717        self.results.iter().map(|r| r.connections.len()).sum()
718    }
719
720    /// Get all listening connections.
721    #[must_use]
722    pub fn listening(&self) -> Vec<&ConnectionRecord> {
723        self.results
724            .iter()
725            .flat_map(|r| &r.connections)
726            .filter(|c| c.state == ConnectionState::Listen)
727            .collect()
728    }
729
730    /// Get all established connections.
731    #[must_use]
732    pub fn established(&self) -> Vec<&ConnectionRecord> {
733        self.results
734            .iter()
735            .flat_map(|r| &r.connections)
736            .filter(|c| c.state == ConnectionState::Established)
737            .collect()
738    }
739}
740
741#[cfg(test)]
742mod tests {
743    use super::*;
744
745    #[test]
746    fn test_rollback_response() {
747        let result = RollbackResult {
748            node: Some("node1".to_string()),
749        };
750        assert_eq!(result.node, Some("node1".to_string()));
751    }
752
753    #[test]
754    fn test_generate_client_config_request() {
755        let req = GenerateClientConfigurationRequest::new();
756        assert!(req.roles.is_empty());
757    }
758
759    #[test]
760    fn test_generate_client_config_builder() {
761        let req = GenerateClientConfigurationRequest::builder()
762            .role("os:admin")
763            .role("os:reader")
764            .crt_ttl_days(30)
765            .build();
766
767        assert_eq!(req.roles, vec!["os:admin", "os:reader"]);
768        assert_eq!(req.crt_ttl_seconds, Some(30 * 86400));
769    }
770
771    #[test]
772    fn test_packet_capture_request() {
773        let req = PacketCaptureRequest::new("eth0");
774        assert_eq!(req.interface, "eth0");
775        assert!(!req.promiscuous);
776        assert_eq!(req.snap_len, 65535);
777    }
778
779    #[test]
780    fn test_packet_capture_builder() {
781        let req = PacketCaptureRequest::builder("bond0")
782            .promiscuous(true)
783            .snap_len(1500)
784            .build();
785
786        assert_eq!(req.interface, "bond0");
787        assert!(req.promiscuous);
788        assert_eq!(req.snap_len, 1500);
789    }
790
791    #[test]
792    fn test_netstat_request() {
793        let req = NetstatRequest::listening();
794        assert_eq!(req.filter, NetstatFilter::Listening);
795    }
796
797    #[test]
798    fn test_netstat_builder() {
799        let req = NetstatRequest::builder()
800            .filter(NetstatFilter::Connected)
801            .include_pid(true)
802            .l4proto(L4ProtoFilter::tcp_only())
803            .host_network(true)
804            .build();
805
806        assert_eq!(req.filter, NetstatFilter::Connected);
807        assert!(req.include_pid);
808        assert!(req.l4proto.is_some());
809        assert!(req.host_network);
810    }
811
812    #[test]
813    fn test_connection_state() {
814        assert_eq!(ConnectionState::from(1), ConnectionState::Established);
815        assert_eq!(ConnectionState::from(10), ConnectionState::Listen);
816        assert_eq!(ConnectionState::from(999), ConnectionState::Reserved);
817    }
818
819    #[test]
820    fn test_l4proto_filter() {
821        let all = L4ProtoFilter::all();
822        assert!(all.tcp && all.tcp6 && all.udp && all.udp6);
823
824        let tcp = L4ProtoFilter::tcp_only();
825        assert!(tcp.tcp && tcp.tcp6 && !tcp.udp && !tcp.udp6);
826    }
827}