1use serde::{Deserialize, Serialize};
6use validator::Validate;
7
8#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
10pub struct ComputeDomain {
11 #[serde(skip_serializing_if = "Option::is_none")]
14 pub cpu: Option<CpuInfo>,
15
16 #[serde(skip_serializing_if = "Option::is_none")]
19 pub memory: Option<MemoryInfo>,
20
21 #[serde(skip_serializing_if = "Option::is_none")]
24 pub gpu: Option<Vec<GpuInfo>>,
25
26 #[serde(skip_serializing_if = "Option::is_none")]
29 pub storage: Option<Vec<StorageInfo>>,
30
31 #[serde(skip_serializing_if = "Option::is_none")]
34 pub processes: Option<ProcessInfo>,
35
36 #[serde(skip_serializing_if = "Option::is_none")]
39 pub ros: Option<RosStatus>,
40
41 #[serde(skip_serializing_if = "Option::is_none")]
44 pub uptime_sec: Option<f64>,
45
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub load_average: Option<[f64; 3]>,
49
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub kernel_version: Option<String>,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub os_version: Option<String>,
57}
58
59#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
61pub struct CpuInfo {
62 #[serde(skip_serializing_if = "Option::is_none")]
64 #[validate(range(min = 0.0, max = 100.0))]
65 pub usage_pct: Option<f64>,
66
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub per_core_pct: Option<Vec<f64>>,
70
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub core_count: Option<u32>,
74
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub temperature_c: Option<f64>,
78
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub frequency_mhz: Option<f64>,
82
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub model: Option<String>,
86
87 #[serde(skip_serializing_if = "Option::is_none")]
89 pub user_pct: Option<f64>,
90
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub system_pct: Option<f64>,
94
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub idle_pct: Option<f64>,
98
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub iowait_pct: Option<f64>,
102
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub is_throttling: Option<bool>,
106}
107
108#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
110pub struct MemoryInfo {
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub total_bytes: Option<u64>,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub used_bytes: Option<u64>,
118
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub available_bytes: Option<u64>,
122
123 #[serde(skip_serializing_if = "Option::is_none")]
125 #[validate(range(min = 0.0, max = 100.0))]
126 pub usage_pct: Option<f64>,
127
128 #[serde(skip_serializing_if = "Option::is_none")]
130 pub swap_total_bytes: Option<u64>,
131
132 #[serde(skip_serializing_if = "Option::is_none")]
134 pub swap_used_bytes: Option<u64>,
135
136 #[serde(skip_serializing_if = "Option::is_none")]
138 pub cached_bytes: Option<u64>,
139
140 #[serde(skip_serializing_if = "Option::is_none")]
142 pub buffers_bytes: Option<u64>,
143}
144
145impl MemoryInfo {
146 pub fn new(total_bytes: u64, used_bytes: u64) -> Self {
148 let usage_pct = if total_bytes > 0 {
149 (used_bytes as f64 / total_bytes as f64) * 100.0
150 } else {
151 0.0
152 };
153 Self {
154 total_bytes: Some(total_bytes),
155 used_bytes: Some(used_bytes),
156 available_bytes: Some(total_bytes.saturating_sub(used_bytes)),
157 usage_pct: Some(usage_pct),
158 ..Default::default()
159 }
160 }
161}
162
163#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
165pub struct GpuInfo {
166 #[serde(skip_serializing_if = "Option::is_none")]
168 pub index: Option<u32>,
169
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub name: Option<String>,
173
174 #[serde(skip_serializing_if = "Option::is_none")]
176 #[validate(range(min = 0.0, max = 100.0))]
177 pub utilization_pct: Option<f64>,
178
179 #[serde(skip_serializing_if = "Option::is_none")]
181 pub memory_total_bytes: Option<u64>,
182
183 #[serde(skip_serializing_if = "Option::is_none")]
185 pub memory_used_bytes: Option<u64>,
186
187 #[serde(skip_serializing_if = "Option::is_none")]
189 #[validate(range(min = 0.0, max = 100.0))]
190 pub memory_usage_pct: Option<f64>,
191
192 #[serde(skip_serializing_if = "Option::is_none")]
194 pub temperature_c: Option<f64>,
195
196 #[serde(skip_serializing_if = "Option::is_none")]
198 pub power_w: Option<f64>,
199
200 #[serde(skip_serializing_if = "Option::is_none")]
202 pub driver_version: Option<String>,
203
204 #[serde(skip_serializing_if = "Option::is_none")]
206 pub cuda_version: Option<String>,
207}
208
209#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
211pub struct StorageInfo {
212 #[serde(skip_serializing_if = "Option::is_none")]
214 pub mount_point: Option<String>,
215
216 #[serde(skip_serializing_if = "Option::is_none")]
218 pub device: Option<String>,
219
220 #[serde(skip_serializing_if = "Option::is_none")]
222 pub fs_type: Option<String>,
223
224 #[serde(skip_serializing_if = "Option::is_none")]
226 pub total_bytes: Option<u64>,
227
228 #[serde(skip_serializing_if = "Option::is_none")]
230 pub used_bytes: Option<u64>,
231
232 #[serde(skip_serializing_if = "Option::is_none")]
234 pub available_bytes: Option<u64>,
235
236 #[serde(skip_serializing_if = "Option::is_none")]
238 #[validate(range(min = 0.0, max = 100.0))]
239 pub usage_pct: Option<f64>,
240
241 #[serde(skip_serializing_if = "Option::is_none")]
243 pub read_bytes_sec: Option<u64>,
244
245 #[serde(skip_serializing_if = "Option::is_none")]
247 pub write_bytes_sec: Option<u64>,
248}
249
250#[derive(Debug, Clone, Default, Serialize, Deserialize)]
252pub struct ProcessInfo {
253 #[serde(skip_serializing_if = "Option::is_none")]
255 pub total_count: Option<u32>,
256
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub running_count: Option<u32>,
260
261 #[serde(skip_serializing_if = "Option::is_none")]
263 pub top_cpu: Option<Vec<ProcessEntry>>,
264
265 #[serde(skip_serializing_if = "Option::is_none")]
267 pub top_memory: Option<Vec<ProcessEntry>>,
268}
269
270#[derive(Debug, Clone, Default, Serialize, Deserialize)]
272pub struct ProcessEntry {
273 #[serde(skip_serializing_if = "Option::is_none")]
275 pub name: Option<String>,
276
277 #[serde(skip_serializing_if = "Option::is_none")]
279 pub pid: Option<u32>,
280
281 #[serde(skip_serializing_if = "Option::is_none")]
283 pub cpu_pct: Option<f64>,
284
285 #[serde(skip_serializing_if = "Option::is_none")]
287 pub memory_bytes: Option<u64>,
288}
289
290#[derive(Debug, Clone, Default, Serialize, Deserialize)]
292pub struct RosStatus {
293 #[serde(skip_serializing_if = "Option::is_none")]
295 pub ros_version: Option<u8>,
296
297 #[serde(skip_serializing_if = "Option::is_none")]
299 pub distribution: Option<String>,
300
301 #[serde(skip_serializing_if = "Option::is_none")]
303 pub is_running: Option<bool>,
304
305 #[serde(skip_serializing_if = "Option::is_none")]
307 pub node_count: Option<u32>,
308
309 #[serde(skip_serializing_if = "Option::is_none")]
311 pub topic_count: Option<u32>,
312
313 #[serde(skip_serializing_if = "Option::is_none")]
315 pub service_count: Option<u32>,
316
317 #[serde(skip_serializing_if = "Option::is_none")]
319 pub nodes: Option<Vec<RosNode>>,
320
321 #[serde(skip_serializing_if = "Option::is_none")]
323 pub dds_middleware: Option<String>,
324
325 #[serde(skip_serializing_if = "Option::is_none")]
327 pub domain_id: Option<u32>,
328}
329
330#[derive(Debug, Clone, Default, Serialize, Deserialize)]
332pub struct RosNode {
333 #[serde(skip_serializing_if = "Option::is_none")]
335 pub name: Option<String>,
336
337 #[serde(skip_serializing_if = "Option::is_none")]
339 pub namespace: Option<String>,
340
341 #[serde(skip_serializing_if = "Option::is_none")]
343 pub is_active: Option<bool>,
344
345 #[serde(skip_serializing_if = "Option::is_none")]
347 pub cpu_pct: Option<f64>,
348
349 #[serde(skip_serializing_if = "Option::is_none")]
351 pub memory_bytes: Option<u64>,
352
353 #[serde(skip_serializing_if = "Option::is_none")]
355 pub pub_count: Option<u32>,
356
357 #[serde(skip_serializing_if = "Option::is_none")]
359 pub sub_count: Option<u32>,
360}
361
362impl ComputeDomain {
363 pub fn with_cpu(cpu: CpuInfo) -> Self {
365 Self {
366 cpu: Some(cpu),
367 ..Default::default()
368 }
369 }
370
371 pub fn with_memory(mut self, memory: MemoryInfo) -> Self {
373 self.memory = Some(memory);
374 self
375 }
376
377 pub fn with_gpu(mut self, gpu: GpuInfo) -> Self {
379 let gpus = self.gpu.get_or_insert_with(Vec::new);
380 gpus.push(gpu);
381 self
382 }
383
384 pub fn with_storage(mut self, storage: StorageInfo) -> Self {
386 let storages = self.storage.get_or_insert_with(Vec::new);
387 storages.push(storage);
388 self
389 }
390
391 pub fn with_ros(mut self, ros: RosStatus) -> Self {
393 self.ros = Some(ros);
394 self
395 }
396
397 pub fn with_load_average(mut self, load: [f64; 3]) -> Self {
399 self.load_average = Some(load);
400 self
401 }
402}
403
404impl CpuInfo {
405 pub fn new(usage_pct: f64, core_count: u32) -> Self {
407 Self {
408 usage_pct: Some(usage_pct),
409 core_count: Some(core_count),
410 ..Default::default()
411 }
412 }
413
414 pub fn with_temperature(mut self, temp_c: f64) -> Self {
416 self.temperature_c = Some(temp_c);
417 self
418 }
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424
425 #[test]
426 fn test_memory_info() {
427 let mem = MemoryInfo::new(8_000_000_000, 4_000_000_000);
428 assert_eq!(mem.usage_pct, Some(50.0));
429 assert_eq!(mem.available_bytes, Some(4_000_000_000));
430 }
431
432 #[test]
433 fn test_compute_domain() {
434 let compute = ComputeDomain::with_cpu(CpuInfo::new(45.0, 8))
435 .with_memory(MemoryInfo::new(16_000_000_000, 8_000_000_000))
436 .with_load_average([1.5, 2.0, 1.8]);
437
438 assert_eq!(compute.cpu.as_ref().unwrap().usage_pct, Some(45.0));
439 assert!(compute.load_average.is_some());
440 }
441
442 #[test]
447 fn test_compute_domain_default() {
448 let compute = ComputeDomain::default();
449 assert!(compute.cpu.is_none());
450 assert!(compute.memory.is_none());
451 assert!(compute.gpu.is_none());
452 assert!(compute.storage.is_none());
453 assert!(compute.processes.is_none());
454 assert!(compute.ros.is_none());
455 assert!(compute.uptime_sec.is_none());
456 assert!(compute.load_average.is_none());
457 }
458
459 #[test]
460 fn test_compute_domain_with_cpu() {
461 let cpu = CpuInfo::new(75.0, 4);
462 let compute = ComputeDomain::with_cpu(cpu);
463
464 assert!(compute.cpu.is_some());
465 assert_eq!(compute.cpu.as_ref().unwrap().core_count, Some(4));
466 }
467
468 #[test]
469 fn test_compute_domain_with_memory() {
470 let mem = MemoryInfo::new(4_000_000_000, 2_000_000_000);
471 let compute = ComputeDomain::default().with_memory(mem);
472
473 assert!(compute.memory.is_some());
474 assert_eq!(compute.memory.as_ref().unwrap().usage_pct, Some(50.0));
475 }
476
477 #[test]
478 fn test_compute_domain_with_gpu() {
479 let gpu = GpuInfo {
480 index: Some(0),
481 name: Some("RTX 3080".to_string()),
482 utilization_pct: Some(80.0),
483 ..Default::default()
484 };
485
486 let compute = ComputeDomain::default().with_gpu(gpu);
487 assert!(compute.gpu.is_some());
488 assert_eq!(compute.gpu.as_ref().unwrap().len(), 1);
489 }
490
491 #[test]
492 fn test_compute_domain_with_storage() {
493 let storage = StorageInfo {
494 mount_point: Some("/".to_string()),
495 total_bytes: Some(500_000_000_000),
496 used_bytes: Some(250_000_000_000),
497 ..Default::default()
498 };
499
500 let compute = ComputeDomain::default().with_storage(storage);
501 assert!(compute.storage.is_some());
502 }
503
504 #[test]
505 fn test_compute_domain_with_ros() {
506 let ros = RosStatus {
507 ros_version: Some(2),
508 distribution: Some("humble".to_string()),
509 ..Default::default()
510 };
511
512 let compute = ComputeDomain::default().with_ros(ros);
513 assert!(compute.ros.is_some());
514 }
515
516 #[test]
517 fn test_compute_domain_chained_builders() {
518 let compute = ComputeDomain::with_cpu(CpuInfo::new(50.0, 8))
519 .with_memory(MemoryInfo::new(16_000_000_000, 8_000_000_000))
520 .with_gpu(GpuInfo::default())
521 .with_storage(StorageInfo::default())
522 .with_ros(RosStatus::default())
523 .with_load_average([1.0, 1.5, 2.0]);
524
525 assert!(compute.cpu.is_some());
526 assert!(compute.memory.is_some());
527 assert!(compute.gpu.is_some());
528 assert!(compute.storage.is_some());
529 assert!(compute.ros.is_some());
530 assert!(compute.load_average.is_some());
531 }
532
533 #[test]
538 fn test_cpu_info_new() {
539 let cpu = CpuInfo::new(65.5, 8);
540 assert_eq!(cpu.usage_pct, Some(65.5));
541 assert_eq!(cpu.core_count, Some(8));
542 }
543
544 #[test]
545 fn test_cpu_info_with_temperature() {
546 let cpu = CpuInfo::new(50.0, 4).with_temperature(72.5);
547 assert_eq!(cpu.temperature_c, Some(72.5));
548 }
549
550 #[test]
551 fn test_cpu_info_default() {
552 let cpu = CpuInfo::default();
553 assert!(cpu.usage_pct.is_none());
554 assert!(cpu.per_core_pct.is_none());
555 assert!(cpu.core_count.is_none());
556 assert!(cpu.temperature_c.is_none());
557 assert!(cpu.frequency_mhz.is_none());
558 assert!(cpu.model.is_none());
559 assert!(cpu.is_throttling.is_none());
560 }
561
562 #[test]
563 fn test_cpu_info_full() {
564 let cpu = CpuInfo {
565 usage_pct: Some(80.0),
566 per_core_pct: Some(vec![75.0, 85.0, 78.0, 82.0]),
567 core_count: Some(4),
568 temperature_c: Some(65.0),
569 frequency_mhz: Some(3200.0),
570 model: Some("Intel i7".to_string()),
571 user_pct: Some(40.0),
572 system_pct: Some(25.0),
573 idle_pct: Some(20.0),
574 iowait_pct: Some(15.0),
575 is_throttling: Some(false),
576 };
577
578 assert_eq!(cpu.model, Some("Intel i7".to_string()));
579 assert_eq!(cpu.is_throttling, Some(false));
580 }
581
582 #[test]
587 fn test_memory_info_new() {
588 let mem = MemoryInfo::new(16_000_000_000, 12_000_000_000);
589 assert_eq!(mem.total_bytes, Some(16_000_000_000));
590 assert_eq!(mem.used_bytes, Some(12_000_000_000));
591 assert_eq!(mem.available_bytes, Some(4_000_000_000));
592 assert_eq!(mem.usage_pct, Some(75.0));
593 }
594
595 #[test]
596 fn test_memory_info_zero_total() {
597 let mem = MemoryInfo::new(0, 0);
598 assert_eq!(mem.usage_pct, Some(0.0));
599 }
600
601 #[test]
602 fn test_memory_info_default() {
603 let mem = MemoryInfo::default();
604 assert!(mem.total_bytes.is_none());
605 assert!(mem.used_bytes.is_none());
606 assert!(mem.available_bytes.is_none());
607 assert!(mem.usage_pct.is_none());
608 assert!(mem.swap_total_bytes.is_none());
609 assert!(mem.swap_used_bytes.is_none());
610 }
611
612 #[test]
617 fn test_gpu_info_default() {
618 let gpu = GpuInfo::default();
619 assert!(gpu.index.is_none());
620 assert!(gpu.name.is_none());
621 assert!(gpu.utilization_pct.is_none());
622 assert!(gpu.memory_total_bytes.is_none());
623 assert!(gpu.memory_used_bytes.is_none());
624 assert!(gpu.temperature_c.is_none());
625 assert!(gpu.power_w.is_none());
626 }
627
628 #[test]
629 fn test_gpu_info_full() {
630 let gpu = GpuInfo {
631 index: Some(0),
632 name: Some("NVIDIA Tesla V100".to_string()),
633 utilization_pct: Some(95.0),
634 memory_total_bytes: Some(32_000_000_000),
635 memory_used_bytes: Some(28_000_000_000),
636 memory_usage_pct: Some(87.5),
637 temperature_c: Some(75.0),
638 power_w: Some(250.0),
639 driver_version: Some("535.104.05".to_string()),
640 cuda_version: Some("12.2".to_string()),
641 };
642
643 assert_eq!(gpu.utilization_pct, Some(95.0));
644 assert_eq!(gpu.power_w, Some(250.0));
645 }
646
647 #[test]
652 fn test_storage_info_default() {
653 let storage = StorageInfo::default();
654 assert!(storage.mount_point.is_none());
655 assert!(storage.device.is_none());
656 assert!(storage.fs_type.is_none());
657 assert!(storage.total_bytes.is_none());
658 assert!(storage.used_bytes.is_none());
659 assert!(storage.available_bytes.is_none());
660 assert!(storage.usage_pct.is_none());
661 }
662
663 #[test]
664 fn test_storage_info_full() {
665 let storage = StorageInfo {
666 mount_point: Some("/data".to_string()),
667 device: Some("/dev/sda1".to_string()),
668 fs_type: Some("ext4".to_string()),
669 total_bytes: Some(1_000_000_000_000),
670 used_bytes: Some(600_000_000_000),
671 available_bytes: Some(400_000_000_000),
672 usage_pct: Some(60.0),
673 read_bytes_sec: Some(100_000_000),
674 write_bytes_sec: Some(50_000_000),
675 };
676
677 assert_eq!(storage.mount_point, Some("/data".to_string()));
678 assert_eq!(storage.read_bytes_sec, Some(100_000_000));
679 }
680
681 #[test]
686 fn test_ros_status_default() {
687 let ros = RosStatus::default();
688 assert!(ros.ros_version.is_none());
689 assert!(ros.distribution.is_none());
690 assert!(ros.domain_id.is_none());
691 assert!(ros.is_running.is_none());
692 assert!(ros.nodes.is_none());
693 }
694
695 #[test]
696 fn test_ros_status_full() {
697 let ros = RosStatus {
698 ros_version: Some(2),
699 distribution: Some("iron".to_string()),
700 domain_id: Some(0),
701 is_running: Some(true),
702 nodes: Some(vec![RosNode {
703 name: Some("/robot_controller".to_string()),
704 is_active: Some(true),
705 ..Default::default()
706 }]),
707 node_count: Some(15),
708 topic_count: Some(50),
709 service_count: None,
710 dds_middleware: None,
711 };
712
713 assert_eq!(ros.distribution, Some("iron".to_string()));
714 assert_eq!(ros.node_count, Some(15));
715 }
716
717 #[test]
722 fn test_compute_domain_serialization_roundtrip() {
723 let compute = ComputeDomain::with_cpu(CpuInfo::new(50.0, 8))
724 .with_memory(MemoryInfo::new(16_000_000_000, 8_000_000_000))
725 .with_load_average([1.0, 1.5, 2.0]);
726
727 let json = serde_json::to_string(&compute).unwrap();
728 let deserialized: ComputeDomain = serde_json::from_str(&json).unwrap();
729
730 assert!(deserialized.cpu.is_some());
731 assert!(deserialized.memory.is_some());
732 assert!(deserialized.load_average.is_some());
733 }
734}