1use std::time::Duration;
9
10use serde::Serialize;
11
12use super::model::HOOK_PATH;
13use crate::error::{Error, Result};
14use crate::exec::{ExecEvent, ExecOptions};
15use crate::jobs::RunLog;
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
19#[serde(rename_all = "snake_case")]
20pub enum HookStatus {
21 Absent,
23 Ok,
24 Failed,
25 TimedOut,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
29pub struct HookResult {
30 pub instance: String,
31 pub status: HookStatus,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 pub error: Option<String>,
34}
35
36pub trait HookRunner {
38 fn present(&self, instance: &str) -> Result<bool>;
40 fn run(
42 &self,
43 instance: &str,
44 env: &[(&str, &str)],
45 timeout: Duration,
46 log: &mut RunLog,
47 ) -> Result<i32>;
48}
49
50pub fn run_hooks(
54 r: &dyn HookRunner,
55 instances: &[String],
56 env: &[(&str, &str)],
57 (timeout, required): (Duration, bool),
58 log: &mut RunLog,
59) -> Result<Vec<HookResult>> {
60 let mut out = Vec::new();
61 for inst in instances {
62 let res = one(r, inst, env, timeout, log);
63 if let Some(e) = &res.error {
64 log.line(&format!("isb: pre-snapshot hook in {inst}: {e}"));
65 if required {
66 return Err(Error::invalid(format!(
67 "the pre-snapshot hook in {inst} failed ({e}); hook_required is set, so no snapshot was taken"
68 )));
69 }
70 log.line("isb: continuing without it (hook_required is off)");
71 }
72 out.push(res);
73 }
74 Ok(out)
75}
76
77fn one(
78 r: &dyn HookRunner,
79 inst: &str,
80 env: &[(&str, &str)],
81 timeout: Duration,
82 log: &mut RunLog,
83) -> HookResult {
84 let res = |status, error: Option<String>| HookResult {
85 instance: inst.to_string(),
86 status,
87 error,
88 };
89 match r.present(inst) {
90 Ok(false) => {
91 log.line(&format!("isb: {inst} has no {HOOK_PATH}"));
92 return res(HookStatus::Absent, None);
93 }
94 Ok(true) => {}
95 Err(e) => return res(HookStatus::Failed, Some(format!("looking for it: {e}"))),
96 }
97 log.line(&format!(
98 "isb: running {HOOK_PATH} in {inst} (timeout {timeout:?})"
99 ));
100 match r.run(inst, env, timeout, log) {
101 Ok(0) => {
102 log.line(&format!("isb: {HOOK_PATH} in {inst} done"));
103 res(HookStatus::Ok, None)
104 }
105 Ok(126) => res(
106 HookStatus::Failed,
107 Some(format!("{HOOK_PATH} is not executable")),
108 ),
109 Ok(code) => res(HookStatus::Failed, Some(format!("exited {code}"))),
110 Err(Error::ExecTimeout { .. }) => res(
111 HookStatus::TimedOut,
112 Some(format!("timed out after {timeout:?} (killed)")),
113 ),
114 Err(e) => res(HookStatus::Failed, Some(e.to_string())),
115 }
116}
117
118pub struct IncusHooks<'a>(pub &'a crate::client::Client);
120
121impl HookRunner for IncusHooks<'_> {
122 fn present(&self, instance: &str) -> Result<bool> {
123 Ok(self.0.read_file(instance, HOOK_PATH)?.is_some())
124 }
125
126 fn run(
127 &self,
128 instance: &str,
129 env: &[(&str, &str)],
130 timeout: Duration,
131 log: &mut RunLog,
132 ) -> Result<i32> {
133 let sb = crate::sandbox::Sandbox::get(self.0, instance)?;
134 let mut opts = ExecOptions::default().timeout(timeout).user("0");
135 for (k, v) in env {
136 opts = opts.env(*k, *v);
137 }
138 let mut s = sb.exec_stream([HOOK_PATH], opts)?;
139 while let Some(ev) = s.next_event() {
140 match ev {
141 ExecEvent::Stdout(b) | ExecEvent::Stderr(b) => log.write(&b),
142 }
143 }
144 s.wait()
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use std::collections::BTreeMap;
152
153 struct Fake(BTreeMap<&'static str, Option<std::result::Result<i32, ()>>>);
155
156 impl HookRunner for Fake {
157 fn present(&self, i: &str) -> Result<bool> {
158 Ok(self.0[i].is_some())
159 }
160 fn run(&self, i: &str, env: &[(&str, &str)], t: Duration, log: &mut RunLog) -> Result<i32> {
161 assert_eq!(env, [("ISB_VOLUME", "home")]);
162 log.line("hook output");
163 match self.0[i] {
164 Some(Ok(c)) => Ok(c),
165 _ => Err(Error::ExecTimeout {
166 argv: HOOK_PATH.into(),
167 timeout: t,
168 }),
169 }
170 }
171 }
172
173 fn insts(names: &[&str]) -> Vec<String> {
174 names.iter().map(|s| s.to_string()).collect()
175 }
176
177 #[test]
178 fn hooks_report_and_block_only_when_required() {
179 let f = Fake(BTreeMap::from([
180 ("none", None),
181 ("good", Some(Ok(0))),
182 ("bad", Some(Ok(3))),
183 ("noexec", Some(Ok(126))),
184 ("slow", Some(Err(()))),
185 ]));
186 let env = [("ISB_VOLUME", "home")];
187 let t = Duration::from_secs(2);
188 let mut log = RunLog::sink();
189 let r = run_hooks(
190 &f,
191 &insts(&["none", "good", "bad", "noexec", "slow"]),
192 &env,
193 (t, false),
194 &mut log,
195 )
196 .unwrap();
197 let st: Vec<_> = r.iter().map(|x| x.status.clone()).collect();
198 assert_eq!(
199 st,
200 [
201 HookStatus::Absent,
202 HookStatus::Ok,
203 HookStatus::Failed,
204 HookStatus::Failed,
205 HookStatus::TimedOut
206 ]
207 );
208 assert!(
209 r[4].error
210 .as_deref()
211 .unwrap()
212 .contains("timed out after 2s")
213 );
214 assert!(r[3].error.as_deref().unwrap().contains("not executable"));
215 let e = run_hooks(
217 &f,
218 &insts(&["good", "slow", "bad"]),
219 &env,
220 (t, true),
221 &mut log,
222 )
223 .unwrap_err()
224 .to_string();
225 assert!(e.contains("slow") && e.contains("hook_required"), "{e}");
226 assert_eq!(
228 run_hooks(&f, &insts(&["good", "none"]), &env, (t, true), &mut log)
229 .unwrap()
230 .len(),
231 2
232 );
233 }
234}