1use planarian_schema::*;
16
17pub fn alarm(unit: &UnitId) -> String {
19 format!("ALARM: {unit} stopped unexpectedly. Network traffic is blocked until it is back.")
20}
21
22pub fn hold_unit(unit: &UnitId) -> UnitId {
25 UnitId::service(&format!("planarian-hold@{}", unit.name))
26}
27
28pub fn held_by(u: &UnitId) -> Option<UnitId> {
30 u.name.strip_prefix("planarian-hold@").map(UnitId::service)
31}
32
33pub fn hold_def(held: &UnitDef) -> UnitDef {
38 let mut d = held.clone();
39 d.id = hold_unit(&held.id);
40 d.desired = Desired { status: DesiredStatus::Stopped, when: Condition::Any(vec![]) };
41 d.edges.clear();
42 d.probes.clear();
43 d.synonyms.clear();
44 d.systemd_restart = None;
45 d.runtime_repair = None;
46 d.restart_disruption = Disruption::None;
47 d.holds = false;
48 d.hold_allow.clear();
49 d.hold_interfaces.clear();
50 d
51}
52
53pub fn network_security(defs: &UnitDefs) -> impl Iterator<Item = &UnitDef> {
55 defs.values().filter(|d| d.holds)
56}
57
58pub fn failed(s: &SystemState, defs: &UnitDefs, u: &UnitId) -> bool {
62 defs.get(u).is_some_and(|d| d.desired.status == DesiredStatus::Running)
63 && s.expected(u, defs)
64 && matches!(s.status(u, defs), UnitStatus::Failed(_) | UnitStatus::Stopped | UnitStatus::Absent)
65}
66
67pub fn back(s: &SystemState, defs: &UnitDefs, u: &UnitId) -> bool {
70 let Some(def) = defs.get(u) else { return false };
71 let readiness = def.probes.iter().filter(|p| p.role == ProbeRole::Readiness).count();
72 s.status(u, defs) == UnitStatus::Running && s.passing_readiness(u, defs).len() == readiness
73}
74
75pub fn release_until(u: &Until) -> Option<Timestamp> {
79 match u {
80 Until::At(t) => Some(*t),
81 Until::NextBoot | Until::Explicit => None,
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn the_hold_unit_names_its_unit_and_back() {
91 let wg = UnitId::service("wireguard-gym");
92 assert_eq!(hold_unit(&wg), UnitId::service("planarian-hold@wireguard-gym"));
93 assert_eq!(held_by(&hold_unit(&wg)), Some(wg.clone()));
94 assert_eq!(held_by(&wg), None);
95 assert_eq!(
96 alarm(&wg),
97 "ALARM: service:wireguard-gym stopped unexpectedly. Network traffic is blocked until it is back."
98 );
99 }
100
101 #[test]
102 fn the_hold_def_carries_sensitivity_and_is_never_expected() {
103 let mut wg = planarian_schema::fixtures::service("wireguard-gym");
104 wg.sensitivity = Sensitivities::of(&[Sensitivity::Security, Sensitivity::External]);
105 wg.holds = true;
106 let d = hold_def(&wg);
107 assert_eq!(d.id, UnitId::service("planarian-hold@wireguard-gym"));
108 assert_eq!(d.sensitivity, wg.sensitivity);
109 assert!(!d.holds);
110 let mut defs = UnitDefs::new();
111 defs.insert(d.id.clone(), d.clone());
112 let s = planarian_schema::fixtures::empty_state(0);
113 assert!(!s.expected(&d.id, &defs), "never watched");
114 }
115
116 #[test]
117 fn a_release_ends_at_the_next_boot_unless_timed() {
118 assert_eq!(release_until(&Until::At(Timestamp(5))), Some(Timestamp(5)));
119 assert_eq!(release_until(&Until::NextBoot), None);
120 assert_eq!(release_until(&Until::Explicit), None);
121 }
122}