]>
Commit | Line | Data |
---|---|---|
7f64a620 TL |
1 | use std::io::Write; |
2 | use std::path::Path; | |
3 | ||
4 | use anyhow::{format_err, Error}; | |
5 | use regex::Regex; | |
6 | use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; | |
7 | ||
8 | use proxmox_apt::repositories::{self, APTRepositoryFile, APTRepositoryPackageType}; | |
9 | use proxmox_backup::api2::node::apt; | |
10 | ||
11 | const OLD_SUITE: &str = "bullseye"; | |
12 | const NEW_SUITE: &str = "bookworm"; | |
13 | const PROXMOX_BACKUP_META: &str = "proxmox-backup"; | |
14 | const MIN_PBS_MAJOR: u8 = 2; | |
15 | const MIN_PBS_MINOR: u8 = 4; | |
16 | const MIN_PBS_PKGREL: u8 = 1; | |
17 | ||
18 | fn main() -> Result<(), Error> { | |
19 | let mut checker = Checker::new(); | |
20 | checker.check_pbs_packages()?; | |
21 | checker.check_misc()?; | |
22 | checker.summary()?; | |
23 | Ok(()) | |
24 | } | |
25 | ||
26 | struct Checker { | |
27 | output: ConsoleOutput, | |
28 | upgraded: bool, | |
29 | } | |
30 | ||
31 | impl Checker { | |
32 | pub fn new() -> Self { | |
33 | Self { | |
34 | output: ConsoleOutput::new(), | |
35 | upgraded: false, | |
36 | } | |
37 | } | |
38 | ||
39 | pub fn check_pbs_packages(&mut self) -> Result<(), Error> { | |
40 | self.output | |
41 | .print_header("CHECKING VERSION INFORMATION FOR PBS PACKAGES")?; | |
42 | ||
43 | self.check_upgradable_packages()?; | |
44 | let pkg_versions = apt::get_versions()?; | |
45 | self.check_meta_package_version(&pkg_versions)?; | |
46 | self.check_kernel_compat(&pkg_versions)?; | |
47 | Ok(()) | |
48 | } | |
49 | ||
50 | fn check_upgradable_packages(&mut self) -> Result<(), Error> { | |
51 | self.output.log_info("Checking for package updates..")?; | |
52 | ||
53 | let result = Self::get_upgradable_packages(); | |
54 | match result { | |
55 | Err(err) => { | |
56 | self.output.log_warn(format!("{err}"))?; | |
57 | self.output | |
58 | .log_fail("unable to retrieve list of package updates!")?; | |
59 | } | |
60 | Ok(cache) => { | |
61 | if cache.package_status.is_empty() { | |
62 | self.output.log_pass("all packages up-to-date")?; | |
63 | } else { | |
64 | let pkgs = cache | |
65 | .package_status | |
66 | .iter() | |
67 | .map(|pkg| pkg.package.clone()) | |
68 | .collect::<Vec<String>>() | |
69 | .join(", "); | |
70 | self.output.log_warn(format!( | |
71 | "updates for the following packages are available:\n {pkgs}", | |
72 | ))?; | |
73 | } | |
74 | } | |
75 | } | |
76 | Ok(()) | |
77 | } | |
78 | ||
79 | fn check_meta_package_version( | |
80 | &mut self, | |
81 | pkg_versions: &[pbs_api_types::APTUpdateInfo], | |
82 | ) -> Result<(), Error> { | |
83 | self.output | |
84 | .log_info("Checking proxmox backup server package version..")?; | |
85 | ||
86 | let pbs_meta_pkg = pkg_versions | |
87 | .iter() | |
88 | .find(|pkg| pkg.package.as_str() == PROXMOX_BACKUP_META); | |
89 | ||
90 | if let Some(pbs_meta_pkg) = pbs_meta_pkg { | |
91 | let pkg_version = Regex::new(r"^(\d+)\.(\d+)[.-](\d+)")?; | |
92 | let captures = pkg_version.captures(&pbs_meta_pkg.old_version); | |
93 | if let Some(captures) = captures { | |
94 | let maj = Self::extract_version_from_captures(1, &captures)?; | |
95 | let min = Self::extract_version_from_captures(2, &captures)?; | |
96 | let pkgrel = Self::extract_version_from_captures(3, &captures)?; | |
97 | ||
98 | if maj > MIN_PBS_MAJOR { | |
99 | self.output | |
100 | .log_pass(format!("Already upgraded to Proxmox Backup Server {maj}"))?; | |
101 | self.upgraded = true; | |
102 | } else if maj >= MIN_PBS_MAJOR && min >= MIN_PBS_MINOR && pkgrel >= MIN_PBS_PKGREL { | |
103 | self.output.log_pass(format!( | |
104 | "'{}' has version >= {}.{}-{}", | |
105 | PROXMOX_BACKUP_META, MIN_PBS_MAJOR, MIN_PBS_MINOR, MIN_PBS_PKGREL, | |
106 | ))?; | |
107 | } else { | |
108 | self.output.log_fail(format!( | |
109 | "'{}' package is too old, please upgrade to >= {}.{}-{}", | |
110 | PROXMOX_BACKUP_META, MIN_PBS_MAJOR, MIN_PBS_MINOR, MIN_PBS_PKGREL, | |
111 | ))?; | |
112 | } | |
113 | } else { | |
114 | self.output.log_fail(format!( | |
115 | "could not match the '{PROXMOX_BACKUP_META}' package version, \ | |
116 | is it installed?", | |
117 | ))?; | |
118 | } | |
119 | } else { | |
120 | self.output | |
121 | .log_fail(format!("'{PROXMOX_BACKUP_META}' package not found!"))?; | |
122 | } | |
123 | Ok(()) | |
124 | } | |
125 | ||
126 | fn check_kernel_compat( | |
127 | &mut self, | |
128 | pkg_versions: &[pbs_api_types::APTUpdateInfo], | |
129 | ) -> Result<(), Error> { | |
130 | self.output.log_info("Check running kernel version..")?; | |
131 | let (krunning, kinstalled) = if self.upgraded { | |
132 | ( | |
133 | Regex::new(r"^6\.(?:2\.(?:[2-9]\d+|1[6-8]|1\d\d+)|5)[^~]*$")?, | |
1f4ae5c7 | 134 | "proxmox-kernel-6.2", |
7f64a620 TL |
135 | ) |
136 | } else { | |
137 | (Regex::new(r"^(?:5\.(?:13|15)|6\.2)")?, "pve-kernel-5.15") | |
138 | }; | |
139 | ||
140 | let output = std::process::Command::new("uname").arg("-r").output(); | |
141 | match output { | |
142 | Err(_err) => self | |
143 | .output | |
144 | .log_fail("unable to determine running kernel version.")?, | |
145 | Ok(ret) => { | |
146 | let running_version = std::str::from_utf8(&ret.stdout[..ret.stdout.len() - 1])?; | |
147 | if krunning.is_match(running_version) { | |
148 | if self.upgraded { | |
149 | self.output.log_pass(format!( | |
150 | "running new kernel '{running_version}' after upgrade." | |
151 | ))?; | |
152 | } else { | |
153 | self.output.log_pass(format!( | |
154 | "running kernel '{running_version}' is considered suitable for \ | |
155 | upgrade." | |
156 | ))?; | |
157 | } | |
158 | } else { | |
159 | let installed_kernel = pkg_versions | |
160 | .iter() | |
161 | .find(|pkg| pkg.package.as_str() == kinstalled); | |
162 | if installed_kernel.is_some() { | |
163 | self.output.log_warn(format!( | |
164 | "a suitable kernel '{kinstalled}' is installed, but an \ | |
165 | unsuitable '{running_version}' is booted, missing reboot?!", | |
166 | ))?; | |
167 | } else { | |
168 | self.output.log_warn(format!( | |
169 | "unexpected running and installed kernel '{running_version}'.", | |
170 | ))?; | |
171 | } | |
172 | } | |
173 | } | |
174 | } | |
175 | Ok(()) | |
176 | } | |
177 | ||
178 | fn extract_version_from_captures( | |
179 | index: usize, | |
180 | captures: ®ex::Captures, | |
181 | ) -> Result<u8, Error> { | |
182 | if let Some(capture) = captures.get(index) { | |
183 | let val = capture.as_str().parse::<u8>()?; | |
184 | Ok(val) | |
185 | } else { | |
186 | Ok(0) | |
187 | } | |
188 | } | |
189 | ||
190 | fn check_bootloader(&mut self) -> Result<(), Error> { | |
191 | self.output | |
192 | .log_info("Checking bootloader configuration...")?; | |
193 | ||
194 | // PBS packages version check needs to be run before | |
195 | if !self.upgraded { | |
196 | self.output | |
197 | .log_skip("not yet upgraded, no need to check the presence of systemd-boot")?; | |
198 | } | |
199 | ||
200 | if !Path::new("/etc/kernel/proxmox-boot-uuids").is_file() { | |
201 | self.output | |
202 | .log_skip("proxmox-boot-tool not used for bootloader configuration")?; | |
203 | return Ok(()); | |
204 | } | |
205 | ||
206 | if !Path::new("/sys/firmware/efi").is_file() { | |
207 | self.output | |
208 | .log_skip("System booted in legacy-mode - no need for systemd-boot")?; | |
209 | return Ok(()); | |
210 | } | |
211 | ||
212 | if Path::new("/usr/share/doc/systemd-boot/changelog.Debian.gz").is_file() { | |
213 | self.output.log_pass("systemd-boot is installed")?; | |
214 | } else { | |
215 | self.output.log_warn( | |
216 | "proxmox-boot-tool is used for bootloader configuration in uefi mode \ | |
217 | but the separate systemd-boot package, existing in Debian Bookworm \ | |
218 | is not installed.\n\ | |
219 | initializing new ESPs will not work unitl the package is installed.", | |
220 | )?; | |
221 | } | |
222 | Ok(()) | |
223 | } | |
224 | ||
225 | fn check_apt_repos(&mut self) -> Result<(), Error> { | |
226 | self.output | |
227 | .log_info("Checking for package repository suite mismatches..")?; | |
228 | ||
229 | let mut strange_suite = false; | |
230 | let mut mismatches = Vec::new(); | |
231 | let mut found_suite: Option<(String, String)> = None; | |
232 | ||
233 | let (repo_files, _repo_errors, _digest) = repositories::repositories()?; | |
234 | for repo_file in repo_files { | |
235 | self.check_repo_file( | |
236 | &mut found_suite, | |
237 | &mut mismatches, | |
238 | &mut strange_suite, | |
239 | repo_file, | |
240 | )?; | |
241 | } | |
242 | ||
243 | match (mismatches.is_empty(), strange_suite) { | |
244 | (true, false) => self.output.log_pass("found no suite mismatch")?, | |
245 | (true, true) => self | |
246 | .output | |
247 | .log_notice("found no suite mismatches, but found at least one strange suite")?, | |
248 | (false, _) => { | |
249 | let mut message = String::from( | |
250 | "Found mixed old and new packages repository suites, fix before upgrading!\ | |
251 | \n Mismatches:", | |
252 | ); | |
253 | for (suite, location) in mismatches.iter() { | |
254 | message.push_str( | |
255 | format!("\n found suite '{suite}' at '{location}'").as_str(), | |
256 | ); | |
257 | } | |
258 | message.push('\n'); | |
259 | self.output.log_fail(message)? | |
260 | } | |
261 | } | |
262 | ||
263 | Ok(()) | |
264 | } | |
265 | ||
266 | pub fn check_misc(&mut self) -> Result<(), Error> { | |
267 | self.output.print_header("MISCELLANEOUS CHECKS")?; | |
268 | self.check_pbs_services()?; | |
269 | self.check_time_sync()?; | |
270 | self.check_apt_repos()?; | |
271 | self.check_bootloader()?; | |
272 | Ok(()) | |
273 | } | |
274 | ||
275 | pub fn summary(&mut self) -> Result<(), Error> { | |
276 | self.output.print_summary() | |
277 | } | |
278 | ||
279 | fn check_repo_file( | |
280 | &mut self, | |
281 | found_suite: &mut Option<(String, String)>, | |
282 | mismatches: &mut Vec<(String, String)>, | |
283 | strange_suite: &mut bool, | |
284 | repo_file: APTRepositoryFile, | |
285 | ) -> Result<(), Error> { | |
286 | for repo in repo_file.repositories { | |
287 | if !repo.enabled || repo.types == [APTRepositoryPackageType::DebSrc] { | |
288 | continue; | |
289 | } | |
290 | for suite in &repo.suites { | |
291 | let suite = match suite.find(&['-', '/'][..]) { | |
292 | Some(n) => &suite[0..n], | |
293 | None => suite, | |
294 | }; | |
295 | ||
296 | if suite != OLD_SUITE && suite != NEW_SUITE { | |
297 | let location = repo_file.path.clone().unwrap_or_default(); | |
298 | self.output.log_notice(format!( | |
299 | "found unusual suite '{suite}', neither old '{OLD_SUITE}' nor new \ | |
300 | '{NEW_SUITE}'..\n Affected file {location}\n Please \ | |
301 | assure this is shipping compatible packages for the upgrade!" | |
302 | ))?; | |
303 | *strange_suite = true; | |
304 | continue; | |
305 | } | |
306 | ||
307 | if let Some((ref current_suite, ref current_location)) = found_suite { | |
308 | let location = repo_file.path.clone().unwrap_or_default(); | |
309 | if suite != current_suite { | |
310 | if mismatches.is_empty() { | |
311 | mismatches.push((current_suite.clone(), current_location.clone())); | |
312 | mismatches.push((suite.to_string(), location)); | |
313 | } else { | |
314 | mismatches.push((suite.to_string(), location)); | |
315 | } | |
316 | } | |
317 | } else { | |
318 | let location = repo_file.path.clone().unwrap_or_default(); | |
319 | *found_suite = Some((suite.to_string(), location)); | |
320 | } | |
321 | } | |
322 | } | |
323 | Ok(()) | |
324 | } | |
325 | ||
326 | fn get_systemd_unit_state( | |
327 | &mut self, | |
328 | unit: &str, | |
329 | ) -> Result<(SystemdUnitState, SystemdUnitState), Error> { | |
330 | let output = std::process::Command::new("systemctl") | |
331 | .arg("is-enabled") | |
332 | .arg(unit) | |
333 | .output() | |
334 | .map_err(|err| format_err!("failed to execute - {err}"))?; | |
335 | ||
336 | let enabled_state = match output.stdout.as_slice() { | |
337 | b"enabled\n" => SystemdUnitState::Enabled, | |
338 | b"disabled\n" => SystemdUnitState::Disabled, | |
339 | _ => SystemdUnitState::Unknown, | |
340 | }; | |
341 | ||
342 | let output = std::process::Command::new("systemctl") | |
343 | .arg("is-active") | |
344 | .arg(unit) | |
345 | .output() | |
346 | .map_err(|err| format_err!("failed to execute - {err}"))?; | |
347 | ||
348 | let active_state = match output.stdout.as_slice() { | |
349 | b"active\n" => SystemdUnitState::Active, | |
350 | b"inactive\n" => SystemdUnitState::Inactive, | |
351 | b"failed\n" => SystemdUnitState::Failed, | |
352 | _ => SystemdUnitState::Unknown, | |
353 | }; | |
354 | Ok((enabled_state, active_state)) | |
355 | } | |
356 | ||
357 | fn check_pbs_services(&mut self) -> Result<(), Error> { | |
358 | self.output.log_info("Checking PBS daemon services..")?; | |
359 | ||
360 | for service in ["proxmox-backup.service", "proxmox-backup-proxy.service"] { | |
361 | match self.get_systemd_unit_state(service)? { | |
362 | (_, SystemdUnitState::Active) => { | |
363 | self.output | |
364 | .log_pass(format!("systemd unit '{service}' is in state 'active'"))?; | |
365 | } | |
366 | (_, SystemdUnitState::Inactive) => { | |
367 | self.output.log_fail(format!( | |
368 | "systemd unit '{service}' is in state 'inactive'\ | |
369 | \n Please check the service for errors and start it.", | |
370 | ))?; | |
371 | } | |
372 | (_, SystemdUnitState::Failed) => { | |
373 | self.output.log_fail(format!( | |
374 | "systemd unit '{service}' is in state 'failed'\ | |
375 | \n Please check the service for errors and start it.", | |
376 | ))?; | |
377 | } | |
378 | (_, _) => { | |
379 | self.output.log_fail(format!( | |
380 | "systemd unit '{service}' is not in state 'active'\ | |
381 | \n Please check the service for errors and start it.", | |
382 | ))?; | |
383 | } | |
384 | } | |
385 | } | |
386 | Ok(()) | |
387 | } | |
388 | ||
389 | fn check_time_sync(&mut self) -> Result<(), Error> { | |
390 | self.output | |
391 | .log_info("Checking for supported & active NTP service..")?; | |
392 | if self.get_systemd_unit_state("systemd-timesyncd.service")?.1 == SystemdUnitState::Active { | |
393 | self.output.log_warn( | |
394 | "systemd-timesyncd is not the best choice for time-keeping on servers, due to only \ | |
395 | applying updates on boot.\ | |
396 | \n While not necessary for the upgrade it's recommended to use one of:\ | |
397 | \n * chrony (Default in new Proxmox Backup Server installations)\ | |
398 | \n * ntpsec\ | |
399 | \n * openntpd" | |
400 | )?; | |
401 | } else if self.get_systemd_unit_state("ntp.service")?.1 == SystemdUnitState::Active { | |
402 | self.output.log_info( | |
403 | "Debian deprecated and removed the ntp package for Bookworm, but the system \ | |
404 | will automatically migrate to the 'ntpsec' replacement package on upgrade.", | |
405 | )?; | |
406 | } else if self.get_systemd_unit_state("chrony.service")?.1 == SystemdUnitState::Active | |
407 | || self.get_systemd_unit_state("openntpd.service")?.1 == SystemdUnitState::Active | |
408 | || self.get_systemd_unit_state("ntpsec.service")?.1 == SystemdUnitState::Active | |
409 | { | |
410 | self.output | |
411 | .log_pass("Detected active time synchronisation unit")?; | |
412 | } else { | |
413 | self.output.log_warn( | |
414 | "No (active) time synchronisation daemon (NTP) detected, but synchronized systems \ | |
415 | are important!", | |
416 | )?; | |
417 | } | |
418 | Ok(()) | |
419 | } | |
420 | ||
421 | fn get_upgradable_packages() -> Result<proxmox_backup::tools::apt::PkgState, Error> { | |
422 | let cache = if let Ok(false) = proxmox_backup::tools::apt::pkg_cache_expired() { | |
423 | if let Ok(Some(cache)) = proxmox_backup::tools::apt::read_pkg_state() { | |
424 | cache | |
425 | } else { | |
426 | proxmox_backup::tools::apt::update_cache()? | |
427 | } | |
428 | } else { | |
429 | proxmox_backup::tools::apt::update_cache()? | |
430 | }; | |
431 | ||
432 | Ok(cache) | |
433 | } | |
434 | } | |
435 | ||
436 | #[derive(PartialEq)] | |
437 | enum SystemdUnitState { | |
438 | Active, | |
439 | Enabled, | |
440 | Disabled, | |
441 | Failed, | |
442 | Inactive, | |
443 | Unknown, | |
444 | } | |
445 | ||
446 | #[derive(Default)] | |
447 | struct Counters { | |
448 | pass: u64, | |
449 | skip: u64, | |
450 | notice: u64, | |
451 | warn: u64, | |
452 | fail: u64, | |
453 | } | |
454 | ||
455 | enum LogLevel { | |
456 | Pass, | |
457 | Info, | |
458 | Skip, | |
459 | Notice, | |
460 | Warn, | |
461 | Fail, | |
462 | } | |
463 | ||
464 | struct ConsoleOutput { | |
465 | stream: StandardStream, | |
466 | first_header: bool, | |
467 | counters: Counters, | |
468 | } | |
469 | ||
470 | impl ConsoleOutput { | |
471 | pub fn new() -> Self { | |
472 | Self { | |
473 | stream: StandardStream::stdout(ColorChoice::Always), | |
474 | first_header: true, | |
475 | counters: Counters::default(), | |
476 | } | |
477 | } | |
478 | ||
479 | pub fn print_header(&mut self, message: &str) -> Result<(), Error> { | |
480 | if !self.first_header { | |
481 | writeln!(&mut self.stream)?; | |
482 | } | |
483 | self.first_header = false; | |
484 | writeln!(&mut self.stream, "= {message} =\n")?; | |
485 | Ok(()) | |
486 | } | |
487 | ||
488 | pub fn set_color(&mut self, color: Color, bold: bool) -> Result<(), Error> { | |
489 | self.stream | |
490 | .set_color(ColorSpec::new().set_fg(Some(color)).set_bold(bold))?; | |
491 | Ok(()) | |
492 | } | |
493 | ||
494 | pub fn reset(&mut self) -> Result<(), std::io::Error> { | |
495 | self.stream.reset() | |
496 | } | |
497 | ||
498 | pub fn log_line(&mut self, level: LogLevel, message: &str) -> Result<(), Error> { | |
499 | match level { | |
500 | LogLevel::Pass => { | |
501 | self.counters.pass += 1; | |
502 | self.set_color(Color::Green, false)?; | |
503 | writeln!(&mut self.stream, "PASS: {}", message)?; | |
504 | } | |
505 | LogLevel::Info => { | |
506 | writeln!(&mut self.stream, "INFO: {}", message)?; | |
507 | } | |
508 | LogLevel::Skip => { | |
509 | self.counters.skip += 1; | |
510 | writeln!(&mut self.stream, "SKIP: {}", message)?; | |
511 | } | |
512 | LogLevel::Notice => { | |
513 | self.counters.notice += 1; | |
514 | self.set_color(Color::White, true)?; | |
515 | writeln!(&mut self.stream, "NOTICE: {}", message)?; | |
516 | } | |
517 | LogLevel::Warn => { | |
518 | self.counters.warn += 1; | |
519 | self.set_color(Color::Yellow, false)?; | |
520 | writeln!(&mut self.stream, "WARN: {}", message)?; | |
521 | } | |
522 | LogLevel::Fail => { | |
523 | self.counters.fail += 1; | |
524 | self.set_color(Color::Red, true)?; | |
525 | writeln!(&mut self.stream, "FAIL: {}", message)?; | |
526 | } | |
527 | } | |
528 | self.reset()?; | |
529 | Ok(()) | |
530 | } | |
531 | ||
532 | pub fn log_pass<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> { | |
533 | self.log_line(LogLevel::Pass, message.as_ref()) | |
534 | } | |
535 | ||
536 | pub fn log_info<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> { | |
537 | self.log_line(LogLevel::Info, message.as_ref()) | |
538 | } | |
539 | ||
540 | pub fn log_skip<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> { | |
541 | self.log_line(LogLevel::Skip, message.as_ref()) | |
542 | } | |
543 | ||
544 | pub fn log_notice<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> { | |
545 | self.log_line(LogLevel::Notice, message.as_ref()) | |
546 | } | |
547 | ||
548 | pub fn log_warn<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> { | |
549 | self.log_line(LogLevel::Warn, message.as_ref()) | |
550 | } | |
551 | ||
552 | pub fn log_fail<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> { | |
553 | self.log_line(LogLevel::Fail, message.as_ref()) | |
554 | } | |
555 | ||
556 | pub fn print_summary(&mut self) -> Result<(), Error> { | |
557 | self.print_header("SUMMARY")?; | |
558 | ||
559 | let total = self.counters.fail | |
560 | + self.counters.pass | |
561 | + self.counters.notice | |
562 | + self.counters.skip | |
563 | + self.counters.warn; | |
564 | ||
565 | writeln!(&mut self.stream, "TOTAL: {total}")?; | |
566 | self.set_color(Color::Green, false)?; | |
567 | writeln!(&mut self.stream, "PASSED: {}", self.counters.pass)?; | |
568 | self.reset()?; | |
569 | writeln!(&mut self.stream, "SKIPPED: {}", self.counters.skip)?; | |
570 | writeln!(&mut self.stream, "NOTICE: {}", self.counters.notice)?; | |
571 | if self.counters.warn > 0 { | |
572 | self.set_color(Color::Yellow, false)?; | |
573 | writeln!(&mut self.stream, "WARNINGS: {}", self.counters.warn)?; | |
574 | } | |
575 | if self.counters.fail > 0 { | |
576 | self.set_color(Color::Red, true)?; | |
577 | writeln!(&mut self.stream, "FAILURES: {}", self.counters.fail)?; | |
578 | } | |
579 | if self.counters.warn > 0 || self.counters.fail > 0 { | |
580 | let (color, bold) = if self.counters.fail > 0 { | |
581 | (Color::Red, true) | |
582 | } else { | |
583 | (Color::Yellow, false) | |
584 | }; | |
585 | ||
586 | self.set_color(color, bold)?; | |
587 | writeln!( | |
588 | &mut self.stream, | |
589 | "\nATTENTION: Please check the output for detailed information!", | |
590 | )?; | |
591 | if self.counters.fail > 0 { | |
592 | writeln!( | |
593 | &mut self.stream, | |
594 | "Try to solve the problems one at a time and rerun this checklist tool again.", | |
595 | )?; | |
596 | } | |
597 | } | |
598 | self.reset()?; | |
599 | Ok(()) | |
600 | } | |
601 | } |