1use crate::ids::{Millis, ProbeId, Timestamp};
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use std::collections::{BTreeMap, BTreeSet};
6use std::fmt;
7use std::str::FromStr;
8use vstd::prelude::*;
9
10verus! {
12
13#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
17pub struct UnitId {
18 pub kind: UnitKind,
19 pub name: String,
20 pub scope: Scope,
21}
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
25#[derive(Serialize, Deserialize)]
26#[verifier::external_derive]
27pub enum UnitKind {
28 Service,
29 Device,
30 Target,
31}
32
33#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
34#[derive(Serialize, Deserialize)]
35#[verifier::external_derive]
36pub enum Scope {
37 System,
38 User { uid: u32, session: Option<String> },
39}
40
41#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
42#[derive(Serialize, Deserialize)]
43#[verifier::external_derive]
44pub enum Actor {
45 Daemon,
46 Watchdog,
47 User { uid: u32, session: Option<String>, seat: Option<String>, active: bool },
52 Model(crate::ids::ModelId),
53 Agent(crate::ids::PrincipalId),
56 Unattributed,
58}
59
60#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
61#[derive(Serialize, Deserialize)]
62#[verifier::external_derive]
63pub struct Override {
64 pub unit: UnitId,
65 pub desired: OverrideDesired,
66 pub until: Until,
67 pub reason: String,
68 pub by: Actor,
71 pub attribution: Attribution,
72 #[serde(default)]
75 pub acknowledged: bool,
76}
77
78#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
81#[derive(Serialize, Deserialize)]
82#[verifier::external_derive]
83pub enum OverrideDesired {
84 LeaveAlone,
86 Stopped,
88 Absent,
90 Detected,
92}
93
94#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
95#[derive(Serialize, Deserialize)]
96#[verifier::external_derive]
97pub enum Attribution {
98 Attested,
100 Inferred,
102}
103
104#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
105#[derive(Serialize, Deserialize)]
106#[verifier::external_derive]
107pub enum Until {
108 At(Timestamp),
109 NextBoot,
110 Explicit,
111}
112
113#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
114#[derive(Serialize, Deserialize)]
115#[verifier::external_derive]
116pub enum Sensitivity {
117 Routine,
118 Security,
119 UserData,
120 External,
121 Core,
122}
123
124#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
126#[derive(Serialize, Deserialize)]
127#[verifier::external_derive]
128pub enum Disruption {
129 None,
130 Brief,
131 SessionLoss,
132 DataInterruption,
133 ConnectivityLoss,
134}
135
136#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
138#[derive(Serialize, Deserialize)]
139#[verifier::external_derive]
140pub enum Recovery {
141 Idempotent,
142 Revertible,
143 NonReversible,
144}
145
146#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
150pub struct Sensitivities {
151 pub routine: bool,
152 pub security: bool,
153 pub user_data: bool,
154 pub external: bool,
155 pub core: bool,
156}
157
158impl Sensitivities {
159 pub open spec fn has(self, s: Sensitivity) -> bool {
160 match s {
161 Sensitivity::Routine => self.routine,
162 Sensitivity::Security => self.security,
163 Sensitivity::UserData => self.user_data,
164 Sensitivity::External => self.external,
165 Sensitivity::Core => self.core,
166 }
167 }
168
169 pub open spec fn routine_only(self) -> bool {
171 !self.security && !self.user_data && !self.external && !self.core
172 }
173
174 pub fn contains(&self, s: Sensitivity) -> (r: bool)
175 ensures r == self.has(s),
176 {
177 match s {
178 Sensitivity::Routine => self.routine,
179 Sensitivity::Security => self.security,
180 Sensitivity::UserData => self.user_data,
181 Sensitivity::External => self.external,
182 Sensitivity::Core => self.core,
183 }
184 }
185
186 pub fn only_routine(&self) -> (r: bool)
187 ensures r == self.routine_only(),
188 {
189 !self.security && !self.user_data && !self.external && !self.core
190 }
191
192 pub fn union(&self, other: &Sensitivities) -> (r: Sensitivities)
193 ensures forall|s: Sensitivity| #[trigger] r.has(s) == (self.has(s) || other.has(s)),
194 {
195 Sensitivities {
196 routine: self.routine || other.routine,
197 security: self.security || other.security,
198 user_data: self.user_data || other.user_data,
199 external: self.external || other.external,
200 core: self.core || other.core,
201 }
202 }
203
204 pub fn insert(&mut self, s: Sensitivity)
205 ensures forall|t: Sensitivity| #[trigger] final(self).has(t) == (old(self).has(t) || t == s),
206 {
207 match s {
208 Sensitivity::Routine => self.routine = true,
209 Sensitivity::Security => self.security = true,
210 Sensitivity::UserData => self.user_data = true,
211 Sensitivity::External => self.external = true,
212 Sensitivity::Core => self.core = true,
213 }
214 }
215}
216
217} impl Actor {
220 pub fn user(uid: u32) -> Actor {
223 Actor::User { uid, session: None, seat: None, active: false }
224 }
225
226 pub fn local(uid: u32, session: &str) -> Actor {
228 Actor::User { uid, session: Some(session.into()), seat: Some("seat0".into()), active: true }
229 }
230
231 pub fn principal(&self) -> crate::ids::PrincipalId {
234 use crate::ids::PrincipalId;
235 const BASE: u64 = 1 << 32;
236 match self {
237 Actor::User { uid, .. } => PrincipalId(*uid as u64),
238 Actor::Daemon => PrincipalId(BASE),
239 Actor::Watchdog => PrincipalId(BASE + 1),
240 Actor::Model(_) => PrincipalId(BASE + 2),
241 Actor::Unattributed => PrincipalId(BASE + 3),
242 Actor::Agent(p) => *p,
243 }
244 }
245
246 pub fn uid(&self) -> Option<u32> {
247 match self {
248 Actor::User { uid, .. } => Some(*uid),
249 _ => None,
250 }
251 }
252}
253
254impl Override {
255 pub fn attested(unit: UnitId, desired: OverrideDesired, until: Until, reason: String, by: Actor) -> Override {
257 Override { unit, desired, until, reason, by, attribution: Attribution::Attested, acknowledged: false }
258 }
259}
260
261impl Sensitivities {
262 pub const ALL: [Sensitivity; 5] =
263 [Sensitivity::Routine, Sensitivity::Security, Sensitivity::UserData, Sensitivity::External, Sensitivity::Core];
264
265 pub fn of(items: &[Sensitivity]) -> Sensitivities {
266 let mut out = Sensitivities::default();
267 for s in items {
268 out.insert(*s);
269 }
270 out
271 }
272
273 pub fn members(&self) -> Vec<Sensitivity> {
275 Self::ALL.into_iter().filter(|s| self.contains(*s)).collect()
276 }
277}
278
279impl Serialize for Sensitivities {
280 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
281 self.members().serialize(s)
282 }
283}
284
285impl<'de> Deserialize<'de> for Sensitivities {
286 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
287 Ok(Sensitivities::of(&Vec::<Sensitivity>::deserialize(d)?))
288 }
289}
290
291impl UnitId {
292 pub fn system(kind: UnitKind, name: &str) -> UnitId {
293 UnitId { kind, name: name.to_owned(), scope: Scope::System }
294 }
295
296 pub fn service(name: &str) -> UnitId {
297 UnitId::system(UnitKind::Service, name)
298 }
299
300 pub fn device(name: &str) -> UnitId {
301 UnitId::system(UnitKind::Device, name)
302 }
303
304 pub fn target(name: &str) -> UnitId {
305 UnitId::system(UnitKind::Target, name)
306 }
307}
308
309impl fmt::Display for UnitId {
310 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311 let kind = match self.kind {
312 UnitKind::Service => "service",
313 UnitKind::Device => "device",
314 UnitKind::Target => "target",
315 };
316 match &self.scope {
317 Scope::System => write!(f, "{kind}:{}", self.name),
318 Scope::User { uid, session: None } => write!(f, "{kind}@{uid}:{}", self.name),
319 Scope::User { uid, session: Some(s) } => write!(f, "{kind}@{uid}/{s}:{}", self.name),
320 }
321 }
322}
323
324#[derive(Debug, PartialEq, Eq)]
325pub struct ParseUnitIdError(pub String);
326
327impl FromStr for UnitId {
328 type Err = ParseUnitIdError;
329
330 fn from_str(s: &str) -> Result<Self, Self::Err> {
331 let err = || ParseUnitIdError(s.to_owned());
332 let (head, name) = s.split_once(':').ok_or_else(err)?;
333 if name.is_empty() {
334 return Err(err());
335 }
336 let (kind, scope) = match head.split_once('@') {
337 None => (head, Scope::System),
338 Some((kind, user)) => {
339 let (uid, session) = match user.split_once('/') {
340 None => (user, None),
341 Some((uid, session)) => (uid, Some(session.to_owned())),
342 };
343 (kind, Scope::User { uid: uid.parse().map_err(|_| err())?, session })
344 }
345 };
346 let kind = match kind {
347 "service" => UnitKind::Service,
348 "device" => UnitKind::Device,
349 "target" => UnitKind::Target,
350 _ => return Err(err()),
351 };
352 Ok(UnitId { kind, name: name.to_owned(), scope })
353 }
354}
355
356impl Serialize for UnitId {
357 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
358 s.collect_str(self)
359 }
360}
361
362impl<'de> Deserialize<'de> for UnitId {
363 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
364 let s = String::deserialize(d)?;
365 s.parse().map_err(|e: ParseUnitIdError| serde::de::Error::custom(format!("bad unit id {:?}", e.0)))
366 }
367}
368
369#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
374pub enum Condition {
375 Always,
376 SessionEstablished {
382 uid: Option<u32>,
383 #[serde(default)]
384 seated: bool,
385 },
386 DeviceConnected(UnitId),
387 OnAcPower,
388 OnBattery,
389 Foreground {
392 session: String,
393 },
394 All(Vec<Condition>),
395 Any(Vec<Condition>),
396}
397
398impl Condition {
399 pub fn holds(&self, atoms: &BTreeSet<Condition>) -> bool {
401 match self {
402 Condition::Always => true,
403 Condition::All(cs) => cs.iter().all(|c| c.holds(atoms)),
404 Condition::Any(cs) => cs.iter().any(|c| c.holds(atoms)),
405 Condition::SessionEstablished { uid, seated } => atoms.iter().any(|a| {
406 matches!(a, Condition::SessionEstablished { uid: u, seated: s }
407 if (uid.is_none() || u == uid) && (*s || !*seated))
408 }),
409 atom => atoms.contains(atom),
410 }
411 }
412
413 pub fn needs_seat(&self) -> bool {
415 match self {
416 Condition::SessionEstablished { seated, .. } => *seated,
417 Condition::All(cs) | Condition::Any(cs) => cs.iter().any(Condition::needs_seat),
418 _ => false,
419 }
420 }
421}
422
423#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
424pub enum DesiredStatus {
425 Running,
426 Stopped,
427 Bound,
428 Absent,
429}
430
431#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
432pub struct Desired {
433 pub status: DesiredStatus,
434 pub when: Condition,
435}
436
437impl Override {
438 pub fn holds_at(&self, now: Timestamp) -> bool {
440 match self.until {
441 Until::At(t) => now < t,
442 Until::NextBoot | Until::Explicit => true,
443 }
444 }
445}
446
447#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
452#[serde(transparent)]
453pub struct Confidence(pub u16);
454
455#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
456pub enum EdgeKind {
457 Ordering,
459 Wants,
461 Requires,
463 Diagnostic { confidence: Confidence },
465}
466
467#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
468pub struct Edge {
469 pub from: UnitId,
470 pub to: UnitId,
471 pub kind: EdgeKind,
472 pub origin: EdgeOrigin,
473}
474
475#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
476pub enum EdgeOrigin {
477 SystemdRequires,
478 SystemdWants,
479 SystemdAfter,
480 Manifest,
481 Learned,
482}
483
484#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
487pub struct SystemdRestartPolicy {
488 pub restart: bool,
489 pub restart_sec: Millis,
490 pub start_limit_burst: u32,
491}
492
493#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
494pub struct UnitDef {
495 pub id: UnitId,
496 pub desired: Desired,
497 pub edges: Vec<Edge>,
498 pub sensitivity: Sensitivities,
499 pub restart_disruption: Disruption,
500 pub probes: Vec<ProbeDef>,
501 pub synonyms: Vec<String>,
502 pub systemd_restart: Option<SystemdRestartPolicy>,
503 #[serde(default)]
506 pub startup_grace: Option<Millis>,
507 #[serde(default)]
511 pub runtime_repair: Option<String>,
512 #[serde(default)]
516 pub stops_are_faults: bool,
517 #[serde(default)]
522 pub holds: bool,
523 #[serde(default)]
525 pub hold_allow: Vec<HoldEndpoint>,
526 #[serde(default)]
528 pub hold_interfaces: Vec<String>,
529}
530
531#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
533pub struct HoldEndpoint {
534 pub address: String,
535 pub port: u16,
536 pub protocol: String,
537}
538
539pub type UnitDefs = BTreeMap<UnitId, UnitDef>;
540
541#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
544pub struct ProbeDef {
545 pub id: ProbeId,
546 pub kind: ProbeKind,
547 pub timeout: Millis,
548 pub origin: ProbeOrigin,
549 pub role: ProbeRole,
551 pub cadence: Cadence,
552 pub cost: ProbeCost,
553 pub side_effects: SideEffects,
554 pub establishes: String,
556}
557
558#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
559pub enum ProbeOrigin {
560 Canonical,
561 User,
562}
563
564#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
565pub enum ProbeKind {
566 SystemdActive,
567 Tcp {
568 addr: String,
569 },
570 Http {
571 url: String,
572 status: u16,
573 },
574 DBus {
575 service: String,
576 path: String,
577 method: String,
578 },
579 FilePresent {
580 path: String,
581 },
582 Exec {
584 argv: Vec<String>,
585 sandbox: ProbeSandbox,
586 },
587 FreeSpace {
590 path: String,
591 min_bytes: u64,
592 min_permille: u16,
593 },
594 SessionTarget {
598 target: String,
599 within: Millis,
600 },
601}
602
603#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
604pub struct ProbeSandbox {
605 pub uid: u32,
606 pub caps: Vec<String>,
607 pub max_output: u32,
608 pub cpu_ms: u32,
609 pub network: bool,
610}
611
612#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
613pub enum ProbeRole {
614 Readiness,
615 Diagnostic,
616}
617
618#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
619pub enum Cadence {
620 EveryTick,
621 Every(Millis),
622 OnEvent,
623 OnDemand,
624}
625
626#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
627pub enum ProbeCost {
628 Trivial,
629 Cheap,
630 WakesHardware,
631}
632
633#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
634pub enum SideEffects {
635 None,
636 Measurement {
638 bytes_max: u32,
639 per: Millis,
640 },
641 Audible,
642 Visible,
643 RemoteStateChange,
644 Disk,
645}
646
647#[derive(Debug, PartialEq, Eq)]
648pub enum ProbeContractError {
649 ReadinessWithSideEffects(ProbeId),
651 SideEffectsOnCadence(ProbeId),
653}
654
655impl ProbeDef {
656 pub fn check_contract(&self) -> Result<(), ProbeContractError> {
658 let benign = matches!(self.side_effects, SideEffects::None | SideEffects::Measurement { .. });
659 if !benign && self.role == ProbeRole::Readiness {
660 return Err(ProbeContractError::ReadinessWithSideEffects(self.id.clone()));
661 }
662 if !benign && self.cadence != Cadence::OnDemand {
663 return Err(ProbeContractError::SideEffectsOnCadence(self.id.clone()));
664 }
665 Ok(())
666 }
667}
668
669#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
670pub enum ProbeResult {
671 Pass,
672 Fail(String),
673 Timeout,
674 Error(String),
675 Abandoned,
680}
681
682#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
684pub struct Evidence {
685 pub probe: ProbeId,
686 pub result: ProbeResult,
687 pub at: Timestamp,
688 pub stale_after: Millis,
689}
690
691impl Evidence {
692 pub fn fresh_at(&self, now: Timestamp) -> bool {
693 now <= self.at.plus(self.stale_after)
694 }
695}
696
697#[cfg(test)]
698mod tests {
699 use super::*;
700
701 #[test]
702 fn unit_id_roundtrip() {
703 for s in [
704 "service:sshd",
705 "device:fs-root",
706 "target:graphical-session",
707 "service@1000:pipewire",
708 "service@1000/c2:pipewire",
709 "service:external:gym-upstream",
710 ] {
711 let id: UnitId = s.parse().unwrap();
712 assert_eq!(id.to_string(), s);
713 }
714 let ext: UnitId = "service:external:gym-upstream".parse().unwrap();
715 assert_eq!(ext.name, "external:gym-upstream");
716 assert!("bogus:x".parse::<UnitId>().is_err());
717 assert!("service:".parse::<UnitId>().is_err());
718 assert!("service@x:y".parse::<UnitId>().is_err());
719 }
720
721 #[test]
722 fn unit_id_keys_json_maps() {
723 let mut m = BTreeMap::new();
724 m.insert(UnitId::service("sshd"), 1);
725 let json = serde_json::to_string(&m).unwrap();
726 assert_eq!(json, r#"{"service:sshd":1}"#);
727 assert_eq!(serde_json::from_str::<BTreeMap<UnitId, i32>>(&json).unwrap(), m);
728 }
729
730 #[test]
731 fn conditions() {
732 let mut atoms = BTreeSet::new();
733 assert!(Condition::Always.holds(&atoms));
734 assert!(!Condition::SessionEstablished { uid: None, seated: false }.holds(&atoms));
735 atoms.insert(Condition::SessionEstablished { uid: Some(1000), seated: false });
736 assert!(Condition::SessionEstablished { uid: None, seated: false }.holds(&atoms));
737 assert!(Condition::SessionEstablished { uid: Some(1000), seated: false }.holds(&atoms));
738 assert!(!Condition::SessionEstablished { uid: Some(1001), seated: false }.holds(&atoms));
739 let any = Condition::Any(vec![
740 Condition::OnBattery,
741 Condition::SessionEstablished { uid: Some(1000), seated: false },
742 ]);
743 assert!(any.holds(&atoms));
744 assert!(!Condition::All(vec![Condition::OnBattery, Condition::Always]).holds(&atoms));
745 }
746
747 #[test]
750 fn only_a_seated_session_expects_a_desktop() {
751 let seat = Condition::SessionEstablished { uid: None, seated: true };
752 let mut atoms: BTreeSet<Condition> = [Condition::SessionEstablished { uid: Some(1000), seated: false }].into();
753 assert!(!seat.holds(&atoms));
754 assert!(Condition::SessionEstablished { uid: None, seated: false }.holds(&atoms));
755 atoms.insert(Condition::SessionEstablished { uid: Some(1002), seated: true });
756 assert!(seat.holds(&atoms));
757 assert!(!Condition::SessionEstablished { uid: Some(1000), seated: true }.holds(&atoms));
758 assert!(Condition::SessionEstablished { uid: Some(1002), seated: true }.holds(&atoms));
759 assert!(Condition::All(vec![Condition::Always, seat]).needs_seat());
760 }
761
762 fn probe(role: ProbeRole, side_effects: SideEffects, cadence: Cadence) -> ProbeDef {
763 ProbeDef {
764 id: ProbeId("p".into()),
765 kind: ProbeKind::SystemdActive,
766 timeout: Millis::secs(10),
767 origin: ProbeOrigin::Canonical,
768 role,
769 cadence,
770 cost: ProbeCost::Trivial,
771 side_effects,
772 establishes: String::new(),
773 }
774 }
775
776 #[test]
777 fn probe_contract() {
778 let m = SideEffects::Measurement { bytes_max: 4096, per: Millis::secs(5) };
779 assert!(probe(ProbeRole::Readiness, m, Cadence::EveryTick).check_contract().is_ok());
780 assert!(probe(ProbeRole::Readiness, SideEffects::Audible, Cadence::OnDemand).check_contract().is_err());
781 assert!(probe(ProbeRole::Diagnostic, SideEffects::Audible, Cadence::EveryTick).check_contract().is_err());
782 assert!(probe(ProbeRole::Diagnostic, SideEffects::Audible, Cadence::OnDemand).check_contract().is_ok());
783 }
784}