sysknife_daemon/actions/
reboot.rs1use super::{command_mechanism, ActionSpec};
25use sysknife_types::RiskLevel;
26
27const REBOOT_REQUIRED_FILE: &str = "/var/run/reboot-required";
33
34const REBOOT_REQUIRED_PKGS_FILE: &str = "/var/run/reboot-required.pkgs";
38
39pub fn specs() -> Vec<ActionSpec> {
45 vec![check_pending_reboot()]
46}
47
48pub fn check_pending_reboot() -> ActionSpec {
65 let script = format!(
72 "if test -f {sentinel}; then cat {sentinel}; cat {pkgs} 2>/dev/null; true; 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#[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_script_uses_ubuntu_dot_pkgs_filename() {
137 let spec = check_pending_reboot();
138 let (_, args) = extract_cmd(&spec);
139 let joined = args.join(" ");
140 assert!(
141 joined.contains("/var/run/reboot-required.pkgs"),
142 "Ubuntu writes reboot-required.pkgs, not the hyphen form: {joined}"
143 );
144 assert!(
145 !joined.contains("reboot-required-pkgs"),
146 "hyphen form is not a file Ubuntu writes: {joined}"
147 );
148 }
149
150 fn run_pending_reboot_script(sentinel: Option<&str>, pkgs: Option<&str>) -> (i32, String) {
151 let dir = tempfile::tempdir().unwrap();
152 let sentinel_path = dir.path().join("reboot-required");
153 let pkgs_path = dir.path().join("reboot-required.pkgs");
154 if let Some(body) = sentinel {
155 std::fs::write(&sentinel_path, body).unwrap();
156 }
157 if let Some(body) = pkgs {
158 std::fs::write(&pkgs_path, body).unwrap();
159 }
160
161 let spec = check_pending_reboot();
162 let (_, args) = extract_cmd(&spec);
163 assert_eq!(args.first().map(String::as_str), Some("-c"));
164 let script = args[1]
167 .replace(REBOOT_REQUIRED_PKGS_FILE, &pkgs_path.to_string_lossy())
168 .replace(REBOOT_REQUIRED_FILE, &sentinel_path.to_string_lossy());
169
170 let output = std::process::Command::new("bash")
171 .args(["-c", &script])
172 .output()
173 .expect("bash");
174 (
175 output.status.code().unwrap_or(-1),
176 String::from_utf8_lossy(&output.stdout).into_owned(),
177 )
178 }
179
180 #[test]
181 fn pending_reboot_without_sentinel_reports_no_reboot() {
182 let (code, stdout) = run_pending_reboot_script(None, None);
183 assert_eq!(code, 0, "stdout={stdout:?}");
184 assert_eq!(stdout.trim(), "No reboot required.");
185 }
186
187 #[test]
188 fn pending_reboot_with_sentinel_and_pkgs_lists_both() {
189 let (code, stdout) = run_pending_reboot_script(
190 Some("*** System restart required ***\n"),
191 Some("linux-image-6.8.0-40-generic\n"),
192 );
193 assert_eq!(code, 0, "stdout={stdout:?}");
194 assert!(
195 stdout.contains("*** System restart required ***"),
196 "stdout={stdout:?}"
197 );
198 assert!(
199 stdout.contains("linux-image-6.8.0-40-generic"),
200 "stdout={stdout:?}"
201 );
202 }
203
204 #[test]
205 fn pending_reboot_with_sentinel_only_is_not_an_error() {
206 let (code, stdout) =
207 run_pending_reboot_script(Some("*** System restart required ***\n"), None);
208 assert_eq!(
209 code, 0,
210 "a missing packages file must not fail the action; stdout={stdout:?}"
211 );
212 assert!(
213 stdout.contains("*** System restart required ***"),
214 "stdout={stdout:?}"
215 );
216 }
217
218 #[test]
219 fn check_pending_reboot_risk_is_low() {
220 assert_eq!(check_pending_reboot().risk_level, RiskLevel::Low);
221 }
222
223 #[test]
224 fn check_pending_reboot_no_reboot_no_rollback() {
225 let spec = check_pending_reboot();
226 assert!(!spec.reboot_required);
227 assert!(!spec.rollback_available);
228 }
229
230 #[test]
231 fn specs_covers_check_pending_reboot() {
232 let spec_names: Vec<&str> = specs().iter().map(|s| s.action_name).collect();
233 assert!(
234 spec_names.contains(&"CheckPendingReboot"),
235 "specs() missing CheckPendingReboot"
236 );
237 }
238}