Skip to main content

planarian_fake/
fixtures.rs

1//! The gym's reference units (catalog `[[fixture]]`), as a manifest would
2//! declare them. Canonical probes are attached by `state` from `canonical()`.
3
4use 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    // Healthy while associated with the selected Wi-Fi network; repaired by
131    // selecting another one in range (§12a).
132    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            // As the gym VM's: the kernel lists a sound card (a driver that
158            // binds but cannot drive the card leaves none).
159            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    // The Wi-Fi radio (mac80211_hwsim in the VM).
177    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    // inferStops = false (§11, B-11): a backup job's absence is harm,
181    // whoever stopped it.
182    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    // A print service (§12f gym cases): nothing foreground work depends on.
190    let printer = unit(
191        "service:gym-printer",
192        r,
193        Disruption::Brief,
194        vec![user_probe("gym-printer-active", ProbeKind::SystemdActive)],
195    );
196    // C10 (§11 floors): two Routine units that differ only in what a restart
197    // costs, so a transition restarting both carries both disruptions.
198    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    // §12d classifier cases: two network tools by their systemd
211    // configuration (the module computes it; lane-daemon carries it), whose
212    // names match nothing. Whether they protect traffic is netwatch's,
213    // scripted by the gym's `footprint` steps.
214    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    // As in the gym VM: pipewire degrades without a card to drive.
263    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
281/// The canonical probes the core package ships (I10), by unit name.
282pub 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}