From 5637a552eaad483d396e19de8c228104499dad71 Mon Sep 17 00:00:00 2001 From: Johan Lundberg Date: Thu, 25 Jun 2026 17:08:49 +0200 Subject: [PATCH] support --exclude for job name --- src/cli.rs | 20 +++++++++- src/jobs_list.rs | 13 +++++- src/main.rs | 2 +- src/modes.rs | 8 ++-- tests/cli_smoke.rs | 99 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 134 insertions(+), 8 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 22e0f58..4278d18 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -36,6 +36,9 @@ pub enum RawMode { }, Check { names: Vec, + /// Names of jobs to exclude from a catch-all check. + #[arg(long, num_args = 1.., value_name = "NAME")] + exclude: Vec, }, Lastlog { names: Vec, @@ -154,7 +157,22 @@ mod tests { fn check_with_names() { let args = RawArgs::parse_from(["scriptherder", "check", "jobA", "jobB"]); match args.mode { - RawMode::Check { names } => assert_eq!(names, vec!["jobA", "jobB"]), + RawMode::Check { names, exclude } => { + assert_eq!(names, vec!["jobA", "jobB"]); + assert!(exclude.is_empty()); + } + _ => panic!("expected Check"), + } + } + + #[test] + fn check_parses_exclude() { + let args = RawArgs::parse_from(["scriptherder", "check", "--exclude", "cosmos", "backup"]); + match args.mode { + RawMode::Check { names, exclude } => { + assert!(names.is_empty()); + assert_eq!(exclude, vec!["cosmos", "backup"]); + } _ => panic!("expected Check"), } } diff --git a/src/jobs_list.rs b/src/jobs_list.rs index 462edf2..522e599 100644 --- a/src/jobs_list.rs +++ b/src/jobs_list.rs @@ -22,6 +22,7 @@ impl JobsList { checkdir: &str, names: &[String], load_not_running: bool, + exclude: &[String], ) -> Result { let mut jobs: Vec = Vec::new(); if let Ok(entries) = std::fs::read_dir(datadir) { @@ -39,6 +40,10 @@ impl JobsList { if !names.is_empty() && names != ["ALL"] && !names.contains(&job.name()) { continue; } + if exclude.contains(&job.name()) { + log::debug!("Excluding {:?} (file {})", job.name(), fname); + continue; + } jobs.push(job); } Err(exc) => { @@ -53,12 +58,12 @@ impl JobsList { } let mut list = JobsList::from_jobs(jobs, load_not_running); if load_not_running { - list.load_not_running(checkdir, names); + list.load_not_running(checkdir, names, exclude); } Ok(list) } - fn load_not_running(&mut self, checkdir: &str, names: &[String]) { + fn load_not_running(&mut self, checkdir: &str, names: &[String], exclude: &[String]) { let present: std::collections::HashSet = self.jobs.iter().map(|j| j.name()).collect(); if let Ok(entries) = std::fs::read_dir(checkdir) { @@ -75,6 +80,10 @@ impl JobsList { if !names.is_empty() && names != ["ALL"] && !names.contains(&name) { continue; } + if exclude.contains(&name) { + log::debug!("Excluding not-running {name:?} (file {fname})"); + continue; + } if !present.contains(&name) { if let Ok(job) = Job::new(&name, vec![]) { self.jobs.push(job); diff --git a/src/main.rs b/src/main.rs index 485a619..b65966f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -99,7 +99,7 @@ fn main() { let code = match &args.mode { RawMode::Wrap { .. } => modes::wrap(&args), RawMode::Ls { names } => modes::ls(&args, names), - RawMode::Check { names } => modes::run_check(&args, names), + RawMode::Check { names, exclude } => modes::run_check(&args, names, exclude), RawMode::Lastlog { names } => modes::lastlog(&args, names, false), RawMode::Lastfaillog { names } => modes::lastlog(&args, names, true), }; diff --git a/src/modes.rs b/src/modes.rs index dbcbaf7..63642f0 100644 --- a/src/modes.rs +++ b/src/modes.rs @@ -77,7 +77,7 @@ pub fn wrap(args: &RawArgs) -> i32 { /// `mode_ls` (../src/scriptherder.py:1153): list saved job states in a table. pub fn ls(args: &RawArgs, names: &[String]) -> i32 { - let jobs = match JobsList::from_dir(&args.datadir, &args.checkdir, names, true) { + let jobs = match JobsList::from_dir(&args.datadir, &args.checkdir, names, true, &[]) { Ok(j) => j, Err(e) => { log::error!("Failed loading jobs: {} ({})", e.reason(), e.filename()); @@ -173,8 +173,8 @@ pub fn ls(args: &RawArgs, names: &[String]) -> i32 { } /// `mode_check` (../src/scriptherder.py:1222): Nagios-style aggregate status. -pub fn run_check(args: &RawArgs, names: &[String]) -> i32 { - let jobs = match JobsList::from_dir(&args.datadir, &args.checkdir, names, true) { +pub fn run_check(args: &RawArgs, names: &[String], exclude: &[String]) -> i32 { + let jobs = match JobsList::from_dir(&args.datadir, &args.checkdir, names, true, exclude) { Ok(j) => j, Err(e) => { println!( @@ -194,7 +194,7 @@ pub fn run_check(args: &RawArgs, names: &[String]) -> i32 { /// `mode_lastlog` (../src/scriptherder.py:1244): print last (or last-failed) output. pub fn lastlog(args: &RawArgs, names: &[String], fail_status: bool) -> i32 { - let jobs = match JobsList::from_dir(&args.datadir, &args.checkdir, names, true) { + let jobs = match JobsList::from_dir(&args.datadir, &args.checkdir, names, true, &[]) { Ok(j) => j, Err(e) => { log::error!("Failed loading jobs: {} ({})", e.reason(), e.filename()); diff --git a/tests/cli_smoke.rs b/tests/cli_smoke.rs index 96e3ee9..39fc88d 100644 --- a/tests/cli_smoke.rs +++ b/tests/cli_smoke.rs @@ -77,3 +77,102 @@ fn lastlog_empty_datadir_no_jobs_exits_1() { assert_eq!(out.status.code(), Some(1)); std::fs::remove_dir_all(&dir).ok(); } + +/// Run `wrap -N -- ` into the given dirs. +fn wrap_job(datadir: &std::path::Path, checkdir: &std::path::Path, name: &str, cmd: &[&str]) { + let w = bin() + .arg("-d") + .arg(datadir) + .arg("--checkdir") + .arg(checkdir) + .arg("wrap") + .arg("-N") + .arg(name) + .arg("--") + .args(cmd) + .output() + .unwrap(); + assert!(w.status.success(), "wrap {name} failed"); +} + +/// `check --exclude` drops a failing job, flipping the aggregate from CRITICAL to OK. +#[test] +fn check_exclude_removes_failing_job() { + let dir = std::env::temp_dir().join(format!("sh_excl1_{}", std::process::id())); + let checkdir = dir.join("check"); + std::fs::create_dir_all(&checkdir).unwrap(); + std::fs::write(checkdir.join("alpha.ini"), "[check]\nok = exit_status=0\n").unwrap(); + std::fs::write(checkdir.join("beta.ini"), "[check]\nok = exit_status=0\n").unwrap(); + wrap_job(&dir, &checkdir, "alpha", &["/bin/true"]); + wrap_job(&dir, &checkdir, "beta", &["/bin/sh", "-c", "exit 1"]); + + // Without exclude: beta is failing → CRITICAL. + let c = bin() + .arg("-d") + .arg(&dir) + .arg("--checkdir") + .arg(&checkdir) + .arg("check") + .output() + .unwrap(); + let stdout = String::from_utf8_lossy(&c.stdout); + assert!(stdout.starts_with("CRITICAL:"), "got: {stdout}"); + assert_eq!(c.status.code(), Some(2)); + + // Excluding beta leaves only alpha (OK). + let c = bin() + .arg("-d") + .arg(&dir) + .arg("--checkdir") + .arg(&checkdir) + .arg("check") + .arg("--exclude") + .arg("beta") + .output() + .unwrap(); + let stdout = String::from_utf8_lossy(&c.stdout); + assert!(stdout.starts_with("OK:"), "got: {stdout}"); + assert_eq!(c.status.code(), Some(0)); + std::fs::remove_dir_all(&dir).ok(); +} + +/// `check --exclude` also drops a not-running job synthesized from a check `.ini`. +#[test] +fn check_exclude_removes_not_running() { + let dir = std::env::temp_dir().join(format!("sh_excl2_{}", std::process::id())); + let checkdir = dir.join("check"); + std::fs::create_dir_all(&checkdir).unwrap(); + std::fs::write(checkdir.join("alpha.ini"), "[check]\nok = exit_status=0\n").unwrap(); + // stale.ini has no corresponding job → load_not_running synthesizes a CRITICAL job. + std::fs::write(checkdir.join("stale.ini"), "[check]\nok = exit_status=0\n").unwrap(); + wrap_job(&dir, &checkdir, "alpha", &["/bin/true"]); + + // Without exclude: stale is not running → CRITICAL. + let c = bin() + .arg("-d") + .arg(&dir) + .arg("--checkdir") + .arg(&checkdir) + .arg("check") + .output() + .unwrap(); + let stdout = String::from_utf8_lossy(&c.stdout); + assert!(stdout.starts_with("CRITICAL:"), "got: {stdout}"); + assert_eq!(c.status.code(), Some(2)); + + // Excluding stale leaves only alpha (OK). + let c = bin() + .arg("-d") + .arg(&dir) + .arg("--checkdir") + .arg(&checkdir) + .arg("check") + .arg("--exclude") + .arg("stale") + .output() + .unwrap(); + let stdout = String::from_utf8_lossy(&c.stdout); + assert!(stdout.starts_with("OK:"), "got: {stdout}"); + assert_eq!(c.status.code(), Some(0)); + std::fs::remove_dir_all(&dir).ok(); +}