]> git.proxmox.com Git - proxmox-backup.git/blame - src/bin/pbs2to3.rs
handle pve-kernel -> proxmox-kernel rename
[proxmox-backup.git] / src / bin / pbs2to3.rs
CommitLineData
7f64a620
TL
1use std::io::Write;
2use std::path::Path;
3
4use anyhow::{format_err, Error};
5use regex::Regex;
6use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
7
8use proxmox_apt::repositories::{self, APTRepositoryFile, APTRepositoryPackageType};
9use proxmox_backup::api2::node::apt;
10
11const OLD_SUITE: &str = "bullseye";
12const NEW_SUITE: &str = "bookworm";
13const PROXMOX_BACKUP_META: &str = "proxmox-backup";
14const MIN_PBS_MAJOR: u8 = 2;
15const MIN_PBS_MINOR: u8 = 4;
16const MIN_PBS_PKGREL: u8 = 1;
17
18fn 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
26struct Checker {
27 output: ConsoleOutput,
28 upgraded: bool,
29}
30
31impl 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: &regex::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)]
437enum SystemdUnitState {
438 Active,
439 Enabled,
440 Disabled,
441 Failed,
442 Inactive,
443 Unknown,
444}
445
446#[derive(Default)]
447struct Counters {
448 pass: u64,
449 skip: u64,
450 notice: u64,
451 warn: u64,
452 fail: u64,
453}
454
455enum LogLevel {
456 Pass,
457 Info,
458 Skip,
459 Notice,
460 Warn,
461 Fail,
462}
463
464struct ConsoleOutput {
465 stream: StandardStream,
466 first_header: bool,
467 counters: Counters,
468}
469
470impl 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}