1use planarian_schema::*;
5use std::collections::BTreeMap;
6
7fn probe(id: &str, kind: ProbeKind, origin: ProbeOrigin, establishes: &str) -> ProbeDef {
8 let measurement = matches!(kind, ProbeKind::Tcp { .. } | ProbeKind::Http { .. });
9 ProbeDef {
10 id: ProbeId(id.into()),
11 kind,
12 timeout: Millis::secs(10),
13 origin,
14 role: ProbeRole::Readiness,
15 cadence: Cadence::EveryTick,
16 cost: ProbeCost::Cheap,
17 side_effects: if measurement {
18 SideEffects::Measurement { bytes_max: 4096, per: Millis::secs(5) }
19 } else {
20 SideEffects::None
21 },
22 establishes: establishes.into(),
23 }
24}
25
26fn user_probe(id: &str, kind: ProbeKind) -> ProbeDef {
27 probe(id, kind, ProbeOrigin::User, id)
28}
29
30pub fn unit(id: &str, sens: &[Sensitivity], disruption: Disruption, probes: Vec<ProbeDef>) -> UnitDef {
31 let id: UnitId = id.parse().expect("unit id");
32 UnitDef {
33 desired: Desired {
34 status: if id.kind == UnitKind::Device && !id.name.starts_with("fs-") {
35 DesiredStatus::Bound
36 } else {
37 DesiredStatus::Running
38 },
39 when: Condition::Always,
40 },
41 id,
42 edges: vec![],
43 sensitivity: Sensitivities::of(sens),
44 restart_disruption: disruption,
45 probes,
46 synonyms: vec![],
47 systemd_restart: None,
48 startup_grace: None,
49 runtime_repair: None,
50 stops_are_faults: false,
51 holds: false,
52 hold_allow: vec![],
53 hold_interfaces: vec![],
54 }
55}
56
57fn edge(d: &mut UnitDef, to: &str, kind: EdgeKind, origin: EdgeOrigin) {
58 d.edges.push(Edge { from: d.id.clone(), to: to.parse().expect("unit id"), kind, origin });
59}
60
61pub fn gym_units() -> UnitDefs {
62 use Sensitivity::*;
63 let r = &[Routine][..];
64 let mut web = unit(
65 "service:gym-web",
66 r,
67 Disruption::Brief,
68 vec![
69 user_probe("gym-web-http", ProbeKind::Http { url: "http://127.0.0.1:8080/health".into(), status: 200 }),
70 user_probe("gym-web-tcp", ProbeKind::Tcp { addr: "127.0.0.1:8080".into() }),
71 ],
72 );
73 web.synonyms = vec!["web server".into(), "web".into()];
74 let db = unit(
75 "service:gym-db",
76 r,
77 Disruption::Brief,
78 vec![user_probe("gym-db-tcp", ProbeKind::Tcp { addr: "127.0.0.1:5432".into() })],
79 );
80 let mut app = unit(
81 "service:gym-app",
82 r,
83 Disruption::Brief,
84 vec![user_probe("gym-app-tcp", ProbeKind::Tcp { addr: "127.0.0.1:8081".into() })],
85 );
86 edge(&mut app, "service:gym-db", EdgeKind::Requires, EdgeOrigin::SystemdRequires);
87 edge(&mut app, "service:gym-cache", EdgeKind::Ordering, EdgeOrigin::SystemdAfter);
88 edge(&mut app, "service:gym-metrics", EdgeKind::Wants, EdgeOrigin::SystemdWants);
89 let cache = unit(
90 "service:gym-cache",
91 r,
92 Disruption::Brief,
93 vec![user_probe("gym-cache-tcp", ProbeKind::Tcp { addr: "127.0.0.1:6379".into() })],
94 );
95 let metrics = unit(
96 "service:gym-metrics",
97 r,
98 Disruption::None,
99 vec![user_probe("gym-metrics-tcp", ProbeKind::Tcp { addr: "127.0.0.1:9100".into() })],
100 );
101 let flap =
102 unit("service:gym-flap", r, Disruption::Brief, vec![user_probe("gym-flap-active", ProbeKind::SystemdActive)]);
103 let mut restartful = unit(
104 "service:gym-restartful",
105 r,
106 Disruption::Brief,
107 vec![user_probe("gym-restartful-active", ProbeKind::SystemdActive)],
108 );
109 restartful.systemd_restart =
110 Some(SystemdRestartPolicy { restart: true, restart_sec: Millis::secs(5), start_limit_burst: 3 });
111 let mut client = unit(
112 "service:gym-upstream-client",
113 r,
114 Disruption::Brief,
115 vec![user_probe(
116 "gym-upstream-client-http",
117 ProbeKind::Http { url: "http://10.0.2.2:9000/health".into(), status: 200 },
118 )],
119 );
120 edge(&mut client, "service:external:gym-upstream", EdgeKind::Requires, EdgeOrigin::Manifest);
121 let upstream = unit(
122 "service:external:gym-upstream",
123 &[External],
124 Disruption::None,
125 vec![user_probe("gym-upstream-tcp", ProbeKind::Tcp { addr: "10.0.2.2:9000".into() })],
126 );
127
128 let mut sshd = unit("service:sshd", &[Security], Disruption::Brief, vec![]);
129 sshd.synonyms = vec!["ssh".into()];
130 let mut nm = unit(
133 "service:NetworkManager",
134 &[External],
135 Disruption::ConnectivityLoss,
136 vec![user_probe(
137 "wifi-associated",
138 ProbeKind::Exec {
139 argv: vec!["nmcli".into(), "-g".into(), "GENERAL.STATE".into(), "device".into(), "show".into()],
140 sandbox: ProbeSandbox { uid: 0, caps: vec![], max_output: 4096, cpu_ms: 500, network: false },
141 },
142 )],
143 );
144 nm.synonyms = vec!["wifi".into(), "wi-fi".into(), "network".into(), "internet".into()];
145 nm.runtime_repair = Some("wifi.network".into());
146 let dm = unit("service:display-manager", r, Disruption::SessionLoss, vec![]);
147 let mut gs = unit("target:graphical-session", r, Disruption::SessionLoss, vec![]);
148 gs.desired.when = Condition::SessionEstablished { uid: None, seated: true };
149 gs.synonyms = vec!["desktop".into()];
150
151 let snd = unit(
152 "device:snd-hda0",
153 r,
154 Disruption::Brief,
155 vec![
156 user_probe("snd-hda0-bound", ProbeKind::SystemdActive),
157 user_probe(
160 "snd-hda0-driven",
161 ProbeKind::Exec {
162 argv: vec!["grep".into(), "-qv".into(), "no soundcards".into(), "/proc/asound/cards".into()],
163 sandbox: ProbeSandbox { uid: 0, caps: vec![], max_output: 4096, cpu_ms: 500, network: false },
164 },
165 ),
166 ],
167 );
168 let nic = unit(
169 "device:net-gym0",
170 &[External],
171 Disruption::ConnectivityLoss,
172 vec![user_probe("net-gym0-bound", ProbeKind::SystemdActive)],
173 );
174 let gpu = unit("device:gpu0", r, Disruption::SessionLoss, vec![user_probe("gpu0-bound", ProbeKind::SystemdActive)]);
175
176 let wlan = unit("device:wlan-hwsim", &[External], Disruption::ConnectivityLoss, vec![]);
178
179 let fs = |name: &str| unit(&format!("device:{name}"), r, Disruption::None, vec![]);
180 let mut backup = unit(
183 "service:gym-backup",
184 r,
185 Disruption::Brief,
186 vec![user_probe("gym-backup-active", ProbeKind::SystemdActive)],
187 );
188 backup.stops_are_faults = true;
189 let printer = unit(
191 "service:gym-printer",
192 r,
193 Disruption::Brief,
194 vec![user_probe("gym-printer-active", ProbeKind::SystemdActive)],
195 );
196 let tunnel = unit(
199 "service:gym-tunnel",
200 r,
201 Disruption::ConnectivityLoss,
202 vec![user_probe("gym-tunnel-active", ProbeKind::SystemdActive)],
203 );
204 let sync = unit(
205 "service:gym-sync",
206 r,
207 Disruption::DataInterruption,
208 vec![user_probe("gym-sync-active", ProbeKind::SystemdActive)],
209 );
210 let netd =
215 unit("service:gym-netd", r, Disruption::Brief, vec![user_probe("gym-netd-active", ProbeKind::SystemdActive)]);
216 let syncd =
217 unit("service:gym-syncd", r, Disruption::Brief, vec![user_probe("gym-syncd-active", ProbeKind::SystemdActive)]);
218
219 [
220 web,
221 db,
222 app,
223 cache,
224 metrics,
225 flap,
226 restartful,
227 client,
228 upstream,
229 sshd,
230 nm,
231 dm,
232 gs,
233 snd,
234 nic,
235 gpu,
236 wlan,
237 backup,
238 printer,
239 tunnel,
240 sync,
241 netd,
242 syncd,
243 fs("fs-root"),
244 fs("fs-nix"),
245 fs("fs-boot"),
246 fs("fs-journal"),
247 ]
248 .into_iter()
249 .map(|d| (d.id.clone(), d))
250 .collect()
251}
252
253pub fn gym_user_units() -> Vec<UnitDef> {
254 let user = |name: &str| UnitId {
255 kind: UnitKind::Service,
256 name: name.into(),
257 scope: Scope::User { uid: 0, session: None },
258 };
259 let mut pw = unit("service:pipewire", &[Sensitivity::Routine], Disruption::Brief, vec![]);
260 pw.id = user("pipewire");
261 pw.synonyms = vec!["sound".into(), "audio".into()];
262 pw.probes.push(user_probe(
264 "pipewire-has-a-card",
265 ProbeKind::Exec {
266 argv: vec!["grep".into(), "-qv".into(), "no soundcards".into(), "/proc/asound/cards".into()],
267 sandbox: ProbeSandbox { uid: 0, caps: vec![], max_output: 4096, cpu_ms: 500, network: false },
268 },
269 ));
270 pw.edges.push(Edge {
271 from: pw.id.clone(),
272 to: UnitId::device("snd-hda0"),
273 kind: EdgeKind::Requires,
274 origin: EdgeOrigin::Manifest,
275 });
276 let mut wp = unit("service:wireplumber", &[Sensitivity::Routine], Disruption::Brief, vec![]);
277 wp.id = user("wireplumber");
278 vec![pw, wp]
279}
280
281pub fn canonical() -> BTreeMap<String, Vec<ProbeDef>> {
283 let c = |id: &str, kind: ProbeKind, est: &str| probe(id, kind, ProbeOrigin::Canonical, est);
284 let free = |id: &str, path: &str, min_bytes: u64, min_permille: u16| {
285 c(id, ProbeKind::FreeSpace { path: path.into(), min_bytes, min_permille }, &format!("{path} has room"))
286 };
287 [
288 (
289 "sshd",
290 vec![c(
291 "sshd-tcp",
292 ProbeKind::Tcp { addr: "127.0.0.1:22".into() },
293 "sshd accepts TCP connections on port 22",
294 )],
295 ),
296 ("NetworkManager", vec![c("networkmanager-active", ProbeKind::SystemdActive, "NetworkManager is running")]),
297 ("display-manager", vec![c("display-manager-active", ProbeKind::SystemdActive, "the greeter is running")]),
298 (
299 "graphical-session",
300 vec![c(
301 "graphical-session-reached",
302 ProbeKind::SessionTarget { target: "graphical-session".into(), within: Millis::secs(60) },
303 "graphical-session.target reached within 60 s of the session's creation",
304 )],
305 ),
306 ("pipewire", vec![c("pipewire-active", ProbeKind::SystemdActive, "PipeWire is running in the user session")]),
307 ("fs-root", vec![free("fs-root-free", "/", 2_000_000_000, 50)]),
308 ("fs-nix", vec![free("fs-nix-free", "/nix", 2_000_000_000, 50)]),
309 ("fs-boot", vec![free("fs-boot-free", "/boot", 64_000_000, 0)]),
310 ("fs-journal", vec![free("fs-journal-free", "/var/lib/planarian", 128_000_000, 0)]),
311 ]
312 .into_iter()
313 .map(|(k, v)| (k.to_string(), v))
314 .collect()
315}