1use chrono::{DateTime, Local, TimeZone};
2use serde::{Deserialize, Serialize};
3use std::convert::TryFrom;
4
5use HostState::*;
6use ServiceState::*;
7
8#[derive(serde::Deserialize, serde::Serialize)]
9pub struct IcingaObjectResults<T> {
10 pub results: Vec<IcingaObjectResult<T>>,
11}
12
13#[derive(serde::Deserialize, serde::Serialize)]
14pub struct IcingaObjectResult<T> {
15 pub attrs: T,
16}
17
18#[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Copy, Clone, Serialize, Deserialize)]
19#[serde(try_from = "f64", into = "f64")]
20pub enum HostState {
21 UP,
22 DOWN,
23}
24
25impl TryFrom<u8> for HostState {
26 type Error = String;
27 fn try_from(i: u8) -> Result<Self, Self::Error> {
28 match i {
29 0 => Ok(UP),
30 1 => Ok(DOWN),
31 _ => Err(format!("Not a valid state number: {}", i)),
32 }
33 }
34}
35
36impl TryFrom<f64> for HostState {
37 type Error = String;
38 fn try_from(f: f64) -> Result<Self, Self::Error> {
39 if f.fract() != 0_f64 || f < 0_f64 || f > 255_f64 {
40 return Err(format!("Not a valid host state number: {}", f));
41 }
42 HostState::try_from(f as u8)
43 }
44}
45
46impl From<HostState> for f64 {
47 fn from(s: HostState) -> Self {
48 u8::from(s) as f64
49 }
50}
51
52impl From<HostState> for u8 {
53 fn from(s: HostState) -> Self {
54 match s {
55 UP => 0,
56 DOWN => 1,
57 }
58 }
59}
60
61#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Serialize, Deserialize)]
62#[serde(try_from = "f64", into = "f64")]
63pub enum ServiceState {
64 OK,
65 WARNING,
66 CRITICAL,
67 UNKNOWN,
68}
69
70impl TryFrom<u8> for ServiceState {
71 type Error = String;
72 fn try_from(i: u8) -> Result<Self, Self::Error> {
73 match i {
74 0 => Ok(OK),
75 1 => Ok(WARNING),
76 2 => Ok(CRITICAL),
77 3 => Ok(UNKNOWN),
78 _ => Err(format!("Not a valid state number: {}", i)),
79 }
80 }
81}
82
83impl TryFrom<f64> for ServiceState {
84 type Error = String;
85 fn try_from(f: f64) -> Result<Self, Self::Error> {
86 if f.fract() != 0_f64 || f < 0_f64 || f > 255_f64 {
87 return Err(format!("Not a valid service state number: {}", f));
88 }
89 ServiceState::try_from(f as u8)
90 }
91}
92impl From<ServiceState> for f64 {
93 fn from(s: ServiceState) -> Self {
94 u8::from(s) as f64
95 }
96}
97
98impl From<ServiceState> for u8 {
99 fn from(s: ServiceState) -> Self {
100 match s {
101 OK => 0,
102 WARNING => 1,
103 CRITICAL => 2,
104 UNKNOWN => 3,
105 }
106 }
107}
108
109#[derive(Debug, PartialEq, PartialOrd, Copy, Clone, Serialize, Deserialize)]
110#[serde(transparent)]
111pub struct Timestamp {
112 icinga_timestamp: f64,
113}
114
115impl Timestamp {
116 pub fn new(icinga_timestamp: f64) -> Self {
117 Timestamp { icinga_timestamp }
118 }
119
120 pub fn zero() -> Self {
121 Timestamp {
122 icinga_timestamp: 0_f64,
123 }
124 }
125
126 pub fn datetime<T: TimeZone>(&self, tz: &T) -> DateTime<T> {
128 let nanos = self.icinga_timestamp.fract() * 1_000_000_000_f64;
129 tz.timestamp(self.icinga_timestamp.trunc() as i64, nanos as u32)
130 }
131
132 pub fn localtime(&self) -> DateTime<Local> {
133 self.datetime(&Local)
134 }
135}
136
137pub trait CheckedObject {
138 const PATH: &'static str;
139 const OBJECT_TYPE_FOR_QUERY: &'static str;
140
141 fn name(&self) -> &str;
142 fn is_ok(&self) -> bool;
143 fn acknowledgement(&self) -> &Acknowledgement;
144 fn is_handled(&self) -> bool;
145
146 fn set_name(&mut self, name: String);
147 fn set_acknowledgement(&mut self, ack: Acknowledgement);
148 fn set_handled(&mut self, handled: bool);
149}
150
151#[derive(Debug, PartialEq, PartialOrd, Clone, Serialize, Deserialize)]
152pub struct Host {
153 pub name: String,
154 pub display_name: String,
155 pub address: String,
156 pub address6: String,
157 pub state: HostState,
158 pub last_state: HostState,
159 pub last_hard_state: HostState,
160 pub last_state_change: Timestamp,
161 pub last_state_up: Timestamp,
162 pub last_state_down: Timestamp,
163
164 pub next_check: Timestamp,
165 pub last_check_result: Option<LastCheckResult>,
166
167 #[serde(flatten)]
168 pub acknowledgement: Acknowledgement,
169
170 pub handled: bool,
171}
172
173impl Host {
174 pub fn is_handled(&self) -> bool {
186 self.handled
187 }
188}
189
190impl CheckedObject for Host {
191 const PATH: &'static str = "/v1/objects/hosts";
192 const OBJECT_TYPE_FOR_QUERY: &'static str = "host";
193
194 fn name(&self) -> &str {
195 &self.name
196 }
197
198 fn is_ok(&self) -> bool {
199 self.state == HostState::UP
200 }
201
202 fn acknowledgement(&self) -> &Acknowledgement {
203 &self.acknowledgement
204 }
205
206 fn is_handled(&self) -> bool {
207 self.handled
208 }
209
210 fn set_name(&mut self, name: String) {
211 self.name = name
212 }
213
214 fn set_acknowledgement(&mut self, ack: Acknowledgement) {
215 self.acknowledgement = ack
216 }
217
218 fn set_handled(&mut self, handled: bool) {
219 self.handled = handled
220 }
221}
222
223#[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Clone, Serialize, Deserialize)]
224pub struct LastCheckResult {
225 pub output: String,
226}
227
228#[derive(Debug, PartialEq, PartialOrd, Clone, Serialize, Deserialize)]
229pub struct Service {
230 #[serde(rename = "__name")]
231 pub name: String,
232 pub display_name: String,
233 pub host_name: String,
234 pub state: ServiceState,
235 pub last_state: ServiceState,
236 pub last_hard_state: ServiceState,
237 pub last_state_change: Timestamp,
238 pub last_state_critical: Timestamp,
239 pub last_state_warning: Timestamp,
240 pub last_state_ok: Timestamp,
241 pub last_state_unknown: Timestamp,
242
243 pub next_check: Timestamp,
244 pub last_check_result: Option<LastCheckResult>,
245
246 #[serde(flatten)]
247 pub acknowledgement: Acknowledgement,
248
249 pub handled: bool,
250 pub last_reachable: bool,
251}
252
253impl Service {
254 pub fn is_handled(&self) -> bool {
265 self.handled
266 }
267
268 pub fn host_is_reachable(&self) -> bool {
269 self.last_reachable
270 }
271}
272
273impl CheckedObject for Service {
274 const PATH: &'static str = "/v1/objects/services";
275 const OBJECT_TYPE_FOR_QUERY: &'static str = "service";
276
277 fn name(&self) -> &str {
278 &self.name
279 }
280
281 fn is_ok(&self) -> bool {
282 self.state == ServiceState::OK
283 }
284
285 fn acknowledgement(&self) -> &Acknowledgement {
286 &self.acknowledgement
287 }
288
289 fn is_handled(&self) -> bool {
290 self.handled
291 }
292
293 fn set_name(&mut self, name: String) {
294 self.name = name
295 }
296
297 fn set_acknowledgement(&mut self, ack: Acknowledgement) {
298 self.acknowledgement = ack
299 }
300
301 fn set_handled(&mut self, handled: bool) {
302 self.handled = handled
303 }
304}
305
306#[derive(Debug, PartialEq, PartialOrd, Clone, Serialize, Deserialize)]
307#[serde(from = "raw::Acknowledgement", into = "raw::Acknowledgement")]
308pub struct Acknowledgement {
309 pub state: AckState,
310 pub last_change: Option<Timestamp>,
311}
312
313#[derive(Debug, PartialEq, PartialOrd, Clone)]
314pub enum AckState {
315 None,
316 Acknowledged {
317 kind: AckKind,
318 expiry: Option<Timestamp>,
319 },
320}
321
322#[derive(Debug, PartialEq, PartialOrd, Clone)]
323pub enum AckKind {
324 Normal,
325 Sticky,
326}
327
328impl Acknowledgement {
329 pub fn none() -> Self {
331 Acknowledgement {
332 state: AckState::None,
333 last_change: None,
334 }
335 }
336
337 pub fn is_acknowledged(&self) -> bool {
338 self.state != AckState::None
339 }
340}
341
342impl From<raw::Acknowledgement> for Acknowledgement {
343 fn from(raw_ack: raw::Acknowledgement) -> Self {
344 fn expiry(ts: Timestamp) -> Option<Timestamp> {
345 if ts.icinga_timestamp == 0_f64 {
346 None
347 } else {
348 Some(ts)
349 }
350 }
351 let state = match raw_ack.state {
352 raw::AckState::None => AckState::None,
353 raw::AckState::Normal => AckState::Acknowledged {
354 kind: AckKind::Normal,
355 expiry: expiry(raw_ack.expiry),
356 },
357 raw::AckState::Sticky => AckState::Acknowledged {
358 kind: AckKind::Sticky,
359 expiry: expiry(raw_ack.expiry),
360 },
361 };
362 Acknowledgement {
363 state,
364 last_change: raw_ack.last_change,
365 }
366 }
367}
368
369impl From<Acknowledgement> for raw::Acknowledgement {
370 fn from(ack: Acknowledgement) -> Self {
371 let (state, expiry) = match ack.state {
372 AckState::None => (raw::AckState::None, Timestamp::zero()),
373 AckState::Acknowledged {
374 kind: AckKind::Normal,
375 expiry,
376 } => (raw::AckState::Normal, expiry.unwrap_or(Timestamp::zero())),
377 AckState::Acknowledged {
378 kind: AckKind::Sticky,
379 expiry,
380 } => (raw::AckState::Sticky, expiry.unwrap_or(Timestamp::zero())),
381 };
382 let last_change = ack.last_change;
383 raw::Acknowledgement {
384 state,
385 expiry,
386 last_change,
387 }
388 }
389}
390
391pub mod raw {
392 use std::convert::TryFrom;
396
397 use super::Timestamp;
398 use serde::{Deserialize, Serialize};
399
400 #[derive(Debug, PartialEq, PartialOrd, Clone, Serialize, Deserialize)]
401 pub struct Acknowledgement {
402 #[serde(rename = "acknowledgement")]
403 pub state: AckState,
404 #[serde(rename = "acknowledgement_expiry")]
405 pub expiry: Timestamp,
406 #[serde(rename = "acknowledgement_last_change")]
407 pub last_change: Option<Timestamp>,
408 }
409
410 #[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Clone, Serialize, Deserialize)]
411 #[serde(try_from = "f64", into = "f64")]
412 pub enum AckState {
413 None,
414 Normal,
415 Sticky,
416 }
417
418 impl TryFrom<f64> for AckState {
419 type Error = String;
420 fn try_from(f: f64) -> Result<Self, Self::Error> {
421 if f.fract() != 0_f64 || f < 0_f64 || f > 255_f64 {
422 return Err(format!("Not a valid host state number: {}", f));
423 }
424 AckState::try_from(f as u8)
425 }
426 }
427
428 impl From<AckState> for f64 {
429 fn from(s: AckState) -> Self {
430 match s {
431 AckState::None => 0_f64,
432 AckState::Normal => 1_f64,
433 AckState::Sticky => 2_f64,
434 }
435 }
436 }
437
438 impl TryFrom<u8> for AckState {
439 type Error = String;
440 fn try_from(i: u8) -> Result<Self, Self::Error> {
441 match i {
442 0 => Ok(AckState::None),
443 1 => Ok(AckState::Normal),
444 2 => Ok(AckState::Sticky),
445 _ => Err(format!("Invalid acknowledgement state number: {}", i)),
446 }
447 }
448 }
449}
450
451#[cfg(test)]
452mod test {
453
454 #[cfg(test)]
455 use pretty_assertions::assert_eq;
456
457 use super::*;
458 use chrono::Local;
459 use serde_json::Value;
460
461 static HOST_OBJ: &[u8] = include_bytes!("../test-input/example-host-object.json");
462 static SERVICE_OBJ: &[u8] = include_bytes!("../test-input/example-service-object.json");
463
464 #[test]
465 fn test_timestamp_localtime() {
466 let icinga_timestamp = 1584180320.814572_f64;
467 assert_eq!(
468 Timestamp::new(icinga_timestamp).localtime(),
469 Local.timestamp(1584180320, 814572095)
470 );
471
472 assert_eq!(
473 Timestamp::new(1583917754.192786_f64).localtime(),
474 Local.timestamp(1583917754, 192785978)
475 );
476
477 assert_eq!(
478 Timestamp::new(1583917754.192786111_f64).localtime(),
479 Local.timestamp(1583917754, 192786216)
480 );
481 }
482
483 #[test]
484 fn test_de_host_object() {
485 let r: Host = serde_json::from_slice(HOST_OBJ).unwrap();
486 assert_eq!(
487 r,
488 Host {
489 name: "example-host".to_string(),
490 display_name: "example-host".to_string(),
491 address: "example-host.de".to_string(),
492 address6: "".to_string(),
493 state: HostState::UP,
494 last_state: HostState::UP,
495 last_hard_state: HostState::UP,
496 last_state_change: Timestamp::new(1584154326.317929_f64),
497 last_state_up: Timestamp::new(1584180320.814572_f64),
498 last_state_down: Timestamp::new(1584154292.773004_f64),
499 next_check: Timestamp::new(1584180380.234708_f64),
500 last_check_result: Some(LastCheckResult {
501 output: "PING OK - Packet loss = 0%, RTA = 23.24 ms".to_string()
502 }),
503
504 acknowledgement: Acknowledgement::none(),
505
506 handled: false,
507 }
508 )
509 }
510
511 static REDUCED_HOST_OBJ: &[u8] = r#"
512{
513 "acknowledgement": 0.0,
514 "acknowledgement_expiry": 0.0,
515 "acknowledgement_last_change": null,
516 "address": "example-host.de",
517 "address6": "",
518 "display_name": "example-host",
519 "handled": false,
520 "last_check_result": {
521 "output": "PING OK - Packet loss = 0%, RTA = 23.24 ms"
522 },
523 "last_hard_state": 0.0,
524 "last_state": 0.0,
525 "last_state_change": 1584154326.317929,
526 "last_state_down": 1584154292.773004,
527 "last_state_up": 1584180320.814572,
528 "name": "example-host",
529 "next_check": 1584180380.234708,
530 "state": 0.0
531 }
532"#
533 .as_bytes();
534
535 #[test]
536 fn test_se_host_object() {
537 let host: Host = Host {
538 name: "example-host".to_string(),
539 display_name: "example-host".to_string(),
540 address: "example-host.de".to_string(),
541 address6: "".to_string(),
542 state: HostState::UP,
543 last_state: HostState::UP,
544 last_hard_state: HostState::UP,
545 last_state_change: Timestamp::new(1584154326.317929_f64),
546 last_state_up: Timestamp::new(1584180320.814572_f64),
547 last_state_down: Timestamp::new(1584154292.773004_f64),
548 next_check: Timestamp::new(1584180380.234708_f64),
549 last_check_result: Some(LastCheckResult {
550 output: "PING OK - Packet loss = 0%, RTA = 23.24 ms".to_string(),
551 }),
552
553 acknowledgement: Acknowledgement::none(),
554
555 handled: false,
556 };
557 assert_eq!(
558 serde_json::to_value(host).unwrap(),
559 serde_json::from_slice::<Value>(REDUCED_HOST_OBJ).unwrap()
560 )
561 }
562
563 #[test]
564 fn test_de_service_object() {
565 let r: Service = serde_json::from_slice(SERVICE_OBJ).unwrap();
566 assert_eq!(
567 r,
568 Service {
569 name: "example-host!example-service".to_string(),
570 display_name: "check-example-service".to_string(),
571 host_name: "example-host".to_string(),
572 state: ServiceState::OK,
573 last_state: ServiceState::OK,
574 last_hard_state: ServiceState::OK,
575 last_state_change: Timestamp::new(1583917754.192786_f64),
576 last_state_critical: Timestamp::new(0_f64),
577 last_state_ok: Timestamp::new(1584184342.288635_f64),
578 last_state_warning: Timestamp::new(1583917693.623402_f64),
579 last_state_unknown: Timestamp::new(1583860703.96808_f64),
580 next_check: Timestamp::new(1584184401.578774_f64),
581 last_check_result: Some(LastCheckResult {
582 output: "OK: nothing wrong".to_string()
583 }),
584 acknowledgement: Acknowledgement::none(),
585 handled: false,
586 last_reachable: true,
587 }
588 )
589 }
590
591 static REDUCED_SERVICE_OBJ: &[u8] = r#"
592{
593 "__name": "example_host!example_service",
594 "acknowledgement": 0.0,
595 "acknowledgement_expiry": 0.0,
596 "acknowledgement_last_change": null,
597 "display_name": "example_service",
598 "handled": false,
599 "last_check_result": {
600 "output": "PING OK - Packet loss = 0%, RTA = 23.24 ms"
601 },
602 "host_name": "example_host",
603 "last_hard_state": 0.0,
604 "last_state": 0.0,
605 "last_state_change": 1584154326.317929,
606 "last_state_critical": 1584154292.773004,
607 "last_state_warning": 1584154292.773004,
608 "last_state_ok": 1584180320.814572,
609 "last_state_unknown": 1584154292.773004,
610 "last_reachable": true,
611 "next_check": 1584180380.234708,
612 "state": 0.0
613 }
614"#
615 .as_bytes();
616
617 #[test]
618 fn test_se_service_object() {
619 let service: Service = Service {
620 name: "example_host!example_service".to_string(),
621 host_name: "example_host".to_string(),
622 display_name: "example_service".to_string(),
623 state: ServiceState::OK,
624 last_state: ServiceState::OK,
625 last_hard_state: ServiceState::OK,
626 last_state_change: Timestamp::new(1584154326.317929_f64),
627 last_state_ok: Timestamp::new(1584180320.814572_f64),
628 last_state_warning: Timestamp::new(1584154292.773004_f64),
629 last_state_critical: Timestamp::new(1584154292.773004_f64),
630 last_state_unknown: Timestamp::new(1584154292.773004_f64),
631 last_reachable: true,
632 next_check: Timestamp::new(1584180380.234708_f64),
633 last_check_result: Some(LastCheckResult {
634 output: "PING OK - Packet loss = 0%, RTA = 23.24 ms".to_string(),
635 }),
636
637 acknowledgement: Acknowledgement::none(),
638
639 handled: false,
640 };
641 assert_eq!(
642 serde_json::to_value(service).unwrap(),
643 serde_json::from_slice::<Value>(REDUCED_SERVICE_OBJ).unwrap()
644 )
645 }
646
647 static ACK_OBJ_STR: &str = r#"
648 {
649 "acknowledgement": 1.0,
650 "acknowledgement_expiry": 0.0,
651 "acknowledgement_last_change": 1614632568.699408
652 }
653 "#;
654 static ACK_OBJ: &[u8] = ACK_OBJ_STR.as_bytes();
655 #[test]
656 fn test_de_ack_raw() {
657 let a: raw::Acknowledgement = serde_json::from_slice(ACK_OBJ).unwrap();
658 assert_eq!(
659 a,
660 raw::Acknowledgement {
661 state: raw::AckState::Normal,
662 expiry: Timestamp::zero(),
663 last_change: Some(Timestamp::new(1614632568.699408f64))
664 }
665 );
666 }
667 #[test]
668 fn test_se_ack_raw() {
669 let a: raw::Acknowledgement = raw::Acknowledgement {
670 state: raw::AckState::Normal,
671 expiry: Timestamp::zero(),
672 last_change: Some(Timestamp::new(1614632568.699408f64)),
673 };
674 assert_eq!(
675 serde_json::to_value(&a).unwrap(),
676 serde_json::from_slice::<Value>(ACK_OBJ).unwrap()
677 );
678 }
679 #[test]
680 fn test_de_ack() {
681 let a: Acknowledgement = serde_json::from_slice(ACK_OBJ).unwrap();
682 assert_eq!(
683 a,
684 Acknowledgement {
685 last_change: Some(Timestamp::new(1614632568.699408f64)),
686 state: AckState::Acknowledged {
687 kind: AckKind::Normal,
688 expiry: None
689 },
690 }
691 );
692
693 assert!(a.is_acknowledged(), "Should be acknowledged");
694 }
695
696 #[test]
697 fn test_se_ack() {
698 let a: Acknowledgement = Acknowledgement {
699 last_change: Some(Timestamp::new(1614632568.699408f64)),
700 state: AckState::Acknowledged {
701 kind: AckKind::Normal,
702 expiry: None,
703 },
704 };
705 assert_eq!(
706 serde_json::to_value(a).unwrap(),
707 serde_json::from_slice::<Value>(ACK_OBJ).unwrap()
708 );
709 }
710
711 static ACK_EXP_OBJ: &[u8] = r#"
712 {
713 "acknowledgement": 1.0,
714 "acknowledgement_expiry": 1714632568.699408,
715 "acknowledgement_last_change": 1614632568.699408
716 }
717 "#
718 .as_bytes();
719
720 #[test]
721 fn test_de_ack_raw_exp() {
722 let expected_expiry = Timestamp::new(1714632568.699408f64);
723 let a: raw::Acknowledgement = serde_json::from_slice(ACK_EXP_OBJ).unwrap();
724 assert_eq!(
725 a,
726 raw::Acknowledgement {
727 state: raw::AckState::Normal,
728 expiry: expected_expiry,
729 last_change: Some(Timestamp::new(1614632568.699408f64))
730 }
731 );
732 }
733
734 #[test]
735 fn test_de_ack_exp() {
736 let expected_expiry = Timestamp::new(1714632568.699408f64);
737 let a: Acknowledgement = serde_json::from_slice(ACK_EXP_OBJ).unwrap();
738 assert_eq!(
739 a,
740 Acknowledgement {
741 last_change: Some(Timestamp::new(1614632568.699408f64)),
742 state: AckState::Acknowledged {
743 kind: AckKind::Normal,
744 expiry: Some(expected_expiry)
745 },
746 }
747 );
748
749 assert!(a.is_acknowledged(), "Should be acknowledged");
750 }
751
752 static NOT_ACK_OBJ: &[u8] = r#"
753 {
754 "acknowledgement": 0.0,
755 "acknowledgement_expiry": 0.0,
756 "acknowledgement_last_change": 1614632568.699408
757 }
758 "#
759 .as_bytes();
760
761 #[test]
762 fn test_de_not_ack_raw() {
763 let a: raw::Acknowledgement = serde_json::from_slice(NOT_ACK_OBJ).unwrap();
764 assert_eq!(
765 a,
766 raw::Acknowledgement {
767 state: raw::AckState::None,
768 expiry: Timestamp::zero(),
769 last_change: Some(Timestamp::new(1614632568.699408_f64))
770 }
771 );
772 }
773
774 #[test]
775 fn test_de_not_ack() {
776 let a: Acknowledgement = serde_json::from_slice(NOT_ACK_OBJ).unwrap();
777 assert_eq!(
778 a,
779 Acknowledgement {
780 state: AckState::None,
781 last_change: Some(Timestamp::new(1614632568.699408_f64))
782 }
783 );
784 assert!(!a.is_acknowledged(), "Should not be acknowledged")
785 }
786}