1use crate::ids::{ChangeId, GenerationId, Hash, IncidentId, Millis, ModelId, SnapshotId, StorePath, TxnId};
4use crate::pathmap::PathMap;
5use crate::state::{SystemState, UnitStatus};
6use crate::unit::{Actor, Disruption, Override, Recovery, Sensitivities, UnitDefs, UnitId, Until};
7use serde::{Deserialize, Serialize};
8use std::collections::{BTreeMap, BTreeSet};
9use vstd::prelude::*;
10
11#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
15#[serde(untagged)]
16pub enum Value {
17 Bool(bool),
18 Int(i64),
19 Str(String),
20 List(Vec<Value>),
21}
22
23verus! {
26
27#[verifier::external_type_specification]
28#[verifier::external_body]
29#[allow(dead_code)]
30pub struct ExValue(Value);
31
32#[derive(Clone, Debug, Default, PartialEq, Eq)]
34#[derive(Serialize, Deserialize)]
35#[verifier::external_derive]
36pub struct ConfigDiff {
37 pub options: PathMap<(Option<Value>, Option<Value>)>,
38}
39
40#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
43#[derive(Serialize, Deserialize)]
44#[verifier::external_derive]
45pub struct DriverRef {
46 pub name: String,
47 pub module: Option<StorePath>,
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
51#[derive(Serialize, Deserialize)]
52#[verifier::external_derive]
53pub enum Resolution {
54 AsCommitted,
55 AsReverted,
56 ManualRepaired,
57}
58
59#[derive(Clone, Debug, PartialEq, Eq)]
60#[derive(Serialize, Deserialize)]
61#[verifier::external_derive]
62pub enum Transition {
63 RestartUnit(UnitId),
64 StopUnit(UnitId),
65 BindDriver {
66 device: UnitId,
67 driver: DriverRef,
68 },
69 ApplyChange {
71 change: ChangeId,
72 args: BTreeMap<String, Value>,
73 },
74 SwitchGeneration(GenerationId),
75 Revert {
76 to: GenerationId,
77 },
78 Rollback,
80 LoadModel(ModelId),
82 RestoreSnapshot(SnapshotId),
84 CollectGarbage {
85 emergency: bool,
86 },
87 SetRuntimeOption {
88 path: String,
89 value: Value,
90 },
91 SetOverride(Override),
93 LiftOverride(UnitId),
94 StopAndSuppress {
96 unit: UnitId,
97 until: Until,
98 reason: String,
99 },
100 Resolve {
103 txn: TxnId,
104 resolution: Resolution,
105 },
106}
107
108#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
110#[derive(Serialize, Deserialize)]
111#[verifier::external_derive]
112pub enum Tier {
113 Observe,
114 Runtime,
115 Tested,
116 AskFirst,
117}
118
119#[derive(Clone, Debug, PartialEq, Eq)]
120#[derive(Serialize, Deserialize)]
121#[verifier::external_derive]
122pub enum Decision {
123 Allow { tier: Tier, approver: ApproverClass },
124 Reject(RejectReason),
125}
126
127#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
132#[derive(Serialize, Deserialize)]
133#[verifier::external_derive]
134pub enum ApproverClass {
135 AnyApprover,
136 Administrator,
137}
138
139#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
142#[derive(Serialize, Deserialize)]
143#[verifier::external_derive]
144pub enum RejectReason {
145 Forbidden,
146 CoreTarget,
147 ProbeTamper,
148 NotInCatalog,
149 LockedOption,
150 Stale,
151 Unauthorized,
152 Interrupted,
153 NoSpace,
154 Refinement,
155 ApprovalExpired,
156 Precondition,
158 ActivationUnknown,
161 Other(String),
162}
163
164#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
172#[derive(Serialize, Deserialize)]
173#[verifier::external_derive]
174#[serde(from = "DisruptionsRepr")]
175pub struct Disruptions {
176 pub brief: bool,
177 pub session_loss: bool,
178 pub data_interruption: bool,
179 pub connectivity_loss: bool,
180}
181
182#[derive(Clone, Debug, PartialEq, Eq)]
185#[derive(Serialize, Deserialize)]
186#[verifier::external_derive]
187pub struct DerivedFacts {
188 pub affected_units: Vec<UnitId>,
190 pub sensitivity: Sensitivities,
191 pub disruption: Disruptions,
192 pub recovery: Recovery,
193 pub touches_manifest: bool,
194 pub touches_canonical_probe: bool,
196 pub changes_desired: bool,
199 pub touches_core: bool,
200 #[serde(default)]
202 pub below_security_floor: bool,
203 #[serde(default)]
205 pub other_sessions_affected: bool,
206 #[serde(default)]
210 pub affected_sessions: Vec<String>,
211 #[serde(default)]
215 pub restores_unattributed_stop: bool,
216 #[serde(default)]
222 pub restarts_failed_unit: bool,
223 #[serde(default)]
229 pub removes_sensitivity: Sensitivities,
230 #[serde(default)]
233 pub removes: Vec<UnitId>,
234 #[serde(default)]
240 pub removes_each: Vec<Sensitivities>,
241 #[serde(default)]
246 pub restarts_held_unit: bool,
247 pub caller: Actor,
248 pub config_diff: Option<ConfigDiff>,
249}
250
251impl Tier {
252 pub open spec fn rank(self) -> u8 {
254 match self {
255 Tier::Observe => 0,
256 Tier::Runtime => 1,
257 Tier::Tested => 2,
258 Tier::AskFirst => 3,
259 }
260 }
261
262 pub fn rank_of(&self) -> (r: u8)
263 ensures r == self.rank(),
264 {
265 match self {
266 Tier::Observe => 0,
267 Tier::Runtime => 1,
268 Tier::Tested => 2,
269 Tier::AskFirst => 3,
270 }
271 }
272}
273
274#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
276#[verifier::external_derive]
277pub enum RecoveryStrategy {
278 ReobserveOnly,
279 RevertGeneration,
280 UnbindDevice,
281 RestoreSnapshotThenRevert,
282}
283
284#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
285#[verifier::external_derive]
286pub enum Handler {
287 UnitOp,
288 GenerationOp,
289 DeviceOp,
290 SnapshotOp,
291 GcOp,
292 IntentOp,
293}
294
295impl Transition {
296 pub open spec fn handler_spec(self) -> Handler {
298 match self {
299 Transition::RestartUnit(_) | Transition::StopUnit(_) | Transition::SetRuntimeOption { .. } => {
300 Handler::UnitOp
301 },
302 Transition::SetOverride(_) | Transition::LiftOverride(_) | Transition::StopAndSuppress { .. }
303 | Transition::Resolve { .. } => Handler::IntentOp,
304 Transition::CollectGarbage { .. } => Handler::GcOp,
305 Transition::ApplyChange { .. } | Transition::SwitchGeneration(_) | Transition::Revert { .. }
306 | Transition::Rollback | Transition::LoadModel(_) => Handler::GenerationOp,
307 Transition::BindDriver { .. } => Handler::DeviceOp,
308 Transition::RestoreSnapshot(_) => Handler::SnapshotOp,
309 }
310 }
311
312 pub open spec fn recovery_spec(self) -> RecoveryStrategy {
313 match self.handler_spec() {
314 Handler::UnitOp | Handler::IntentOp | Handler::GcOp => RecoveryStrategy::ReobserveOnly,
315 Handler::GenerationOp => RecoveryStrategy::RevertGeneration,
316 Handler::DeviceOp => RecoveryStrategy::UnbindDevice,
317 Handler::SnapshotOp => RecoveryStrategy::RestoreSnapshotThenRevert,
318 }
319 }
320
321 pub open spec fn changes_generation_spec(self) -> bool {
323 self.recovery_spec().reverts_generation()
324 }
325
326 pub fn handler(&self) -> (r: Handler)
327 ensures
328 r == self.handler_spec(),
329 {
330 match self {
331 Transition::RestartUnit(_) | Transition::StopUnit(_) | Transition::SetRuntimeOption { .. } => {
332 Handler::UnitOp
333 },
334 Transition::SetOverride(_) | Transition::LiftOverride(_) | Transition::StopAndSuppress { .. }
335 | Transition::Resolve { .. } => Handler::IntentOp,
336 Transition::CollectGarbage { .. } => Handler::GcOp,
337 Transition::ApplyChange { .. } | Transition::SwitchGeneration(_) | Transition::Revert { .. }
338 | Transition::Rollback | Transition::LoadModel(_) => Handler::GenerationOp,
339 Transition::BindDriver { .. } => Handler::DeviceOp,
340 Transition::RestoreSnapshot(_) => Handler::SnapshotOp,
341 }
342 }
343
344 pub fn recovery(&self) -> (r: RecoveryStrategy)
345 ensures
346 r == self.recovery_spec(),
347 {
348 match self.handler() {
349 Handler::UnitOp | Handler::IntentOp | Handler::GcOp => RecoveryStrategy::ReobserveOnly,
350 Handler::GenerationOp => RecoveryStrategy::RevertGeneration,
351 Handler::DeviceOp => RecoveryStrategy::UnbindDevice,
352 Handler::SnapshotOp => RecoveryStrategy::RestoreSnapshotThenRevert,
353 }
354 }
355
356 pub fn changes_generation(&self) -> (r: bool)
357 ensures
358 r == self.changes_generation_spec(),
359 {
360 match self.recovery() {
361 RecoveryStrategy::RevertGeneration | RecoveryStrategy::RestoreSnapshotThenRevert => true,
362 _ => false,
363 }
364 }
365}
366
367impl RecoveryStrategy {
368 pub open spec fn reverts_generation(self) -> bool {
370 match self {
371 RecoveryStrategy::RevertGeneration | RecoveryStrategy::RestoreSnapshotThenRevert => true,
372 _ => false,
373 }
374 }
375}
376
377} impl Disruptions {
380 pub fn of(d: Disruption) -> Disruptions {
382 let mut s = Disruptions::default();
383 s.insert(d);
384 s
385 }
386
387 pub fn insert(&mut self, d: Disruption) {
388 match d {
389 Disruption::None => {}
390 Disruption::Brief => self.brief = true,
391 Disruption::SessionLoss => self.session_loss = true,
392 Disruption::DataInterruption => self.data_interruption = true,
393 Disruption::ConnectivityLoss => self.connectivity_loss = true,
394 }
395 }
396
397 pub fn contains(&self, d: Disruption) -> bool {
398 match d {
399 Disruption::None => self.is_none(),
400 Disruption::Brief => self.brief,
401 Disruption::SessionLoss => self.session_loss,
402 Disruption::DataInterruption => self.data_interruption,
403 Disruption::ConnectivityLoss => self.connectivity_loss,
404 }
405 }
406
407 pub fn is_none(&self) -> bool {
408 *self == Disruptions::default()
409 }
410
411 pub fn members(&self) -> Vec<Disruption> {
413 [Disruption::Brief, Disruption::SessionLoss, Disruption::DataInterruption, Disruption::ConnectivityLoss]
414 .into_iter()
415 .filter(|d| self.contains(*d))
416 .collect()
417 }
418}
419
420impl From<Disruption> for Disruptions {
421 fn from(d: Disruption) -> Disruptions {
422 Disruptions::of(d)
423 }
424}
425
426#[derive(Deserialize)]
429#[serde(untagged)]
430enum DisruptionsRepr {
431 One(Disruption),
432 Set {
433 #[serde(default)]
434 brief: bool,
435 #[serde(default)]
436 session_loss: bool,
437 #[serde(default)]
438 data_interruption: bool,
439 #[serde(default)]
440 connectivity_loss: bool,
441 },
442}
443
444impl From<DisruptionsRepr> for Disruptions {
445 fn from(r: DisruptionsRepr) -> Disruptions {
446 match r {
447 DisruptionsRepr::One(d) => Disruptions::of(d),
448 DisruptionsRepr::Set { brief, session_loss, data_interruption, connectivity_loss } => {
449 Disruptions { brief, session_loss, data_interruption, connectivity_loss }
450 }
451 }
452 }
453}
454
455pub fn unit_set(units: impl IntoIterator<Item = UnitId>) -> Vec<UnitId> {
457 let set: BTreeSet<UnitId> = units.into_iter().collect();
458 set.into_iter().collect()
459}
460
461impl ConfigDiff {
462 pub fn then(&self, next: &ConfigDiff) -> ConfigDiff {
465 let mut out = self.options.clone();
466 for (path, (before, after)) in &next.options {
467 let first = match out.get(path) {
468 Some((b, _)) => b.clone(),
469 None => before.clone(),
470 };
471 out.insert(path.clone(), (first, after.clone()));
472 }
473 out.retain(|_, (b, a)| b != a);
474 ConfigDiff { options: out }
475 }
476
477 pub fn hash(&self) -> Hash {
478 Hash::of(&serde_json::to_vec(self).expect("diff serializes"))
479 }
480}
481
482#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
484pub struct EvaluatedGeneration {
485 pub source: Hash,
486 pub options: BTreeMap<String, Value>,
487 pub units: UnitDefs,
488 pub user_units: Vec<crate::unit::UnitDef>,
491 pub activation: ActivationPlan,
493 pub locked: BTreeSet<String>,
496 pub core_units: BTreeMap<String, Hash>,
499 #[serde(default)]
501 pub packages: BTreeMap<String, String>,
502 #[serde(default)]
504 pub insecure: BTreeSet<String>,
505 #[serde(default)]
509 pub declarations: BTreeMap<String, Vec<String>>,
510 #[serde(default)]
515 pub inputs: BTreeMap<String, (String, String)>,
516 #[serde(default)]
520 pub services: BTreeMap<String, Sensitivities>,
521}
522
523impl EvaluatedGeneration {
524 pub fn security_fixes(&self, base: &EvaluatedGeneration) -> Vec<String> {
527 base.insecure
528 .iter()
529 .filter(|p| self.packages.contains_key(*p) && !self.insecure.contains(*p))
530 .cloned()
531 .collect()
532 }
533}
534
535#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
536pub struct ActivationPlan {
537 pub start: BTreeSet<UnitId>,
538 pub stop: BTreeSet<UnitId>,
539 pub restart: BTreeSet<UnitId>,
540 pub reload: BTreeSet<UnitId>,
541 pub needs_reboot_to_confirm: bool,
544}
545
546impl ActivationPlan {
547 pub fn boot_only(&self) -> bool {
550 self.needs_reboot_to_confirm
551 && self.start.is_empty()
552 && self.stop.is_empty()
553 && self.restart.is_empty()
554 && self.reload.is_empty()
555 }
556}
557
558pub trait TransitionSpec {
559 fn precondition(&self, s: &SystemState, defs: &UnitDefs) -> bool;
560 fn predict(&self, s: &SystemState, defs: &UnitDefs) -> Expectation;
561 fn derive(&self, s: &SystemState, defs: &UnitDefs, generation: &EvaluatedGeneration, caller: Actor)
562 -> DerivedFacts;
563}
564
565#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
566pub struct Expectation {
567 pub units: BTreeMap<UnitId, UnitStatus>,
568 pub generation: Option<GenerationId>,
569 pub may_change: BTreeSet<UnitId>,
570 pub others_unchanged: bool,
571 pub settle: Settle,
572}
573
574#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
575pub enum Settle {
576 Duration(Millis),
577 Ready { unit: UnitId, timeout: Millis },
578}
579
580#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
581pub struct Proposal {
582 pub observed: Hash,
583 pub transition: Transition,
584 pub expected: Expectation,
586 pub reason: String,
587 pub source: Source,
588}
589
590#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
591pub enum Source {
592 Diagnoser {
593 incident: IncidentId,
594 },
595 Model(ModelId),
596 User,
597 Maintenance,
599 Intent {
601 phrase: String,
602 confidence: crate::unit::Confidence,
603 },
604}
605
606#[cfg(test)]
607mod tests {
608
609 #[test]
612 fn disruptions_read_the_old_single_value() {
613 let old: Disruptions = serde_json::from_str("\"SessionLoss\"").unwrap();
614 assert_eq!(old, Disruptions::of(Disruption::SessionLoss));
615 let none: Disruptions = serde_json::from_str("\"None\"").unwrap();
616 assert!(none.is_none());
617 let mut both = Disruptions::of(Disruption::ConnectivityLoss);
618 both.insert(Disruption::DataInterruption);
619 let back: Disruptions = serde_json::from_str(&serde_json::to_string(&both).unwrap()).unwrap();
620 assert_eq!(back, both);
621 assert_eq!(both.members(), vec![Disruption::DataInterruption, Disruption::ConnectivityLoss]);
622 let partial: Disruptions = serde_json::from_str("{\"brief\":true}").unwrap();
623 assert_eq!(partial, Disruptions::of(Disruption::Brief));
624 }
625 use super::*;
626
627 #[test]
628 fn strategy_table() {
629 let u = UnitId::service("x");
630 assert_eq!(Transition::RestartUnit(u.clone()).recovery(), RecoveryStrategy::ReobserveOnly);
631 assert_eq!(
632 Transition::StopAndSuppress { unit: u.clone(), until: Until::Explicit, reason: String::new() }.recovery(),
633 RecoveryStrategy::ReobserveOnly
634 );
635 assert_eq!(Transition::Rollback.recovery(), RecoveryStrategy::RevertGeneration);
637 assert!(Transition::Rollback.changes_generation());
638 let bind = Transition::BindDriver {
639 device: UnitId::device("d"),
640 driver: DriverRef { name: "snd_hda_intel".into(), module: None },
641 };
642 assert_eq!(bind.recovery(), RecoveryStrategy::UnbindDevice);
643 assert!(!bind.changes_generation());
644 assert!(!Transition::CollectGarbage { emergency: true }.changes_generation());
645 }
646
647 #[test]
648 fn diff_composition() {
649 let d = |p: &str, b: Option<i64>, a: Option<i64>| ConfigDiff {
650 options: [(p.to_owned(), (b.map(Value::Int), a.map(Value::Int)))].into(),
651 };
652 let both = d("x", Some(1), Some(2)).then(&d("x", Some(2), Some(3)));
653 assert_eq!(both, d("x", Some(1), Some(3)));
654 let undone = d("x", Some(1), Some(2)).then(&d("x", Some(2), Some(1)));
655 assert!(undone.options.is_empty());
656 }
657}