use std::time::Duration;
use serde::Serialize;
use super::model::HOOK_PATH;
use crate::error::{Error, Result};
use crate::exec::{ExecEvent, ExecOptions};
use crate::jobs::RunLog;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum HookStatus {
Absent,
Ok,
Failed,
TimedOut,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct HookResult {
pub instance: String,
pub status: HookStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
pub trait HookRunner {
fn present(&self, instance: &str) -> Result<bool>;
fn run(
&self,
instance: &str,
env: &[(&str, &str)],
timeout: Duration,
log: &mut RunLog,
) -> Result<i32>;
}
pub fn run_hooks(
r: &dyn HookRunner,
instances: &[String],
env: &[(&str, &str)],
(timeout, required): (Duration, bool),
log: &mut RunLog,
) -> Result<Vec<HookResult>> {
let mut out = Vec::new();
for inst in instances {
let res = one(r, inst, env, timeout, log);
if let Some(e) = &res.error {
log.line(&format!("isb: pre-snapshot hook in {inst}: {e}"));
if required {
return Err(Error::invalid(format!(
"the pre-snapshot hook in {inst} failed ({e}); hook_required is set, so no snapshot was taken"
)));
}
log.line("isb: continuing without it (hook_required is off)");
}
out.push(res);
}
Ok(out)
}
fn one(
r: &dyn HookRunner,
inst: &str,
env: &[(&str, &str)],
timeout: Duration,
log: &mut RunLog,
) -> HookResult {
let res = |status, error: Option<String>| HookResult {
instance: inst.to_string(),
status,
error,
};
match r.present(inst) {
Ok(false) => {
log.line(&format!("isb: {inst} has no {HOOK_PATH}"));
return res(HookStatus::Absent, None);
}
Ok(true) => {}
Err(e) => return res(HookStatus::Failed, Some(format!("looking for it: {e}"))),
}
log.line(&format!(
"isb: running {HOOK_PATH} in {inst} (timeout {timeout:?})"
));
match r.run(inst, env, timeout, log) {
Ok(0) => {
log.line(&format!("isb: {HOOK_PATH} in {inst} done"));
res(HookStatus::Ok, None)
}
Ok(126) => res(
HookStatus::Failed,
Some(format!("{HOOK_PATH} is not executable")),
),
Ok(code) => res(HookStatus::Failed, Some(format!("exited {code}"))),
Err(Error::ExecTimeout { .. }) => res(
HookStatus::TimedOut,
Some(format!("timed out after {timeout:?} (killed)")),
),
Err(e) => res(HookStatus::Failed, Some(e.to_string())),
}
}
pub struct IncusHooks<'a>(pub &'a crate::client::Client);
impl HookRunner for IncusHooks<'_> {
fn present(&self, instance: &str) -> Result<bool> {
Ok(self.0.read_file(instance, HOOK_PATH)?.is_some())
}
fn run(
&self,
instance: &str,
env: &[(&str, &str)],
timeout: Duration,
log: &mut RunLog,
) -> Result<i32> {
let sb = crate::sandbox::Sandbox::get(self.0, instance)?;
let mut opts = ExecOptions::default().timeout(timeout).user("0");
for (k, v) in env {
opts = opts.env(*k, *v);
}
let mut s = sb.exec_stream([HOOK_PATH], opts)?;
while let Some(ev) = s.next_event() {
match ev {
ExecEvent::Stdout(b) | ExecEvent::Stderr(b) => log.write(&b),
}
}
s.wait()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeMap;
struct Fake(BTreeMap<&'static str, Option<std::result::Result<i32, ()>>>);
impl HookRunner for Fake {
fn present(&self, i: &str) -> Result<bool> {
Ok(self.0[i].is_some())
}
fn run(&self, i: &str, env: &[(&str, &str)], t: Duration, log: &mut RunLog) -> Result<i32> {
assert_eq!(env, [("ISB_VOLUME", "home")]);
log.line("hook output");
match self.0[i] {
Some(Ok(c)) => Ok(c),
_ => Err(Error::ExecTimeout {
argv: HOOK_PATH.into(),
timeout: t,
}),
}
}
}
fn insts(names: &[&str]) -> Vec<String> {
names.iter().map(|s| s.to_string()).collect()
}
#[test]
fn hooks_report_and_block_only_when_required() {
let f = Fake(BTreeMap::from([
("none", None),
("good", Some(Ok(0))),
("bad", Some(Ok(3))),
("noexec", Some(Ok(126))),
("slow", Some(Err(()))),
]));
let env = [("ISB_VOLUME", "home")];
let t = Duration::from_secs(2);
let mut log = RunLog::sink();
let r = run_hooks(
&f,
&insts(&["none", "good", "bad", "noexec", "slow"]),
&env,
(t, false),
&mut log,
)
.unwrap();
let st: Vec<_> = r.iter().map(|x| x.status.clone()).collect();
assert_eq!(
st,
[
HookStatus::Absent,
HookStatus::Ok,
HookStatus::Failed,
HookStatus::Failed,
HookStatus::TimedOut
]
);
assert!(
r[4].error
.as_deref()
.unwrap()
.contains("timed out after 2s")
);
assert!(r[3].error.as_deref().unwrap().contains("not executable"));
let e = run_hooks(
&f,
&insts(&["good", "slow", "bad"]),
&env,
(t, true),
&mut log,
)
.unwrap_err()
.to_string();
assert!(e.contains("slow") && e.contains("hook_required"), "{e}");
assert_eq!(
run_hooks(&f, &insts(&["good", "none"]), &env, (t, true), &mut log)
.unwrap()
.len(),
2
);
}
}