Skip to main content

sysknife_daemon/actions/
reboot.rs

1//! Pending-reboot detection action (Ubuntu / Debian).
2//!
3//! ## CheckPendingReboot
4//!
5//! Checks whether a reboot is pending by inspecting
6//! `/var/run/reboot-required`.  When a kernel or glibc update is installed via
7//! `apt`, the installer touches that file.  If the file exists this action also
8//! cats `/var/run/reboot-required-pkgs` (which lists the packages that require
9//! the reboot) so the operator sees the full picture in one step.
10//!
11//! ### Why Ubuntu-only?
12//!
13//! On Fedora/Silverblue the equivalent information is surfaced through
14//! `rpm-ostree status --json` (field `deployments[0].staged`).  That path is
15//! already covered by the existing `GetPendingUpdates` action in the Fedora
16//! action catalogue.  Adding a cross-distro `CheckPendingReboot` action would
17//! require runtime distro detection inside the executor — a 50-line refactor
18//! with no architectural precedent in the codebase.  Path (b) from the spec
19//! was therefore chosen: `CheckPendingReboot` covers Debian/Ubuntu, and
20//! Fedora operators use `GetPendingUpdates`.  The prompt places
21//! `CheckPendingReboot` in `DEBIAN_RISK_TABLES` and notes the Fedora
22//! equivalent in `DEBIAN_SELECTION_RULES`.
23
24use super::{command_mechanism, ActionSpec};
25use sysknife_types::RiskLevel;
26
27// ---------------------------------------------------------------------------
28// Constants
29// ---------------------------------------------------------------------------
30
31/// Sentinel file written by apt/dpkg when a reboot is required.
32const REBOOT_REQUIRED_FILE: &str = "/var/run/reboot-required";
33
34/// Optional file listing the packages that triggered the reboot requirement.
35const REBOOT_REQUIRED_PKGS_FILE: &str = "/var/run/reboot-required-pkgs";
36
37// ---------------------------------------------------------------------------
38// specs() — for action_consistency tests
39// ---------------------------------------------------------------------------
40
41/// Return one representative `ActionSpec` for this module.
42pub fn specs() -> Vec<ActionSpec> {
43    vec![check_pending_reboot()]
44}
45
46// ---------------------------------------------------------------------------
47// Action constructor
48// ---------------------------------------------------------------------------
49
50/// Check whether a system reboot is pending on a Debian/Ubuntu host.
51///
52/// Risk: Low. Read-only file inspection; no system changes.
53///
54/// Exit-code semantics depend on which script branch fires:
55/// - **No `/var/run/reboot-required`**: echoes "No reboot required." and
56///   exits 0.
57/// - **Sentinel exists, packages file readable**: prints the sentinel
58///   contents plus `/var/run/reboot-required.pkgs`; exits 0.
59/// - **Sentinel exists but packages file is unreadable**: prints the
60///   sentinel; the inner `cat $pkgs 2>/dev/null` suppresses stderr but
61///   the script's overall exit code is the last command's status, which
62///   can be non-zero. Stdout still carries the human-readable status.
63///
64/// Callers should treat stdout as authoritative and not rely on a clean
65/// exit code as the "no reboot needed" signal.
66pub fn check_pending_reboot() -> ActionSpec {
67    // `test -f` returns 1 when the file is absent; the shell fragment treats
68    // that as "no reboot needed" and echoes a human-readable message instead
69    // of failing the whole action.  `cat pkgs` may silently fail if the
70    // packages file does not exist — the `2>/dev/null` suppresses that.
71    let script = format!(
72        "if test -f {sentinel}; then cat {sentinel}; cat {pkgs} 2>/dev/null; else echo 'No reboot required.'; fi",
73        sentinel = REBOOT_REQUIRED_FILE,
74        pkgs = REBOOT_REQUIRED_PKGS_FILE,
75    );
76    ActionSpec {
77        action_name: "CheckPendingReboot",
78        mechanism: command_mechanism("bash", ["-c", &script]),
79        risk_level: RiskLevel::Low,
80        reboot_required: false,
81        rollback_available: false,
82    }
83}
84
85// ---------------------------------------------------------------------------
86// Tests
87// ---------------------------------------------------------------------------
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use crate::actions::ActionMechanism;
93
94    fn extract_cmd(spec: &ActionSpec) -> (&'static str, Vec<String>) {
95        match &spec.mechanism {
96            ActionMechanism::Command { program, args } => (*program, args.clone()),
97            _ => panic!("expected Command mechanism"),
98        }
99    }
100
101    #[test]
102    fn check_pending_reboot_action_name() {
103        assert_eq!(check_pending_reboot().action_name, "CheckPendingReboot");
104    }
105
106    #[test]
107    fn check_pending_reboot_uses_bash() {
108        let spec = check_pending_reboot();
109        let (prog, _) = extract_cmd(&spec);
110        assert_eq!(prog, "bash");
111    }
112
113    #[test]
114    fn check_pending_reboot_script_references_sentinel_file() {
115        let spec = check_pending_reboot();
116        let (_, args) = extract_cmd(&spec);
117        let joined = args.join(" ");
118        assert!(
119            joined.contains(REBOOT_REQUIRED_FILE),
120            "missing sentinel path in script: {joined}"
121        );
122    }
123
124    #[test]
125    fn check_pending_reboot_script_references_pkgs_file() {
126        let spec = check_pending_reboot();
127        let (_, args) = extract_cmd(&spec);
128        let joined = args.join(" ");
129        assert!(
130            joined.contains(REBOOT_REQUIRED_PKGS_FILE),
131            "missing pkgs path in script: {joined}"
132        );
133    }
134
135    #[test]
136    fn check_pending_reboot_risk_is_low() {
137        assert_eq!(check_pending_reboot().risk_level, RiskLevel::Low);
138    }
139
140    #[test]
141    fn check_pending_reboot_no_reboot_no_rollback() {
142        let spec = check_pending_reboot();
143        assert!(!spec.reboot_required);
144        assert!(!spec.rollback_available);
145    }
146
147    #[test]
148    fn specs_covers_check_pending_reboot() {
149        let spec_names: Vec<&str> = specs().iter().map(|s| s.action_name).collect();
150        assert!(
151            spec_names.contains(&"CheckPendingReboot"),
152            "specs() missing CheckPendingReboot"
153        );
154    }
155}