Skip to main content

sysknife_daemon/actions/
livepatch.rs

1//! Canonical Livepatch status action (Ubuntu Pro).
2//!
3//! Canonical Livepatch applies kernel security patches without a reboot.
4//! It requires:
5//!
6//! 1. The `canonical-livepatch` binary (`apt install canonical-livepatch`).
7//! 2. An Ubuntu Pro subscription and an attached Livepatch token.
8//!
9//! If the binary is not installed, the OS returns "command not found" (exit
10//! code 127). The daemon surfaces this error to the user as-is; no special
11//! handling is needed.
12//!
13//! ## Risk classification
14//!
15//! `LivepatchStatus` is read-only — Low / Observer.
16
17use super::{command_mechanism, ActionSpec};
18use sysknife_types::RiskLevel;
19
20// ---------------------------------------------------------------------------
21// specs() — for action_consistency tests
22// ---------------------------------------------------------------------------
23
24/// Return one representative `ActionSpec` per livepatch action name.
25pub fn specs() -> Vec<ActionSpec> {
26    vec![livepatch_status()]
27}
28
29// ---------------------------------------------------------------------------
30// Action constructors
31// ---------------------------------------------------------------------------
32
33/// Show Canonical Livepatch status (`sudo canonical-livepatch status --verbose`).
34///
35/// Risk: Low / Observer. Read-only; shows the patch state for the running
36/// kernel. Requires the `canonical-livepatch` binary and an Ubuntu Pro
37/// subscription. If the binary is not installed, the command exits with
38/// "command not found" (exit 127) which is surfaced to the user.
39pub fn livepatch_status() -> ActionSpec {
40    ActionSpec {
41        action_name: "LivepatchStatus",
42        mechanism: command_mechanism("sudo", ["canonical-livepatch", "status", "--verbose"]),
43        risk_level: RiskLevel::Low,
44        reboot_required: false,
45        rollback_available: false,
46    }
47}
48
49// ---------------------------------------------------------------------------
50// Tests
51// ---------------------------------------------------------------------------
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56    use crate::actions::ActionMechanism;
57
58    fn extract_cmd(spec: &ActionSpec) -> (&'static str, Vec<&str>) {
59        match &spec.mechanism {
60            ActionMechanism::Command { program, args } => {
61                (*program, args.iter().map(String::as_str).collect())
62            }
63            _ => panic!("expected Command mechanism"),
64        }
65    }
66
67    #[test]
68    fn livepatch_status_action_name() {
69        assert_eq!(livepatch_status().action_name, "LivepatchStatus");
70    }
71
72    #[test]
73    fn livepatch_status_risk_low() {
74        assert_eq!(livepatch_status().risk_level, RiskLevel::Low);
75    }
76
77    #[test]
78    fn livepatch_status_no_reboot() {
79        assert!(!livepatch_status().reboot_required);
80    }
81
82    #[test]
83    fn livepatch_status_no_rollback() {
84        assert!(!livepatch_status().rollback_available);
85    }
86
87    #[test]
88    fn livepatch_status_argv() {
89        let spec = livepatch_status();
90        let (prog, args) = extract_cmd(&spec);
91        assert_eq!(prog, "sudo");
92        assert!(args.contains(&"canonical-livepatch"));
93        assert!(args.contains(&"status"));
94        assert!(args.contains(&"--verbose"));
95    }
96
97    #[test]
98    fn specs_covers_all_action_names() {
99        let expected = ["LivepatchStatus"];
100        let spec_names: Vec<&str> = specs().iter().map(|s| s.action_name).collect();
101        for name in &expected {
102            assert!(spec_names.contains(name), "specs() missing {name}");
103        }
104    }
105}