Skip to content

Commit 0ef2ae4

Browse files
authored
Merge pull request #45 from say-paul/disable-healthchecks
add: config to disable healtchecks
2 parents 31ac496 + 22615c6 commit 0ef2ae4

7 files changed

Lines changed: 267 additions & 108 deletions

File tree

etc/greenboot/greenboot.conf

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
11
# Greenboot configuration file
22

33
## Generic
4-
GREENBOOT_MAX_BOOT_ATTEMPTS=3
4+
GREENBOOT_MAX_BOOT_ATTEMPTS=3
5+
6+
7+
### Multiple healthchecks may be skipped by separating
8+
### the script names with spaces.
9+
### NOTE: Script names must be spelled EXACTLY. Typos will result in
10+
### unwanted behavior.
11+
### DISABLED_HEALTHCHECKS=(
12+
### "01_repository_dns_check.sh"
13+
### "02_watchdog.sh"
14+
### )
15+
DISABLED_HEALTHCHECKS=()

src/lib/greenboot.rs

Lines changed: 162 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use anyhow::{Result, bail};
22
use glob::glob;
3+
use std::collections::HashSet;
34
use std::error::Error;
45
use std::path::Path;
56
use std::process::Command;
@@ -8,109 +9,162 @@ use std::process::Command;
89
static GREENBOOT_INSTALL_PATHS: [&str; 2] = ["/usr/lib/greenboot", "/etc/greenboot"];
910

