11use anyhow:: { Result , bail} ;
22use glob:: glob;
3+ use std:: collections:: HashSet ;
34use std:: error:: Error ;
45use std:: path:: Path ;
56use std:: process:: Command ;
@@ -8,109 +9,162 @@ use std::process::Command;
89static 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
4974pub 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
6287pub 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) ]
112167mod 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 }
0 commit comments