Skip to main content

talos_api_rs/resources/
system.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Typed wrappers for System Information APIs.
4//!
5//! Provides access to system metrics like CPU, memory, disk, and network stats.
6
7use crate::api::generated::machine::{
8    CpUsInfo as ProtoCpUsInfo, CpuInfo as ProtoCpuInfo, CpuInfoResponse as ProtoCpuInfoResponse,
9    DiskStat as ProtoDiskStat, DiskStats as ProtoDiskStats,
10    DiskStatsResponse as ProtoDiskStatsResponse, LoadAvg as ProtoLoadAvg,
11    LoadAvgResponse as ProtoLoadAvgResponse, Memory as ProtoMemory,
12    MemoryResponse as ProtoMemoryResponse, MountStat as ProtoMountStat,
13    MountsResponse as ProtoMountsResponse, NetDev as ProtoNetDev,
14    NetworkDeviceStats as ProtoNetworkDeviceStats,
15    NetworkDeviceStatsResponse as ProtoNetworkDeviceStatsResponse, Process as ProtoProcess,
16    ProcessInfo as ProtoProcessInfo, ProcessesResponse as ProtoProcessesResponse,
17};
18
19// =============================================================================
20// LoadAvg
21// =============================================================================
22
23/// System load averages.
24#[derive(Debug, Clone)]
25pub struct LoadAvgResult {
26    /// Node that returned this result.
27    pub node: Option<String>,
28    /// 1-minute load average.
29    pub load1: f64,
30    /// 5-minute load average.
31    pub load5: f64,
32    /// 15-minute load average.
33    pub load15: f64,
34}
35
36impl From<ProtoLoadAvg> for LoadAvgResult {
37    fn from(proto: ProtoLoadAvg) -> Self {
38        Self {
39            node: proto.metadata.map(|m| m.hostname),
40            load1: proto.load1,
41            load5: proto.load5,
42            load15: proto.load15,
43        }
44    }
45}
46
47/// Response from load average request.
48#[derive(Debug, Clone)]
49pub struct LoadAvgResponse {
50    /// Results from each node.
51    pub results: Vec<LoadAvgResult>,
52}
53
54impl From<ProtoLoadAvgResponse> for LoadAvgResponse {
55    fn from(proto: ProtoLoadAvgResponse) -> Self {
56        Self {
57            results: proto
58                .messages
59                .into_iter()
60                .map(LoadAvgResult::from)
61                .collect(),
62        }
63    }
64}
65
66impl LoadAvgResponse {
67    /// Get the first result.
68    #[must_use]
69    pub fn first(&self) -> Option<&LoadAvgResult> {
70        self.results.first()
71    }
72}
73
74// =============================================================================
75// Memory
76// =============================================================================
77
78/// Memory information for a node.
79#[derive(Debug, Clone)]
80pub struct MemoryResult {
81    /// Node that returned this result.
82    pub node: Option<String>,
83    /// Total memory in bytes.
84    pub mem_total: u64,
85    /// Free memory in bytes.
86    pub mem_free: u64,
87    /// Available memory in bytes.
88    pub mem_available: u64,
89    /// Buffer memory in bytes.
90    pub buffers: u64,
91    /// Cached memory in bytes.
92    pub cached: u64,
93    /// Swap total in bytes.
94    pub swap_total: u64,
95    /// Swap free in bytes.
96    pub swap_free: u64,
97}
98
99impl From<ProtoMemory> for MemoryResult {
100    fn from(proto: ProtoMemory) -> Self {
101        let meminfo = proto.meminfo.unwrap_or_default();
102        Self {
103            node: proto.metadata.map(|m| m.hostname),
104            mem_total: meminfo.memtotal,
105            mem_free: meminfo.memfree,
106            mem_available: meminfo.memavailable,
107            buffers: meminfo.buffers,
108            cached: meminfo.cached,
109            swap_total: meminfo.swaptotal,
110            swap_free: meminfo.swapfree,
111        }
112    }
113}
114
115impl MemoryResult {
116    /// Get total memory in bytes.
117    #[must_use]
118    pub fn total(&self) -> u64 {
119        self.mem_total
120    }
121
122    /// Get free memory in bytes.
123    #[must_use]
124    pub fn free(&self) -> u64 {
125        self.mem_free
126    }
127
128    /// Get available memory in bytes.
129    #[must_use]
130    pub fn available(&self) -> u64 {
131        self.mem_available
132    }
133
134    /// Get used memory in bytes.
135    #[must_use]
136    pub fn used(&self) -> u64 {
137        self.mem_total.saturating_sub(self.mem_available)
138    }
139
140    /// Get memory usage percentage.
141    #[must_use]
142    pub fn usage_percent(&self) -> f64 {
143        if self.mem_total == 0 {
144            0.0
145        } else {
146            (self.used() as f64 / self.mem_total as f64) * 100.0
147        }
148    }
149}
150
151/// Response from memory request.
152#[derive(Debug, Clone)]
153pub struct MemoryResponse {
154    /// Results from each node.
155    pub results: Vec<MemoryResult>,
156}
157
158impl From<ProtoMemoryResponse> for MemoryResponse {
159    fn from(proto: ProtoMemoryResponse) -> Self {
160        Self {
161            results: proto.messages.into_iter().map(MemoryResult::from).collect(),
162        }
163    }
164}
165
166impl MemoryResponse {
167    /// Get the first result.
168    #[must_use]
169    pub fn first(&self) -> Option<&MemoryResult> {
170        self.results.first()
171    }
172}
173
174// =============================================================================
175// CPUInfo
176// =============================================================================
177
178/// Information about a single CPU.
179#[derive(Debug, Clone)]
180pub struct CpuInfo {
181    /// Processor number.
182    pub processor: u32,
183    /// Vendor ID.
184    pub vendor_id: String,
185    /// Model name.
186    pub model_name: String,
187    /// CPU MHz.
188    pub cpu_mhz: f64,
189    /// Number of cores.
190    pub cpu_cores: u32,
191    /// CPU flags.
192    pub flags: Vec<String>,
193}
194
195impl From<ProtoCpuInfo> for CpuInfo {
196    fn from(proto: ProtoCpuInfo) -> Self {
197        Self {
198            processor: proto.processor,
199            vendor_id: proto.vendor_id,
200            model_name: proto.model_name,
201            cpu_mhz: proto.cpu_mhz,
202            cpu_cores: proto.cpu_cores,
203            flags: proto.flags,
204        }
205    }
206}
207
208/// CPU information result for a node.
209#[derive(Debug, Clone)]
210pub struct CpuInfoResult {
211    /// Node that returned this result.
212    pub node: Option<String>,
213    /// CPU information.
214    pub cpus: Vec<CpuInfo>,
215}
216
217impl From<ProtoCpUsInfo> for CpuInfoResult {
218    fn from(proto: ProtoCpUsInfo) -> Self {
219        Self {
220            node: proto.metadata.map(|m| m.hostname),
221            cpus: proto.cpu_info.into_iter().map(CpuInfo::from).collect(),
222        }
223    }
224}
225
226/// Response from CPU info request.
227#[derive(Debug, Clone)]
228pub struct CpuInfoResponse {
229    /// Results from each node.
230    pub results: Vec<CpuInfoResult>,
231}
232
233impl From<ProtoCpuInfoResponse> for CpuInfoResponse {
234    fn from(proto: ProtoCpuInfoResponse) -> Self {
235        Self {
236            results: proto
237                .messages
238                .into_iter()
239                .map(CpuInfoResult::from)
240                .collect(),
241        }
242    }
243}
244
245impl CpuInfoResponse {
246    /// Get the first result.
247    #[must_use]
248    pub fn first(&self) -> Option<&CpuInfoResult> {
249        self.results.first()
250    }
251
252    /// Get total number of CPUs across all results.
253    #[must_use]
254    pub fn total_cpus(&self) -> usize {
255        self.results.iter().map(|r| r.cpus.len()).sum()
256    }
257}
258
259// =============================================================================
260// DiskStats
261// =============================================================================
262
263/// Statistics for a single disk.
264#[derive(Debug, Clone)]
265pub struct DiskStat {
266    /// Device name.
267    pub name: String,
268    /// Reads completed.
269    pub read_completed: u64,
270    /// Sectors read.
271    pub read_sectors: u64,
272    /// Read time in ms.
273    pub read_time_ms: u64,
274    /// Writes completed.
275    pub write_completed: u64,
276    /// Sectors written.
277    pub write_sectors: u64,
278    /// Write time in ms.
279    pub write_time_ms: u64,
280    /// I/O operations in progress.
281    pub io_in_progress: u64,
282    /// I/O time in ms.
283    pub io_time_ms: u64,
284}
285
286impl From<ProtoDiskStat> for DiskStat {
287    fn from(proto: ProtoDiskStat) -> Self {
288        Self {
289            name: proto.name,
290            read_completed: proto.read_completed,
291            read_sectors: proto.read_sectors,
292            read_time_ms: proto.read_time_ms,
293            write_completed: proto.write_completed,
294            write_sectors: proto.write_sectors,
295            write_time_ms: proto.write_time_ms,
296            io_in_progress: proto.io_in_progress,
297            io_time_ms: proto.io_time_ms,
298        }
299    }
300}
301
302/// Disk statistics result for a node.
303#[derive(Debug, Clone)]
304pub struct DiskStatsResult {
305    /// Node that returned this result.
306    pub node: Option<String>,
307    /// Total stats across all devices.
308    pub total: Option<DiskStat>,
309    /// Per-device stats.
310    pub devices: Vec<DiskStat>,
311}
312
313impl From<ProtoDiskStats> for DiskStatsResult {
314    fn from(proto: ProtoDiskStats) -> Self {
315        Self {
316            node: proto.metadata.map(|m| m.hostname),
317            total: proto.total.map(DiskStat::from),
318            devices: proto.devices.into_iter().map(DiskStat::from).collect(),
319        }
320    }
321}
322
323/// Response from disk stats request.
324#[derive(Debug, Clone)]
325pub struct DiskStatsResponse {
326    /// Results from each node.
327    pub results: Vec<DiskStatsResult>,
328}
329
330impl From<ProtoDiskStatsResponse> for DiskStatsResponse {
331    fn from(proto: ProtoDiskStatsResponse) -> Self {
332        Self {
333            results: proto
334                .messages
335                .into_iter()
336                .map(DiskStatsResult::from)
337                .collect(),
338        }
339    }
340}
341
342impl DiskStatsResponse {
343    /// Get the first result.
344    #[must_use]
345    pub fn first(&self) -> Option<&DiskStatsResult> {
346        self.results.first()
347    }
348}
349
350// =============================================================================
351// NetworkDeviceStats
352// =============================================================================
353
354/// Statistics for a network device.
355#[derive(Debug, Clone)]
356pub struct NetDevStat {
357    /// Device name.
358    pub name: String,
359    /// Bytes received.
360    pub rx_bytes: u64,
361    /// Packets received.
362    pub rx_packets: u64,
363    /// Receive errors.
364    pub rx_errors: u64,
365    /// Bytes transmitted.
366    pub tx_bytes: u64,
367    /// Packets transmitted.
368    pub tx_packets: u64,
369    /// Transmit errors.
370    pub tx_errors: u64,
371}
372
373impl From<ProtoNetDev> for NetDevStat {
374    fn from(proto: ProtoNetDev) -> Self {
375        Self {
376            name: proto.name,
377            rx_bytes: proto.rx_bytes,
378            rx_packets: proto.rx_packets,
379            rx_errors: proto.rx_errors,
380            tx_bytes: proto.tx_bytes,
381            tx_packets: proto.tx_packets,
382            tx_errors: proto.tx_errors,
383        }
384    }
385}
386
387/// Network stats result for a node.
388#[derive(Debug, Clone)]
389pub struct NetworkDeviceStatsResult {
390    /// Node that returned this result.
391    pub node: Option<String>,
392    /// Total stats across all devices.
393    pub total: Option<NetDevStat>,
394    /// Per-device stats.
395    pub devices: Vec<NetDevStat>,
396}
397
398impl From<ProtoNetworkDeviceStats> for NetworkDeviceStatsResult {
399    fn from(proto: ProtoNetworkDeviceStats) -> Self {
400        Self {
401            node: proto.metadata.map(|m| m.hostname),
402            total: proto.total.map(NetDevStat::from),
403            devices: proto.devices.into_iter().map(NetDevStat::from).collect(),
404        }
405    }
406}
407
408/// Response from network device stats request.
409#[derive(Debug, Clone)]
410pub struct NetworkDeviceStatsResponse {
411    /// Results from each node.
412    pub results: Vec<NetworkDeviceStatsResult>,
413}
414
415impl From<ProtoNetworkDeviceStatsResponse> for NetworkDeviceStatsResponse {
416    fn from(proto: ProtoNetworkDeviceStatsResponse) -> Self {
417        Self {
418            results: proto
419                .messages
420                .into_iter()
421                .map(NetworkDeviceStatsResult::from)
422                .collect(),
423        }
424    }
425}
426
427impl NetworkDeviceStatsResponse {
428    /// Get the first result.
429    #[must_use]
430    pub fn first(&self) -> Option<&NetworkDeviceStatsResult> {
431        self.results.first()
432    }
433}
434
435// =============================================================================
436// Mounts
437// =============================================================================
438
439/// Mount point information.
440#[derive(Debug, Clone)]
441pub struct MountStat {
442    /// Filesystem type.
443    pub filesystem: String,
444    /// Total size in bytes.
445    pub size: u64,
446    /// Available space in bytes.
447    pub available: u64,
448    /// Mount point path.
449    pub mounted_on: String,
450}
451
452impl From<ProtoMountStat> for MountStat {
453    fn from(proto: ProtoMountStat) -> Self {
454        Self {
455            filesystem: proto.filesystem,
456            size: proto.size,
457            available: proto.available,
458            mounted_on: proto.mounted_on,
459        }
460    }
461}
462
463impl MountStat {
464    /// Get used space in bytes.
465    #[must_use]
466    pub fn used(&self) -> u64 {
467        self.size.saturating_sub(self.available)
468    }
469
470    /// Get usage percentage.
471    #[must_use]
472    pub fn usage_percent(&self) -> f64 {
473        if self.size == 0 {
474            0.0
475        } else {
476            (self.used() as f64 / self.size as f64) * 100.0
477        }
478    }
479}
480
481/// Mounts result for a node.
482#[derive(Debug, Clone)]
483pub struct MountsResult {
484    /// Node that returned this result.
485    pub node: Option<String>,
486    /// Mount points.
487    pub stats: Vec<MountStat>,
488}
489
490/// Response from mounts request.
491#[derive(Debug, Clone)]
492pub struct MountsResponse {
493    /// Results from each node.
494    pub results: Vec<MountsResult>,
495}
496
497impl From<ProtoMountsResponse> for MountsResponse {
498    fn from(proto: ProtoMountsResponse) -> Self {
499        Self {
500            results: proto
501                .messages
502                .into_iter()
503                .map(|m| MountsResult {
504                    node: m.metadata.map(|meta| meta.hostname),
505                    stats: m.stats.into_iter().map(MountStat::from).collect(),
506                })
507                .collect(),
508        }
509    }
510}
511
512impl MountsResponse {
513    /// Get the first result.
514    #[must_use]
515    pub fn first(&self) -> Option<&MountsResult> {
516        self.results.first()
517    }
518}
519
520// =============================================================================
521// Processes
522// =============================================================================
523
524/// Information about a process.
525#[derive(Debug, Clone)]
526pub struct ProcessInfo {
527    /// Process ID.
528    pub pid: i32,
529    /// Parent process ID.
530    pub ppid: i32,
531    /// Process state.
532    pub state: String,
533    /// Number of threads.
534    pub threads: i32,
535    /// CPU time.
536    pub cpu_time: f64,
537    /// Virtual memory size.
538    pub virtual_memory: u64,
539    /// Resident memory size.
540    pub resident_memory: u64,
541    /// Command name.
542    pub command: String,
543    /// Executable path.
544    pub executable: String,
545    /// Command line arguments.
546    pub args: String,
547}
548
549impl From<ProtoProcessInfo> for ProcessInfo {
550    fn from(proto: ProtoProcessInfo) -> Self {
551        Self {
552            pid: proto.pid,
553            ppid: proto.ppid,
554            state: proto.state,
555            threads: proto.threads,
556            cpu_time: proto.cpu_time,
557            virtual_memory: proto.virtual_memory,
558            resident_memory: proto.resident_memory,
559            command: proto.command,
560            executable: proto.executable,
561            args: proto.args,
562        }
563    }
564}
565
566/// Processes result for a node.
567#[derive(Debug, Clone)]
568pub struct ProcessesResult {
569    /// Node that returned this result.
570    pub node: Option<String>,
571    /// List of processes.
572    pub processes: Vec<ProcessInfo>,
573}
574
575impl From<ProtoProcess> for ProcessesResult {
576    fn from(proto: ProtoProcess) -> Self {
577        Self {
578            node: proto.metadata.map(|m| m.hostname),
579            processes: proto.processes.into_iter().map(ProcessInfo::from).collect(),
580        }
581    }
582}
583
584/// Response from processes request.
585#[derive(Debug, Clone)]
586pub struct ProcessesResponse {
587    /// Results from each node.
588    pub results: Vec<ProcessesResult>,
589}
590
591impl From<ProtoProcessesResponse> for ProcessesResponse {
592    fn from(proto: ProtoProcessesResponse) -> Self {
593        Self {
594            results: proto
595                .messages
596                .into_iter()
597                .map(ProcessesResult::from)
598                .collect(),
599        }
600    }
601}
602
603impl ProcessesResponse {
604    /// Get the first result.
605    #[must_use]
606    pub fn first(&self) -> Option<&ProcessesResult> {
607        self.results.first()
608    }
609
610    /// Get total number of processes across all results.
611    #[must_use]
612    pub fn total_processes(&self) -> usize {
613        self.results.iter().map(|r| r.processes.len()).sum()
614    }
615}
616
617#[cfg(test)]
618mod tests {
619    use super::*;
620
621    #[test]
622    fn test_load_avg_result() {
623        let result = LoadAvgResult {
624            node: Some("node1".to_string()),
625            load1: 0.5,
626            load5: 0.7,
627            load15: 0.9,
628        };
629        assert_eq!(result.load1, 0.5);
630    }
631
632    #[test]
633    fn test_memory_result() {
634        let result = MemoryResult {
635            node: Some("node1".to_string()),
636            mem_total: 16_000_000_000,
637            mem_free: 4_000_000_000,
638            mem_available: 8_000_000_000,
639            buffers: 100_000_000,
640            cached: 2_000_000_000,
641            swap_total: 1_000_000_000,
642            swap_free: 500_000_000,
643        };
644
645        assert_eq!(result.total(), 16_000_000_000);
646        assert_eq!(result.available(), 8_000_000_000);
647        assert_eq!(result.used(), 8_000_000_000);
648        assert!((result.usage_percent() - 50.0).abs() < 0.01);
649    }
650
651    #[test]
652    fn test_mount_stat() {
653        let stat = MountStat {
654            filesystem: "ext4".to_string(),
655            size: 100_000_000_000,
656            available: 40_000_000_000,
657            mounted_on: "/".to_string(),
658        };
659
660        assert_eq!(stat.used(), 60_000_000_000);
661        assert!((stat.usage_percent() - 60.0).abs() < 0.01);
662    }
663
664    #[test]
665    fn test_cpu_info() {
666        let cpu = CpuInfo {
667            processor: 0,
668            vendor_id: "GenuineIntel".to_string(),
669            model_name: "Intel Core i7".to_string(),
670            cpu_mhz: 3200.0,
671            cpu_cores: 4,
672            flags: vec!["avx".to_string(), "sse".to_string()],
673        };
674
675        assert_eq!(cpu.processor, 0);
676        assert_eq!(cpu.cpu_cores, 4);
677    }
678
679    #[test]
680    fn test_disk_stat() {
681        let stat = DiskStat {
682            name: "sda".to_string(),
683            read_completed: 1000,
684            read_sectors: 50000,
685            read_time_ms: 500,
686            write_completed: 500,
687            write_sectors: 25000,
688            write_time_ms: 250,
689            io_in_progress: 2,
690            io_time_ms: 750,
691        };
692
693        assert_eq!(stat.name, "sda");
694        assert_eq!(stat.read_completed, 1000);
695    }
696
697    #[test]
698    fn test_net_dev_stat() {
699        let stat = NetDevStat {
700            name: "eth0".to_string(),
701            rx_bytes: 1_000_000,
702            rx_packets: 1000,
703            rx_errors: 0,
704            tx_bytes: 500_000,
705            tx_packets: 500,
706            tx_errors: 0,
707        };
708
709        assert_eq!(stat.name, "eth0");
710        assert_eq!(stat.rx_bytes, 1_000_000);
711    }
712
713    #[test]
714    fn test_process_info() {
715        let proc = ProcessInfo {
716            pid: 1,
717            ppid: 0,
718            state: "S".to_string(),
719            threads: 1,
720            cpu_time: 10.5,
721            virtual_memory: 1_000_000,
722            resident_memory: 500_000,
723            command: "init".to_string(),
724            executable: "/sbin/init".to_string(),
725            args: "".to_string(),
726        };
727
728        assert_eq!(proc.pid, 1);
729        assert_eq!(proc.command, "init");
730    }
731}