1011
/// runs all the scripts in required.d and wanted.d
11-
pub fn run_diagnostics() -> Result<()> {
12-
let mut required_script_failure: bool = false;
13-
let mut path_exists: bool = false;
12+
pub fn run_diagnostics(skipped: Vec<String>) -> Result<Vec<String>> {
13+
let mut required_script_failure = false;
14+
let mut path_exists = false;
15+
let mut all_skipped = HashSet::new();
16+
17+
// Convert input skipped Vec to HashSet for efficient lookups
18+
let disabled_scripts: HashSet<String> = skipped.clone().into_iter().collect();
19+
20+
// Run required checks
1421
for path in GREENBOOT_INSTALL_PATHS {
15-
let greenboot_required_path = format!("{path}/check/required.d/");
22+
let greenboot_required_path = format!("{}/check/required.d/", path);
1623
if !Path::new(&greenboot_required_path).is_dir() {
17-
log::warn!("skipping test as {greenboot_required_path} is not a dir");
24+
log::warn!("skipping test as {} is not a dir", greenboot_required_path);
1825
continue;
1926
}
2027
path_exists = true;
21-
let errors = run_scripts("required", &greenboot_required_path);
22-
if !errors.is_empty() {
28+
let result = run_scripts("required", &greenboot_required_path, Some(&skipped));
29+
all_skipped.extend(result.skipped);
30+
31+
if !result.errors.is_empty() {
2332
log::error!("required script error:");
24-
errors.iter().for_each(|e| log::error!("{e}"));
25-
if !required_script_failure {
26-
required_script_failure = true;
27-
}
33+
result.errors.iter().for_each(|e| log::error!("{e}"));
34+
required_script_failure = true;
2835
}
2936
}
37+
3038
if !path_exists {
3139
bail!("cannot find any required.d folder");
3240
}
41+
42+
// Run wanted checks
3343
for path in GREENBOOT_INSTALL_PATHS {
34-
let greenboot_wanted_path = format!("{path}/check/wanted.d/");
35-
let errors = run_scripts("wanted", &greenboot_wanted_path);
36-
if !errors.is_empty() {
44+
let greenboot_wanted_path = format!("{}/check/wanted.d/", path);
45+
let result = run_scripts("wanted", &greenboot_wanted_path, Some(&skipped));
46+
all_skipped.extend(result.skipped);
47+
48+
if !result.errors.is_empty() {
3749
log::warn!("wanted script runner error:");
38-
errors.iter().for_each(|e| log::error!("{e}"));
50+
result.errors.iter().for_each(|e| log::error!("{e}"));
3951
}
4052
}
4153

54+
// Check for disabled scripts that weren't found
55+
let missing_disabled: Vec<String> = disabled_scripts
56+
.difference(&all_skipped)
57+
.map(|s| s.to_string()) // Convert &String to String
58+
.collect();
59+
60+
if !missing_disabled.is_empty() {
61+
log::warn!(
62+
"The following disabled scripts were not found in any directory: {:?}",
63+
missing_disabled
64+
);
65+
}
66+
4267
if required_script_failure {
4368
bail!("health-check failed!");
4469
}
45-
Ok(())
70+
Ok(missing_disabled)
4671
}
4772

48-
/// runs all the scripts in red.d when health-check fails
73+
// runs all the scripts in red.d when health-check fails
4974
pub fn run_red() -> Vec<Box<dyn Error>> {
5075
let mut errors = Vec::new();
76+
5177
for path in GREENBOOT_INSTALL_PATHS {
52-
let red_path = format!("{path}/red.d/");
53-
let e = run_scripts("red", &red_path);
54-
if !e.is_empty() {
55-
errors.extend(e);
56-
}
78+
let red_path = format!("{}/red.d/", path);
79+
let result = run_scripts("red", &red_path, None); // Pass None for disabled scripts
80+
errors.extend(result.errors);
5781
}
82+
5883
errors
5984
}
6085

6186
/// runs all the scripts green.d when health-check passes
6287
pub fn run_green() -> Vec<Box<dyn Error>> {
6388
let mut errors = Vec::new();
89+
6490
for path in GREENBOOT_INSTALL_PATHS {
65-
let green_path = format!("{path}/green.d/");
66-
let e = run_scripts("green", &green_path);
67-
if !e.is_empty() {
68-
errors.extend(e);
69-
}
91+
let green_path = format!("{}/green.d/", path);
92+
let result = run_scripts("green", &green_path, None); // Pass None for disabled scripts
93+
errors.extend(result.errors);
7094
}
95+
7196
errors
7297
}
7398

74-
/// takes in a path and runs all the .sh files within the path
75-
/// returns false if any script fails
76-
fn run_scripts(name: &str, path: &str) -> Vec<Box<dyn Error>> {
77-
let mut errors = Vec::new();
78-
let scripts = format!("{path}*.sh");
79-
match glob(&scripts) {
80-
Ok(s) => {
81-
for entry in s.flatten() {
82-
log::info!("running {name} check {}", entry.to_string_lossy());
83-
let output = Command::new("bash").arg("-C").arg(entry.clone()).output();
84-
match output {
85-
Ok(o) => {
86-
if !o.status.success() {
87-
errors.push(Box::new(std::io::Error::new(
88-
std::io::ErrorKind::Other,
89-
format!(
90-
"{name} script {} failed! \n{} \n{}",
91-
entry.to_string_lossy(),
92-
String::from_utf8_lossy(&o.stdout),
93-
String::from_utf8_lossy(&o.stderr)
94-
),
95-
)) as Box<dyn Error>);
96-
} else {
97-
log::info!("{name} script {} success!", entry.to_string_lossy());
98-
}
99-
}
100-
Err(e) => {
101-
errors.push(Box::new(e) as Box<dyn Error>);
102-
}
103-
}
99+
struct ScriptRunResult {
100+
errors: Vec<Box<dyn Error>>,
101+
skipped: Vec<String>,
102+
}
103+
104+
fn run_scripts(name: &str, path: &str, disabled_scripts: Option<&[String]>) -> ScriptRunResult {
105+
let mut result = ScriptRunResult {
106+
errors: Vec::new(),
107+
skipped: Vec::new(),
108+
};
109+
110+
// Handle glob pattern error early
111+
let entries = match glob(&format!("{}*.sh", path)) {
112+
Ok(e) => e,
113+
Err(e) => {
114+
result.errors.push(Box::new(e));
115+
return result;
116+
}
117+
};
118+
119+
for entry in entries.flatten() {
120+
// Process script name
121+
let script_name = match entry.file_name().and_then(|n| n.to_str()) {
122+
Some(name) => name,
123+
None => continue,
124+
};
125+
126+
// Check if script should be skipped
127+
if let Some(disabled) = disabled_scripts {
128+
if disabled.contains(&script_name.to_string()) {
129+
log::info!("Skipping disabled script: {}", script_name);
130+
result.skipped.push(script_name.to_string());
131+
continue;
132+
}
133+
}
134+
135+
log::info!("running {} check {}", name, entry.to_string_lossy());
136+
137+
// Execute script and handle output
138+
let output = Command::new("bash").arg("-C").arg(&entry).output();
139+
140+
match output {
141+
Ok(o) if o.status.success() => {
142+
log::info!("{} script {} success!", name, entry.to_string_lossy());
143+
}
144+
Ok(o) => {
145+
let error_msg = format!(
146+
"{} script {} failed!\n{}\n{}",
147+
name,
148+
entry.to_string_lossy(),
149+
String::from_utf8_lossy(&o.stdout),
150+
String::from_utf8_lossy(&o.stderr)
151+
);
152+
result.errors.push(Box::new(std::io::Error::new(
153+
std::io::ErrorKind::Other,
154+
error_msg,
155+
)));
156+
}
157+
Err(e) => {
158+
result.errors.push(Box::new(e));
104159
}
105160
}
106-
Err(e) => errors.push(Box::new(e) as Box<dyn Error>),
107161
}
108-
errors
162+
163+
result
109164
}
110165

111166
#[cfg(test)]
112167
mod test {
113-
114168
use super::*;
115169
use anyhow::{Context, Result};
116170
use std::fs;
@@ -121,7 +175,7 @@ mod test {
121175
#[test]
122176
fn missing_required_folder() {
123177
assert_eq!(
124-
run_diagnostics().unwrap_err().to_string(),
178+
run_diagnostics(vec![]).unwrap_err().to_string(),
125179
String::from("cannot find any required.d folder")
126180
);
127181
}
@@ -131,7 +185,7 @@ mod test {
131185
setup_folder_structure(true)
132186
.context("Test setup failed")
133187
.unwrap();
134-
let state = run_diagnostics();
188+
let state = run_diagnostics(vec![]);
135189
assert!(state.is_ok());
136190
tear_down().context("Test teardown failed").unwrap();
137191
}
@@ -141,11 +195,44 @@ mod test {
141195
setup_folder_structure(false)
142196
.context("Test setup failed")
143197
.unwrap();
144-
let failed_msg = run_diagnostics().unwrap_err().to_string();
198+
let failed_msg = run_diagnostics(vec![]).unwrap_err().to_string();
145199
assert_eq!(failed_msg, String::from("health-check failed!"));
146200
tear_down().context("Test teardown failed").unwrap();
147201
}
148202

203+
#[test]
204+
fn test_skip_nonexistent_script() {
205+
let nonexistent_script_name = "nonexistent_script.sh".to_string();
206+
setup_folder_structure(true)
207+
.context("Test setup failed")
208+
.unwrap();
209+
210+
// Try to skip a script that doesn't exist
211+
let state = run_diagnostics(vec![nonexistent_script_name.clone()]);
212+
assert!(
213+
state.unwrap().contains(&nonexistent_script_name),
214+
"non existent script names did not match"
215+
);
216+
217+
tear_down().context("Test teardown failed").unwrap();
218+
}
219+
220+
#[test]
221+
fn test_skip_failing_script() {
222+
setup_folder_structure(false)
223+
.context("Test setup failed")
224+
.unwrap();
225+
226+
// Skip the failing script in required.d
227+
let state = run_diagnostics(vec!["failing_script.sh".to_string()]);
228+
assert!(
229+
state.is_ok(),
230+
"Should pass when skipping failing required script"
231+
);
232+
233+
tear_down().context("Test teardown failed").unwrap();
234+
}
235+
149236
fn setup_folder_structure(passing: bool) -> Result<()> {
150237
let required_path = format!("{}/check/required.d", GREENBOOT_INSTALL_PATHS[1]);
151238
let wanted_path = format!("{}/check/wanted.d", GREENBOOT_INSTALL_PATHS[1]);
@@ -154,31 +241,34 @@ mod test {
154241

155242
fs::create_dir_all(&required_path).expect("cannot create folder");
156243
fs::create_dir_all(&wanted_path).expect("cannot create folder");
157-
let _a = fs::copy(
244+
245+
// Create passing script in both required and wanted
246+
fs::copy(
158247
passing_test_scripts,
159248
format!("{}/passing_script.sh", &required_path),
160249
)
161-
.context("unable to copy test assets");
250+
.context("unable to copy passing script to required.d")?;
162251

163-
let _a = fs::copy(
252+
fs::copy(
164253
passing_test_scripts,
165254
format!("{}/passing_script.sh", &wanted_path),
166255
)
167-
.context("unable to copy test assets");
256+
.context("unable to copy passing script to wanted.d")?;
168257

169-
let _a = fs::copy(
258+
// Create failing script in wanted.d
259+
fs::copy(
170260
failing_test_scripts,
171261
format!("{}/failing_script.sh", &wanted_path),
172262
)
173-
.context("unable to copy test assets");
263+
.context("unable to copy failing script to wanted.d")?;
174264

175265
if !passing {
176-
let _a = fs::copy(
266+
// Create failing script in required.d for failure cases
267+
fs::copy(
177268
failing_test_scripts,
178269
format!("{}/failing_script.sh", &required_path),
179270
)
180-
.context("unable to copy test assets");
181-
return Ok(());
271+
.context("unable to copy failing script to required.d")?;
182272
}
183273
Ok(())
184274
}

src/lib/mount.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,13 @@ pub fn remount_boot_rw(mounts_path: &Path) -> Result<(), MountError> {
104104
}
105105
}
106106

107+
/// For testing without actually remounting /mount
107108
#[cfg(feature = "test-remount")]
108109
pub fn remount_boot_rw(_mounts_path: &Path) -> Result<(), MountError> {
109110
// Stubbed for testing
110111
Ok(())
111112
}
112-
113+
/// For testing without actually remounting /mount
113114
#[cfg(feature = "test-remount")]
114115
pub fn remount_boot_ro(_mounts_path: &Path) -> Result<(), MountError> {
115116
Ok(())

0 commit comments

Comments
 (0)