4 use anyhow
::{format_err, Error}
;
6 use termcolor
::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}
;
8 use proxmox_apt
::repositories
::{self, APTRepositoryFile, APTRepositoryPackageType}
;
9 use proxmox_backup
::api2
::node
::apt
;
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;
18 fn main() -> Result
<(), Error
> {
19 let mut checker
= Checker
::new();
20 checker
.check_pbs_packages()?
;
21 checker
.check_misc()?
;
27 output
: ConsoleOutput
,
32 pub fn new() -> Self {
34 output
: ConsoleOutput
::new(),
39 pub fn check_pbs_packages(&mut self) -> Result
<(), Error
> {
41 .print_header("CHECKING VERSION INFORMATION FOR PBS PACKAGES")?
;
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
)?
;
50 fn check_upgradable_packages(&mut self) -> Result
<(), Error
> {
51 self.output
.log_info("Checking for package updates..")?
;
53 let result
= Self::get_upgradable_packages();
56 self.output
.log_warn(format
!("{err}"))?
;
58 .log_fail("unable to retrieve list of package updates!")?
;
61 if cache
.package_status
.is_empty() {
62 self.output
.log_pass("all packages up-to-date")?
;
67 .map(|pkg
| pkg
.package
.clone())
68 .collect
::<Vec
<String
>>()
70 self.output
.log_warn(format
!(
71 "updates for the following packages are available:\n {pkgs}",
79 fn check_meta_package_version(
81 pkg_versions
: &[pbs_api_types
::APTUpdateInfo
],
82 ) -> Result
<(), Error
> {
84 .log_info("Checking proxmox backup server package version..")?
;
86 let pbs_meta_pkg
= pkg_versions
88 .find(|pkg
| pkg
.package
.as_str() == PROXMOX_BACKUP_META
);
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
)?
;
98 if maj
> MIN_PBS_MAJOR
{
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
,
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
,
114 self.output
.log_fail(format
!(
115 "could not match the '{PROXMOX_BACKUP_META}' package version, \
121 .log_fail(format
!("'{PROXMOX_BACKUP_META}' package not found!"))?
;
126 fn check_kernel_compat(
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
{
133 Regex
::new(r
"^6\.(?:2\.(?:[2-9]\d+|1[6-8]|1\d\d+)|5)[^~]*$")?
,
134 "proxmox-kernel-6.2",
137 (Regex
::new(r
"^(?:5\.(?:13|15)|6\.2)")?
, "pve-kernel-5.15")
140 let output
= std
::process
::Command
::new("uname").arg("-r").output();
144 .log_fail("unable to determine running kernel version.")?
,
146 let running_version
= std
::str::from_utf8(&ret
.stdout
[..ret
.stdout
.len() - 1])?
;
147 if krunning
.is_match(running_version
) {
149 self.output
.log_pass(format
!(
150 "running new kernel '{running_version}' after upgrade."
153 self.output
.log_pass(format
!(
154 "running kernel '{running_version}' is considered suitable for \
159 let installed_kernel
= pkg_versions
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?!",
168 self.output
.log_warn(format
!(
169 "unexpected running and installed kernel '{running_version}'.",
178 fn extract_version_from_captures(
180 captures
: ®ex
::Captures
,
181 ) -> Result
<u8, Error
> {
182 if let Some(capture
) = captures
.get(index
) {
183 let val
= capture
.as_str().parse
::<u8>()?
;
190 fn check_bootloader(&mut self) -> Result
<(), Error
> {
192 .log_info("Checking bootloader configuration...")?
;
194 // PBS packages version check needs to be run before
197 .log_skip("not yet upgraded, no need to check the presence of systemd-boot")?
;
200 if !Path
::new("/etc/kernel/proxmox-boot-uuids").is_file() {
202 .log_skip("proxmox-boot-tool not used for bootloader configuration")?
;
206 if !Path
::new("/sys/firmware/efi").is_file() {
208 .log_skip("System booted in legacy-mode - no need for systemd-boot")?
;
212 if Path
::new("/usr/share/doc/systemd-boot/changelog.Debian.gz").is_file() {
213 self.output
.log_pass("systemd-boot is installed")?
;
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 \
219 initializing new ESPs will not work unitl the package is installed.",
225 fn check_apt_repos(&mut self) -> Result
<(), Error
> {
227 .log_info("Checking for package repository suite mismatches..")?
;
229 let mut strange_suite
= false;
230 let mut mismatches
= Vec
::new();
231 let mut found_suite
: Option
<(String
, String
)> = None
;
233 let (repo_files
, _repo_errors
, _digest
) = repositories
::repositories()?
;
234 for repo_file
in repo_files
{
235 self.check_repo_file(
243 match (mismatches
.is_empty(), strange_suite
) {
244 (true, false) => self.output
.log_pass("found no suite mismatch")?
,
247 .log_notice("found no suite mismatches, but found at least one strange suite")?
,
249 let mut message
= String
::from(
250 "Found mixed old and new packages repository suites, fix before upgrading!\
253 for (suite
, location
) in mismatches
.iter() {
255 format
!("\n found suite '{suite}' at '{location}'").as_str(),
259 self.output
.log_fail(message
)?
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()?
;
275 pub fn summary(&mut self) -> Result
<(), Error
> {
276 self.output
.print_summary()
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
] {
290 for suite
in &repo
.suites
{
291 let suite
= match suite
.find(&['
-'
, '
/'
][..]) {
292 Some(n
) => &suite
[0..n
],
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!"
303 *strange_suite
= true;
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
));
314 mismatches
.push((suite
.to_string(), location
));
318 let location
= repo_file
.path
.clone().unwrap_or_default();
319 *found_suite
= Some((suite
.to_string(), location
));
326 fn get_systemd_unit_state(
329 ) -> Result
<(SystemdUnitState
, SystemdUnitState
), Error
> {
330 let output
= std
::process
::Command
::new("systemctl")
334 .map_err(|err
| format_err
!("failed to execute - {err}"))?
;
336 let enabled_state
= match output
.stdout
.as_slice() {
337 b
"enabled\n" => SystemdUnitState
::Enabled
,
338 b
"disabled\n" => SystemdUnitState
::Disabled
,
339 _
=> SystemdUnitState
::Unknown
,
342 let output
= std
::process
::Command
::new("systemctl")
346 .map_err(|err
| format_err
!("failed to execute - {err}"))?
;
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
,
354 Ok((enabled_state
, active_state
))
357 fn check_pbs_services(&mut self) -> Result
<(), Error
> {
358 self.output
.log_info("Checking PBS daemon services..")?
;
360 for service
in ["proxmox-backup.service", "proxmox-backup-proxy.service"] {
361 match self.get_systemd_unit_state(service
)?
{
362 (_
, SystemdUnitState
::Active
) => {
364 .log_pass(format
!("systemd unit '{service}' is in state 'active'"))?
;
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.",
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.",
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.",
389 fn check_time_sync(&mut self) -> Result
<(), Error
> {
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)\
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.",
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
411 .log_pass("Detected active time synchronisation unit")?
;
413 self.output
.log_warn(
414 "No (active) time synchronisation daemon (NTP) detected, but synchronized systems \
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() {
426 proxmox_backup
::tools
::apt
::update_cache()?
429 proxmox_backup
::tools
::apt
::update_cache()?
437 enum SystemdUnitState
{
464 struct ConsoleOutput
{
465 stream
: StandardStream
,
471 pub fn new() -> Self {
473 stream
: StandardStream
::stdout(ColorChoice
::Always
),
475 counters
: Counters
::default(),
479 pub fn print_header(&mut self, message
: &str) -> Result
<(), Error
> {
480 if !self.first_header
{
481 writeln
!(&mut self.stream
)?
;
483 self.first_header
= false;
484 writeln
!(&mut self.stream
, "= {message} =\n")?
;
488 pub fn set_color(&mut self, color
: Color
, bold
: bool
) -> Result
<(), Error
> {
490 .set_color(ColorSpec
::new().set_fg(Some(color
)).set_bold(bold
))?
;
494 pub fn reset(&mut self) -> Result
<(), std
::io
::Error
> {
498 pub fn log_line(&mut self, level
: LogLevel
, message
: &str) -> Result
<(), Error
> {
501 self.counters
.pass
+= 1;
502 self.set_color(Color
::Green
, false)?
;
503 writeln
!(&mut self.stream
, "PASS: {}", message
)?
;
506 writeln
!(&mut self.stream
, "INFO: {}", message
)?
;
509 self.counters
.skip
+= 1;
510 writeln
!(&mut self.stream
, "SKIP: {}", message
)?
;
512 LogLevel
::Notice
=> {
513 self.counters
.notice
+= 1;
514 self.set_color(Color
::White
, true)?
;
515 writeln
!(&mut self.stream
, "NOTICE: {}", message
)?
;
518 self.counters
.warn
+= 1;
519 self.set_color(Color
::Yellow
, false)?
;
520 writeln
!(&mut self.stream
, "WARN: {}", message
)?
;
523 self.counters
.fail
+= 1;
524 self.set_color(Color
::Red
, true)?
;
525 writeln
!(&mut self.stream
, "FAIL: {}", message
)?
;
532 pub fn log_pass
<T
: AsRef
<str>>(&mut self, message
: T
) -> Result
<(), Error
> {
533 self.log_line(LogLevel
::Pass
, message
.as_ref())
536 pub fn log_info
<T
: AsRef
<str>>(&mut self, message
: T
) -> Result
<(), Error
> {
537 self.log_line(LogLevel
::Info
, message
.as_ref())
540 pub fn log_skip
<T
: AsRef
<str>>(&mut self, message
: T
) -> Result
<(), Error
> {
541 self.log_line(LogLevel
::Skip
, message
.as_ref())
544 pub fn log_notice
<T
: AsRef
<str>>(&mut self, message
: T
) -> Result
<(), Error
> {
545 self.log_line(LogLevel
::Notice
, message
.as_ref())
548 pub fn log_warn
<T
: AsRef
<str>>(&mut self, message
: T
) -> Result
<(), Error
> {
549 self.log_line(LogLevel
::Warn
, message
.as_ref())
552 pub fn log_fail
<T
: AsRef
<str>>(&mut self, message
: T
) -> Result
<(), Error
> {
553 self.log_line(LogLevel
::Fail
, message
.as_ref())
556 pub fn print_summary(&mut self) -> Result
<(), Error
> {
557 self.print_header("SUMMARY")?
;
559 let total
= self.counters
.fail
561 + self.counters
.notice
563 + self.counters
.warn
;
565 writeln
!(&mut self.stream
, "TOTAL: {total}")?
;
566 self.set_color(Color
::Green
, false)?
;
567 writeln
!(&mut self.stream
, "PASSED: {}", self.counters
.pass
)?
;
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
)?
;
575 if self.counters
.fail
> 0 {
576 self.set_color(Color
::Red
, true)?
;
577 writeln
!(&mut self.stream
, "FAILURES: {}", self.counters
.fail
)?
;
579 if self.counters
.warn
> 0 || self.counters
.fail
> 0 {
580 let (color
, bold
) = if self.counters
.fail
> 0 {
583 (Color
::Yellow
, false)
586 self.set_color(color
, bold
)?
;
589 "\nATTENTION: Please check the output for detailed information!",
591 if self.counters
.fail
> 0 {
594 "Try to solve the problems one at a time and rerun this checklist tool again.",