* [pdm-devel] [PATCH proxmox 1/1] upgrade-checks: add upgrade checker crate
2025-09-17 12:18 [pdm-devel] [PATCH datacenter-manager/proxmox{, -backup} 0/3] move upgrade checks to a common crate under proxmox-rs Shannon Sterz
@ 2025-09-17 12:18 ` Shannon Sterz
2025-09-17 12:18 ` [pdm-devel] [PATCH proxmox-backup 1/1] pbs3to4: move upgrade check script to use the new common crate Shannon Sterz
2025-09-17 12:18 ` [pdm-devel] [PATCH datacenter-manager 1/1] lib: move proxmox-upgrade-checks into common crate and depend on that Shannon Sterz
2 siblings, 0 replies; 4+ messages in thread
From: Shannon Sterz @ 2025-09-17 12:18 UTC (permalink / raw)
To: pdm-devel
that allows easily creating upgrade checks for proxmox products
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
Cargo.toml | 3 +
proxmox-upgrade-checks/Cargo.toml | 21 +
proxmox-upgrade-checks/debian/changelog | 5 +
proxmox-upgrade-checks/debian/control | 45 ++
proxmox-upgrade-checks/debian/copyright | 18 +
proxmox-upgrade-checks/debian/debcargo.toml | 7 +
proxmox-upgrade-checks/src/lib.rs | 847 ++++++++++++++++++++
7 files changed, 946 insertions(+)
create mode 100644 proxmox-upgrade-checks/Cargo.toml
create mode 100644 proxmox-upgrade-checks/debian/changelog
create mode 100644 proxmox-upgrade-checks/debian/control
create mode 100644 proxmox-upgrade-checks/debian/copyright
create mode 100644 proxmox-upgrade-checks/debian/debcargo.toml
create mode 100644 proxmox-upgrade-checks/src/lib.rs
diff --git a/Cargo.toml b/Cargo.toml
index f149af65..7b06731a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -50,6 +50,7 @@ members = [
"proxmox-tfa",
"proxmox-time",
"proxmox-time-api",
+ "proxmox-upgrade-checks",
"proxmox-uuid",
"proxmox-worker-task",
"pbs-api-types",
@@ -118,6 +119,7 @@ serde_plain = "1.0"
syn = { version = "2", features = [ "full", "visit-mut" ] }
sync_wrapper = "1"
tar = "0.4"
+termcolor = "1.1.2"
thiserror = "2"
tokio = "1.6"
tokio-openssl = "0.6.1"
@@ -136,6 +138,7 @@ zstd = "0.13"
proxmox-access-control = { version = "0.2.5", path = "proxmox-access-control" }
proxmox-acme = { version = "1.0.0", path = "proxmox-acme", default-features = false }
proxmox-api-macro = { version = "1.4.1", path = "proxmox-api-macro" }
+proxmox-apt = { version = "0.99", path = "proxmox-apt" }
proxmox-apt-api-types = { version = "2.0.0", path = "proxmox-apt-api-types" }
proxmox-auth-api = { version = "1.0.0", path = "proxmox-auth-api" }
proxmox-async = { version = "0.5.0", path = "proxmox-async" }
diff --git a/proxmox-upgrade-checks/Cargo.toml b/proxmox-upgrade-checks/Cargo.toml
new file mode 100644
index 00000000..62d50c74
--- /dev/null
+++ b/proxmox-upgrade-checks/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "proxmox-upgrade-checks"
+description = "Helpers to implement upgrade checks for Proxmox products."
+version = "1.0.0"
+
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+homepage.workspace = true
+exclude.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+const_format.workspace = true
+regex.workspace = true
+termcolor.workspace = true
+
+proxmox-apt = { workspace = true, features = [ "cache" ] }
+proxmox-apt-api-types.workspace = true
diff --git a/proxmox-upgrade-checks/debian/changelog b/proxmox-upgrade-checks/debian/changelog
new file mode 100644
index 00000000..b5cc19fc
--- /dev/null
+++ b/proxmox-upgrade-checks/debian/changelog
@@ -0,0 +1,5 @@
+rust-proxmox-upgrade-checks (1.0.0-1) trixie; urgency=medium
+
+ * Initial release.
+
+ -- Proxmox Support Team <support@proxmox.com> Tue, 16 Sep 2025 16:47:42 +0100
diff --git a/proxmox-upgrade-checks/debian/control b/proxmox-upgrade-checks/debian/control
new file mode 100644
index 00000000..b43f19fe
--- /dev/null
+++ b/proxmox-upgrade-checks/debian/control
@@ -0,0 +1,45 @@
+Source: rust-proxmox-upgrade-checks
+Section: rust
+Priority: optional
+Build-Depends: debhelper-compat (= 13),
+ dh-sequence-cargo
+Build-Depends-Arch: cargo:native <!nocheck>,
+ rustc:native (>= 1.82) <!nocheck>,
+ libstd-rust-dev <!nocheck>,
+ librust-anyhow-1+default-dev <!nocheck>,
+ librust-const-format-0.2+default-dev <!nocheck>,
+ librust-proxmox-apt-0.99+cache-dev <!nocheck>,
+ librust-proxmox-apt-0.99+default-dev <!nocheck>,
+ librust-proxmox-apt-api-types-2+default-dev <!nocheck>,
+ librust-regex-1+default-dev (>= 1.5-~~) <!nocheck>,
+ librust-termcolor-1+default-dev (>= 1.1.2-~~) <!nocheck>
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Standards-Version: 4.7.0
+Vcs-Git: git://git.proxmox.com/git/proxmox.git
+Vcs-Browser: https://git.proxmox.com/?p=proxmox.git
+Homepage: https://proxmox.com
+X-Cargo-Crate: proxmox-upgrade-checks
+Rules-Requires-Root: no
+
+Package: librust-proxmox-upgrade-checks-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-anyhow-1+default-dev,
+ librust-const-format-0.2+default-dev,
+ librust-proxmox-apt-0.99+cache-dev,
+ librust-proxmox-apt-0.99+default-dev,
+ librust-proxmox-apt-api-types-2+default-dev,
+ librust-regex-1+default-dev (>= 1.5-~~),
+ librust-termcolor-1+default-dev (>= 1.1.2-~~)
+Provides:
+ librust-proxmox-upgrade-checks+default-dev (= ${binary:Version}),
+ librust-proxmox-upgrade-checks-1-dev (= ${binary:Version}),
+ librust-proxmox-upgrade-checks-1+default-dev (= ${binary:Version}),
+ librust-proxmox-upgrade-checks-1.0-dev (= ${binary:Version}),
+ librust-proxmox-upgrade-checks-1.0+default-dev (= ${binary:Version}),
+ librust-proxmox-upgrade-checks-1.0.0-dev (= ${binary:Version}),
+ librust-proxmox-upgrade-checks-1.0.0+default-dev (= ${binary:Version})
+Description: Helpers to implement upgrade checks for Proxmox products - Rust source code
+ Source code for Debianized Rust crate "proxmox-upgrade-checks"
diff --git a/proxmox-upgrade-checks/debian/copyright b/proxmox-upgrade-checks/debian/copyright
new file mode 100644
index 00000000..1ea8a56b
--- /dev/null
+++ b/proxmox-upgrade-checks/debian/copyright
@@ -0,0 +1,18 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+
+Files:
+ *
+Copyright: 2019 - 2025 Proxmox Server Solutions GmbH <support@proxmox.com>
+License: AGPL-3.0-or-later
+ This program is free software: you can redistribute it and/or modify it under
+ the terms of the GNU Affero General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or (at your option) any
+ later version.
+ .
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ details.
+ .
+ You should have received a copy of the GNU Affero General Public License along
+ with this program. If not, see <https://www.gnu.org/licenses/>.
diff --git a/proxmox-upgrade-checks/debian/debcargo.toml b/proxmox-upgrade-checks/debian/debcargo.toml
new file mode 100644
index 00000000..b7864cdb
--- /dev/null
+++ b/proxmox-upgrade-checks/debian/debcargo.toml
@@ -0,0 +1,7 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support@proxmox.com>"
+
+[source]
+vcs_git = "git://git.proxmox.com/git/proxmox.git"
+vcs_browser = "https://git.proxmox.com/?p=proxmox.git"
diff --git a/proxmox-upgrade-checks/src/lib.rs b/proxmox-upgrade-checks/src/lib.rs
new file mode 100644
index 00000000..e95366f2
--- /dev/null
+++ b/proxmox-upgrade-checks/src/lib.rs
@@ -0,0 +1,847 @@
+use std::path::Path;
+use std::{io::Write, path::PathBuf};
+
+use anyhow::{format_err, Error};
+use const_format::concatcp;
+use regex::Regex;
+use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
+
+use proxmox_apt::repositories;
+use proxmox_apt_api_types::{APTRepositoryFile, APTRepositoryPackageType};
+
+/// Easily create and configure an upgrade checker for Proxmox products.
+pub struct UpgradeCheckerBuilder {
+ old_suite: String,
+ new_suite: String,
+ meta_package_name: String,
+ minimum_major_version: u8,
+ minimum_minor_version: u8,
+ minimum_pkgrel: u8,
+ apt_state_file: Option<PathBuf>,
+ api_server_package: Option<String>,
+ running_api_server_version: String,
+ services_list: Vec<String>,
+}
+
+impl UpgradeCheckerBuilder {
+ /// Create a new UpgradeCheckerBuilder
+ ///
+ /// * `old_suite`: The Debian suite before the upgrade.
+ /// * `new_suite`: The Debian suite after the upgrade.
+ /// * `meta_package_name`: The name of the product's meta package.
+ /// * `minimum_major_version`: The minimum major version before the upgrade.
+ /// * `minimum_minor_version`: The minimum minor version before the upgrade.
+ /// * `minimum_pkgrel`: The minimum package release before the upgrade.
+ /// * `running_api_server_version`: The currently running API server version.
+ pub fn new(
+ old_suite: &str,
+ new_suite: &str,
+ meta_package_name: &str,
+ minimum_major_version: u8,
+ minimum_minor_version: u8,
+ minimum_pkgrel: u8,
+ running_api_server_version: &str,
+ ) -> UpgradeCheckerBuilder {
+ UpgradeCheckerBuilder {
+ old_suite: old_suite.into(),
+ new_suite: new_suite.into(),
+ meta_package_name: meta_package_name.into(),
+ minimum_major_version,
+ minimum_minor_version,
+ minimum_pkgrel,
+ apt_state_file: None,
+ api_server_package: None,
+ running_api_server_version: running_api_server_version.into(),
+ services_list: Vec::new(),
+ }
+ }
+
+ /// Set the location of the APT state file.
+ pub fn apt_state_file_location<P: AsRef<Path>>(mut self, path: P) -> Self {
+ self.apt_state_file = Some(PathBuf::from(path.as_ref()));
+ self
+ }
+
+ /// Set the API server package name.
+ pub fn api_server_package(mut self, api_server_package: &str) -> Self {
+ self.api_server_package = Some(api_server_package.into());
+ self
+ }
+
+ /// Add a service to the list of services that will be checked.
+ pub fn add_service_to_checks(mut self, service_name: &str) -> Self {
+ self.services_list.push(service_name.into());
+ self
+ }
+
+ /// Construct the UpgradeChecker, consumes the UpgradeCheckerBuilder
+ pub fn build(mut self) -> UpgradeChecker {
+ UpgradeChecker {
+ output: ConsoleOutput::new(),
+ upgraded: false,
+ old_suite: self.old_suite,
+ new_suite: self.new_suite,
+ minimum_major_version: self.minimum_major_version,
+ minimum_minor_version: self.minimum_minor_version,
+ minimum_pkgrel: self.minimum_pkgrel,
+ apt_state_file: self.apt_state_file.take().unwrap_or_else(|| {
+ PathBuf::from(format!(
+ "/var/lib/{}/pkg-state.json",
+ &self.meta_package_name
+ ))
+ }),
+ api_server_package: self
+ .api_server_package
+ .unwrap_or_else(|| self.meta_package_name.clone()),
+ running_api_server_version: self.running_api_server_version,
+ meta_package_name: self.meta_package_name,
+ services_list: self.services_list,
+ }
+ }
+}
+
+/// Helpers to easily construct a set of upgrade checks.
+pub struct UpgradeChecker {
+ output: ConsoleOutput,
+ upgraded: bool,
+ old_suite: String,
+ new_suite: String,
+ meta_package_name: String,
+ minimum_major_version: u8,
+ minimum_minor_version: u8,
+ minimum_pkgrel: u8,
+ apt_state_file: PathBuf,
+ api_server_package: String,
+ running_api_server_version: String,
+ services_list: Vec<String>,
+}
+
+impl UpgradeChecker {
+ /// Run all checks.
+ pub fn run(&mut self) -> Result<(), Error> {
+ self.check_packages()?;
+ self.check_misc()?;
+ self.summary()
+ }
+
+ /// Run miscellaneous checks.
+ pub fn check_misc(&mut self) -> Result<(), Error> {
+ self.output.print_header("MISCELLANEOUS CHECKS")?;
+ self.check_services()?;
+ self.check_time_sync()?;
+ self.check_apt_repos()?;
+ self.check_bootloader()?;
+ self.check_dkms_modules()?;
+ Ok(())
+ }
+
+ /// Print a summary of all checks run so far.
+ pub fn summary(&mut self) -> Result<(), Error> {
+ self.output.print_summary()
+ }
+
+ /// Run all package related checks.
+ pub fn check_packages(&mut self) -> Result<(), Error> {
+ self.output.print_header(&format!(
+ "CHECKING VERSION INFORMATION FOR {} PACKAGES",
+ self.meta_package_name.to_uppercase()
+ ))?;
+
+ self.check_upgradable_packages()?;
+
+ let pkg_versions = proxmox_apt::get_package_versions(
+ &self.meta_package_name,
+ &self.api_server_package,
+ &self.running_api_server_version,
+ &[],
+ )?;
+
+ self.check_meta_package_version(&pkg_versions)?;
+ self.check_kernel_compat(&pkg_versions)?;
+ Ok(())
+ }
+
+ fn check_upgradable_packages(&mut self) -> Result<(), Error> {
+ self.output.log_info("Checking for package updates..")?;
+
+ let result = proxmox_apt::list_available_apt_update(&self.apt_state_file);
+ match result {
+ Err(err) => {
+ self.output.log_warn(format!("{err}"))?;
+ self.output
+ .log_fail("unable to retrieve list of package updates!")?;
+ }
+ Ok(package_status) => {
+ if package_status.is_empty() {
+ self.output.log_pass("all packages up-to-date")?;
+ } else {
+ let pkgs = package_status
+ .iter()
+ .map(|pkg| pkg.package.clone())
+ .collect::<Vec<String>>()
+ .join(", ");
+ self.output.log_warn(format!(
+ "updates for the following packages are available:\n {pkgs}",
+ ))?;
+ }
+ }
+ }
+ Ok(())
+ }
+
+ fn check_meta_package_version(
+ &mut self,
+ pkg_versions: &[proxmox_apt_api_types::APTUpdateInfo],
+ ) -> Result<(), Error> {
+ self.output.log_info(format!(
+ "Checking {} package version..",
+ self.meta_package_name
+ ))?;
+
+ let meta_pkg = pkg_versions
+ .iter()
+ .find(|pkg| pkg.package.as_str() == self.meta_package_name);
+
+ if let Some(meta_pkg) = meta_pkg {
+ let pkg_version = Regex::new(r"^(\d+)\.(\d+)[.-](\d+)")?;
+ let captures = pkg_version.captures(&meta_pkg.old_version);
+ if let Some(captures) = captures {
+ let maj = Self::extract_version_from_captures(1, &captures)?;
+ let min = Self::extract_version_from_captures(2, &captures)?;
+ let pkgrel = Self::extract_version_from_captures(3, &captures)?;
+
+ let min_version = format!(
+ "{}.{}.{}",
+ self.minimum_major_version, self.minimum_minor_version, self.minimum_pkgrel
+ );
+
+ if (maj > self.minimum_major_version && self.minimum_major_version != 0)
+ // Handle alpha and beta version upgrade checks:
+ || (self.minimum_major_version == 0 && min > self.minimum_minor_version)
+ {
+ self.output
+ .log_pass(format!("Already upgraded to {maj}.{min}"))?;
+ self.upgraded = true;
+ } else if maj >= self.minimum_major_version
+ && min >= self.minimum_minor_version
+ && pkgrel >= self.minimum_pkgrel
+ {
+ self.output.log_pass(format!(
+ "'{}' has version >= {min_version}",
+ self.meta_package_name
+ ))?;
+ } else {
+ self.output.log_fail(format!(
+ "'{}' package is too old, please upgrade to >= {min_version}",
+ self.meta_package_name
+ ))?;
+ }
+ } else {
+ self.output.log_fail(format!(
+ "could not match the '{}' package version, \
+ is it installed?",
+ self.meta_package_name
+ ))?;
+ }
+ } else {
+ self.output
+ .log_fail(format!("'{}' package not found!", self.meta_package_name))?;
+ }
+ Ok(())
+ }
+
+ fn is_kernel_version_compatible(&self, running_version: &str) -> bool {
+ // TODO: rework this to parse out maj.min.patch and do numerical comparison and detect
+ // those with a "bpo12" as backport kernels from the older release.
+ const MINIMUM_RE: &str = r"6\.(?:14\.(?:[1-9]\d+|[6-9])|1[5-9])[^~]*";
+ const ARBITRARY_RE: &str = r"(?:1[4-9]|2\d+)\.(?:[0-9]|\d{2,})[^~]*-pve";
+
+ let re = if self.upgraded {
+ concatcp!(r"^(?:", MINIMUM_RE, r"|", ARBITRARY_RE, r")$")
+ } else {
+ r"^(?:6\.(?:2|5|8|11|14))"
+ };
+ let re = Regex::new(re).expect("failed to compile kernel compat regex");
+
+ re.is_match(running_version)
+ }
+
+ fn check_kernel_compat(
+ &mut self,
+ pkg_versions: &[proxmox_apt_api_types::APTUpdateInfo],
+ ) -> Result<(), Error> {
+ self.output.log_info("Check running kernel version..")?;
+
+ let kinstalled = if self.upgraded {
+ "proxmox-kernel-6.14"
+ } else {
+ "proxmox-kernel-6.8"
+ };
+
+ let output = std::process::Command::new("uname").arg("-r").output();
+ match output {
+ Err(_err) => self
+ .output
+ .log_fail("unable to determine running kernel version.")?,
+ Ok(ret) => {
+ let running_version = std::str::from_utf8(&ret.stdout[..ret.stdout.len() - 1])?;
+ if self.is_kernel_version_compatible(running_version) {
+ if self.upgraded {
+ self.output.log_pass(format!(
+ "running new kernel '{running_version}' after upgrade."
+ ))?;
+ } else {
+ self.output.log_pass(format!(
+ "running kernel '{running_version}' is considered suitable for \
+ upgrade."
+ ))?;
+ }
+ } else {
+ let installed_kernel = pkg_versions
+ .iter()
+ .find(|pkg| pkg.package.as_str() == kinstalled);
+ if installed_kernel.is_some() {
+ self.output.log_warn(format!(
+ "a suitable kernel '{kinstalled}' is installed, but an \
+ unsuitable '{running_version}' is booted, missing reboot?!",
+ ))?;
+ } else {
+ self.output.log_warn(format!(
+ "unexpected running and installed kernel '{running_version}'.",
+ ))?;
+ }
+ }
+ }
+ }
+ Ok(())
+ }
+
+ fn extract_version_from_captures(
+ index: usize,
+ captures: ®ex::Captures,
+ ) -> Result<u8, Error> {
+ if let Some(capture) = captures.get(index) {
+ let val = capture.as_str().parse::<u8>()?;
+ Ok(val)
+ } else {
+ Ok(0)
+ }
+ }
+
+ fn check_bootloader(&mut self) -> Result<(), Error> {
+ self.output
+ .log_info("Checking bootloader configuration...")?;
+
+ let sd_boot_installed =
+ Path::new("/usr/share/doc/systemd-boot/changelog.Debian.gz").is_file();
+
+ if !Path::new("/sys/firmware/efi").is_dir() {
+ if sd_boot_installed {
+ self.output.log_warn(
+ "systemd-boot package installed on legacy-boot system is not \
+ necessary, consider removing it",
+ )?;
+ return Ok(());
+ }
+ self.output
+ .log_skip("System booted in legacy-mode - no need for additional packages.")?;
+ return Ok(());
+ }
+
+ let mut boot_ok = true;
+ if Path::new("/etc/kernel/proxmox-boot-uuids").is_file() {
+ // Package version check needs to be run before
+ if !self.upgraded {
+ let output = std::process::Command::new("proxmox-boot-tool")
+ .arg("status")
+ .output()
+ .map_err(|err| {
+ format_err!("failed to retrieve proxmox-boot-tool status - {err}")
+ })?;
+ let re = Regex::new(r"configured with:.* (uefi|systemd-boot) \(versions:")
+ .expect("failed to proxmox-boot-tool status");
+ if re.is_match(std::str::from_utf8(&output.stdout)?) {
+ self.output
+ .log_skip("not yet upgraded, systemd-boot still needed for bootctl")?;
+ return Ok(());
+ }
+ }
+ } else {
+ if !Path::new("/usr/share/doc/grub-efi-amd64/changelog.Debian.gz").is_file() {
+ self.output.log_warn(
+ "System booted in uefi mode but grub-efi-amd64 meta-package not installed, \
+ new grub versions will not be installed to /boot/efi!
+ Install grub-efi-amd64.",
+ )?;
+ boot_ok = false;
+ }
+ if Path::new("/boot/efi/EFI/BOOT/BOOTX64.efi").is_file() {
+ let output = std::process::Command::new("debconf-show")
+ .arg("--db")
+ .arg("configdb")
+ .arg("grub-efi-amd64")
+ .arg("grub-pc")
+ .output()
+ .map_err(|err| format_err!("failed to retrieve debconf settings - {err}"))?;
+ let re = Regex::new(r"grub2/force_efi_extra_removable: +true(?:\n|$)")
+ .expect("failed to compile dbconfig regex");
+ if !re.is_match(std::str::from_utf8(&output.stdout)?) {
+ self.output.log_warn(
+ "Removable bootloader found at '/boot/efi/EFI/BOOT/BOOTX64.efi', but GRUB packages \
+ not set up to update it!\nRun the following command:\n\
+ echo 'grub-efi-amd64 grub2/force_efi_extra_removable boolean true' | debconf-set-selections -v -u\n\
+ Then reinstall GRUB with 'apt install --reinstall grub-efi-amd64'"
+ )?;
+ boot_ok = false;
+ }
+ }
+ }
+ if sd_boot_installed {
+ self.output.log_fail(
+ "systemd-boot meta-package installed. This will cause problems on upgrades of other \
+ boot-related packages.\n\
+ Remove the 'systemd-boot' package.\n\
+ Please consult the upgrade guide for further information!"
+ )?;
+ boot_ok = false;
+ }
+ if boot_ok {
+ self.output
+ .log_pass("bootloader packages installed correctly")?;
+ }
+ Ok(())
+ }
+
+ fn check_apt_repos(&mut self) -> Result<(), Error> {
+ self.output
+ .log_info("Checking for package repository suite mismatches..")?;
+
+ let mut strange_suite = false;
+ let mut mismatches = Vec::new();
+ let mut found_suite: Option<(String, String)> = None;
+
+ let (repo_files, _repo_errors, _digest) = repositories::repositories()?;
+ for repo_file in repo_files {
+ self.check_repo_file(
+ &mut found_suite,
+ &mut mismatches,
+ &mut strange_suite,
+ repo_file,
+ )?;
+ }
+
+ match (mismatches.is_empty(), strange_suite) {
+ (true, false) => self.output.log_pass("found no suite mismatch")?,
+ (true, true) => self
+ .output
+ .log_notice("found no suite mismatches, but found at least one strange suite")?,
+ (false, _) => {
+ let mut message = String::from(
+ "Found mixed old and new packages repository suites, fix before upgrading!\
+ \n Mismatches:",
+ );
+ for (suite, location) in mismatches.iter() {
+ message.push_str(
+ format!("\n found suite '{suite}' at '{location}'").as_str(),
+ );
+ }
+ message.push('\n');
+ self.output.log_fail(message)?
+ }
+ }
+
+ Ok(())
+ }
+
+ fn check_dkms_modules(&mut self) -> Result<(), Error> {
+ let kver = std::process::Command::new("uname")
+ .arg("-r")
+ .output()
+ .map_err(|err| format_err!("failed to retrieve running kernel version - {err}"))?;
+
+ let output = std::process::Command::new("dkms")
+ .arg("status")
+ .arg("-k")
+ .arg(std::str::from_utf8(&kver.stdout)?)
+ .output();
+ match output {
+ Err(_err) => self.output.log_skip("could not get dkms status")?,
+ Ok(ret) => {
+ let num_dkms_modules = std::str::from_utf8(&ret.stdout)?.lines().count();
+ if num_dkms_modules == 0 {
+ self.output.log_pass("no dkms modules found")?;
+ } else {
+ self.output
+ .log_warn("dkms modules found, this might cause issues during upgrade.")?;
+ }
+ }
+ }
+ Ok(())
+ }
+
+ fn check_repo_file(
+ &mut self,
+ found_suite: &mut Option<(String, String)>,
+ mismatches: &mut Vec<(String, String)>,
+ strange_suite: &mut bool,
+ repo_file: APTRepositoryFile,
+ ) -> Result<(), Error> {
+ for repo in repo_file.repositories {
+ if !repo.enabled || repo.types == [APTRepositoryPackageType::DebSrc] {
+ continue;
+ }
+ for suite in &repo.suites {
+ let suite = match suite.find(&['-', '/'][..]) {
+ Some(n) => &suite[0..n],
+ None => suite,
+ };
+
+ if suite != self.old_suite && suite != self.new_suite {
+ let location = repo_file.path.clone().unwrap_or_default();
+ self.output.log_notice(format!(
+ "found unusual suite '{suite}', neither old '{}' nor new \
+ '{}'..\n Affected file {location}\n Please \
+ assure this is shipping compatible packages for the upgrade!",
+ self.old_suite, self.new_suite
+ ))?;
+ *strange_suite = true;
+ continue;
+ }
+
+ if let Some((ref current_suite, ref current_location)) = found_suite {
+ let location = repo_file.path.clone().unwrap_or_default();
+ if suite != current_suite {
+ if mismatches.is_empty() {
+ mismatches.push((current_suite.clone(), current_location.clone()));
+ mismatches.push((suite.to_string(), location));
+ } else {
+ mismatches.push((suite.to_string(), location));
+ }
+ }
+ } else {
+ let location = repo_file.path.clone().unwrap_or_default();
+ *found_suite = Some((suite.to_string(), location));
+ }
+ }
+ }
+ Ok(())
+ }
+
+ fn get_systemd_unit_state(
+ &self,
+ unit: &str,
+ ) -> Result<(SystemdUnitState, SystemdUnitState), Error> {
+ let output = std::process::Command::new("systemctl")
+ .arg("is-enabled")
+ .arg(unit)
+ .output()
+ .map_err(|err| format_err!("failed to execute - {err}"))?;
+
+ let enabled_state = match output.stdout.as_slice() {
+ b"enabled\n" => SystemdUnitState::Enabled,
+ b"disabled\n" => SystemdUnitState::Disabled,
+ _ => SystemdUnitState::Unknown,
+ };
+
+ let output = std::process::Command::new("systemctl")
+ .arg("is-active")
+ .arg(unit)
+ .output()
+ .map_err(|err| format_err!("failed to execute - {err}"))?;
+
+ let active_state = match output.stdout.as_slice() {
+ b"active\n" => SystemdUnitState::Active,
+ b"inactive\n" => SystemdUnitState::Inactive,
+ b"failed\n" => SystemdUnitState::Failed,
+ _ => SystemdUnitState::Unknown,
+ };
+ Ok((enabled_state, active_state))
+ }
+
+ fn check_services(&mut self) -> Result<(), Error> {
+ self.output.log_info(format!(
+ "Checking {} daemon services..",
+ self.meta_package_name
+ ))?;
+
+ for service in self.services_list.as_slice() {
+ match self.get_systemd_unit_state(service)? {
+ (_, SystemdUnitState::Active) => {
+ self.output
+ .log_pass(format!("systemd unit '{service}' is in state 'active'"))?;
+ }
+ (_, SystemdUnitState::Inactive) => {
+ self.output.log_fail(format!(
+ "systemd unit '{service}' is in state 'inactive'\
+ \n Please check the service for errors and start it.",
+ ))?;
+ }
+ (_, SystemdUnitState::Failed) => {
+ self.output.log_fail(format!(
+ "systemd unit '{service}' is in state 'failed'\
+ \n Please check the service for errors and start it.",
+ ))?;
+ }
+ (_, _) => {
+ self.output.log_fail(format!(
+ "systemd unit '{service}' is not in state 'active'\
+ \n Please check the service for errors and start it.",
+ ))?;
+ }
+ }
+ }
+ Ok(())
+ }
+
+ fn check_time_sync(&mut self) -> Result<(), Error> {
+ self.output
+ .log_info("Checking for supported & active NTP service..")?;
+ if self.get_systemd_unit_state("systemd-timesyncd.service")?.1 == SystemdUnitState::Active {
+ self.output.log_warn(
+ "systemd-timesyncd is not the best choice for time-keeping on servers, due to only \
+ applying updates on boot.\
+ \n While not necessary for the upgrade it's recommended to use one of:\
+ \n * chrony (Default in new Proxmox product installations)\
+ \n * ntpsec\
+ \n * openntpd"
+ )?;
+ } else if self.get_systemd_unit_state("ntp.service")?.1 == SystemdUnitState::Active {
+ self.output.log_info(
+ "Debian deprecated and removed the ntp package for Bookworm, but the system \
+ will automatically migrate to the 'ntpsec' replacement package on upgrade.",
+ )?;
+ } else if self.get_systemd_unit_state("chrony.service")?.1 == SystemdUnitState::Active
+ || self.get_systemd_unit_state("openntpd.service")?.1 == SystemdUnitState::Active
+ || self.get_systemd_unit_state("ntpsec.service")?.1 == SystemdUnitState::Active
+ {
+ self.output
+ .log_pass("Detected active time synchronisation unit")?;
+ } else {
+ self.output.log_warn(
+ "No (active) time synchronisation daemon (NTP) detected, but synchronized systems \
+ are important!",
+ )?;
+ }
+ Ok(())
+ }
+}
+
+#[derive(PartialEq)]
+enum SystemdUnitState {
+ Active,
+ Enabled,
+ Disabled,
+ Failed,
+ Inactive,
+ Unknown,
+}
+
+#[derive(Default)]
+struct Counters {
+ pass: u64,
+ skip: u64,
+ notice: u64,
+ warn: u64,
+ fail: u64,
+}
+
+enum LogLevel {
+ Pass,
+ Info,
+ Skip,
+ Notice,
+ Warn,
+ Fail,
+}
+
+struct ConsoleOutput {
+ stream: StandardStream,
+ first_header: bool,
+ counters: Counters,
+}
+
+impl ConsoleOutput {
+ fn new() -> Self {
+ Self {
+ stream: StandardStream::stdout(ColorChoice::Always),
+ first_header: true,
+ counters: Counters::default(),
+ }
+ }
+
+ fn print_header(&mut self, message: &str) -> Result<(), Error> {
+ if !self.first_header {
+ writeln!(&mut self.stream)?;
+ }
+ self.first_header = false;
+ writeln!(&mut self.stream, "= {message} =\n")?;
+ Ok(())
+ }
+
+ fn set_color(&mut self, color: Color, bold: bool) -> Result<(), Error> {
+ self.stream
+ .set_color(ColorSpec::new().set_fg(Some(color)).set_bold(bold))?;
+ Ok(())
+ }
+
+ fn reset(&mut self) -> Result<(), std::io::Error> {
+ self.stream.reset()
+ }
+
+ fn log_line(&mut self, level: LogLevel, message: &str) -> Result<(), Error> {
+ match level {
+ LogLevel::Pass => {
+ self.counters.pass += 1;
+ self.set_color(Color::Green, false)?;
+ writeln!(&mut self.stream, "PASS: {}", message)?;
+ }
+ LogLevel::Info => {
+ writeln!(&mut self.stream, "INFO: {}", message)?;
+ }
+ LogLevel::Skip => {
+ self.counters.skip += 1;
+ writeln!(&mut self.stream, "SKIP: {}", message)?;
+ }
+ LogLevel::Notice => {
+ self.counters.notice += 1;
+ self.set_color(Color::White, true)?;
+ writeln!(&mut self.stream, "NOTICE: {}", message)?;
+ }
+ LogLevel::Warn => {
+ self.counters.warn += 1;
+ self.set_color(Color::Yellow, false)?;
+ writeln!(&mut self.stream, "WARN: {}", message)?;
+ }
+ LogLevel::Fail => {
+ self.counters.fail += 1;
+ self.set_color(Color::Red, true)?;
+ writeln!(&mut self.stream, "FAIL: {}", message)?;
+ }
+ }
+ self.reset()?;
+ Ok(())
+ }
+
+ fn log_pass<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
+ self.log_line(LogLevel::Pass, message.as_ref())
+ }
+
+ fn log_info<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
+ self.log_line(LogLevel::Info, message.as_ref())
+ }
+
+ fn log_skip<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
+ self.log_line(LogLevel::Skip, message.as_ref())
+ }
+
+ fn log_notice<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
+ self.log_line(LogLevel::Notice, message.as_ref())
+ }
+
+ fn log_warn<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
+ self.log_line(LogLevel::Warn, message.as_ref())
+ }
+
+ fn log_fail<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
+ self.log_line(LogLevel::Fail, message.as_ref())
+ }
+
+ fn print_summary(&mut self) -> Result<(), Error> {
+ self.print_header("SUMMARY")?;
+
+ let total = self.counters.fail
+ + self.counters.pass
+ + self.counters.notice
+ + self.counters.skip
+ + self.counters.warn;
+
+ writeln!(&mut self.stream, "TOTAL: {total}")?;
+ self.set_color(Color::Green, false)?;
+ writeln!(&mut self.stream, "PASSED: {}", self.counters.pass)?;
+ self.reset()?;
+ writeln!(&mut self.stream, "SKIPPED: {}", self.counters.skip)?;
+ writeln!(&mut self.stream, "NOTICE: {}", self.counters.notice)?;
+ if self.counters.warn > 0 {
+ self.set_color(Color::Yellow, false)?;
+ writeln!(&mut self.stream, "WARNINGS: {}", self.counters.warn)?;
+ }
+ if self.counters.fail > 0 {
+ self.set_color(Color::Red, true)?;
+ writeln!(&mut self.stream, "FAILURES: {}", self.counters.fail)?;
+ }
+ if self.counters.warn > 0 || self.counters.fail > 0 {
+ let (color, bold) = if self.counters.fail > 0 {
+ (Color::Red, true)
+ } else {
+ (Color::Yellow, false)
+ };
+
+ self.set_color(color, bold)?;
+ writeln!(
+ &mut self.stream,
+ "\nATTENTION: Please check the output for detailed information!",
+ )?;
+ if self.counters.fail > 0 {
+ writeln!(
+ &mut self.stream,
+ "Try to solve the problems one at a time and rerun this checklist tool again.",
+ )?;
+ }
+ }
+ self.reset()?;
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn test_is_kernel_version_compatible(
+ expected_versions: &[&str],
+ unexpected_versions: &[&str],
+ upgraded: bool,
+ ) {
+ let mut checker = UpgradeCheckerBuilder::new(
+ "bookworm",
+ "trixie",
+ "proxmox-backup",
+ 3,
+ 4,
+ 0,
+ "running version: 3.4",
+ )
+ .build();
+
+ checker.upgraded = upgraded;
+
+ for version in expected_versions {
+ assert!(
+ checker.is_kernel_version_compatible(version),
+ "compatible kernel version '{version}' did not pass as expected!"
+ );
+ }
+ for version in unexpected_versions {
+ assert!(
+ !checker.is_kernel_version_compatible(version),
+ "incompatible kernel version '{version}' passed as expected!"
+ );
+ }
+ }
+
+ #[test]
+ fn test_before_upgrade_kernel_version_compatibility() {
+ let expected_versions = &["6.2.16-20-pve", "6.5.13-6-pve", "6.8.12-1-pve"];
+ let unexpected_versions = &["6.1.10-1-pve", "5.19.17-2-pve"];
+
+ test_is_kernel_version_compatible(expected_versions, unexpected_versions, false);
+ }
+
+ #[test]
+ fn test_after_upgrade_kernel_version_compatibility() {
+ let expected_versions = &["6.14.6-1-pve", "6.17.0-1-pve"];
+ let unexpected_versions = &["6.12.1-1-pve", "6.2.1-1-pve"];
+
+ test_is_kernel_version_compatible(expected_versions, unexpected_versions, true);
+ }
+}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 4+ messages in thread
* [pdm-devel] [PATCH proxmox-backup 1/1] pbs3to4: move upgrade check script to use the new common crate
2025-09-17 12:18 [pdm-devel] [PATCH datacenter-manager/proxmox{, -backup} 0/3] move upgrade checks to a common crate under proxmox-rs Shannon Sterz
2025-09-17 12:18 ` [pdm-devel] [PATCH proxmox 1/1] upgrade-checks: add upgrade checker crate Shannon Sterz
@ 2025-09-17 12:18 ` Shannon Sterz
2025-09-17 12:18 ` [pdm-devel] [PATCH datacenter-manager 1/1] lib: move proxmox-upgrade-checks into common crate and depend on that Shannon Sterz
2 siblings, 0 replies; 4+ messages in thread
From: Shannon Sterz @ 2025-09-17 12:18 UTC (permalink / raw)
To: pdm-devel
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
Cargo.toml | 3 +
src/bin/pbs3to4.rs | 738 ++-------------------------------------------
2 files changed, 20 insertions(+), 721 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index b3f55b4d..309aa960 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -92,6 +92,7 @@ proxmox-sys = "1"
proxmox-systemd = "1"
proxmox-tfa = { version = "6.0.3", features = [ "api", "api-types" ] }
proxmox-time = "2"
+proxmox-upgrade-checks = "1"
proxmox-uuid = { version = "1", features = [ "serde" ] }
proxmox-worker-task = "1"
pbs-api-types = "1.0.3"
@@ -241,6 +242,7 @@ proxmox-sys = { workspace = true, features = [ "timer" ] }
proxmox-systemd.workspace = true
proxmox-tfa.workspace = true
proxmox-time.workspace = true
+proxmox-upgrade-checks.workspace = true
proxmox-uuid.workspace = true
proxmox-worker-task.workspace = true
pbs-api-types.workspace = true
@@ -305,6 +307,7 @@ proxmox-rrd-api-types.workspace = true
#proxmox-systemd = { path = "../proxmox/proxmox-systemd" }
#proxmox-tfa = { path = "../proxmox/proxmox-tfa" }
#proxmox-time = { path = "../proxmox/proxmox-time" }
+#proxmox-upgrade-checks = { path = "../proxmox/proxmox-upgrade-checks" }
#proxmox-uuid = { path = "../proxmox/proxmox-uuid" }
#proxmox-worker-task = { path = "../proxmox/proxmox-worker-task" }
diff --git a/src/bin/pbs3to4.rs b/src/bin/pbs3to4.rs
index 4b432beb..ec39d6bc 100644
--- a/src/bin/pbs3to4.rs
+++ b/src/bin/pbs3to4.rs
@@ -1,723 +1,19 @@
-use std::io::Write;
-use std::path::Path;
+use pbs_buildcfg::{APT_PKG_STATE_FN, PROXMOX_PKG_RELEASE, PROXMOX_PKG_VERSION};
+use proxmox_upgrade_checks::UpgradeCheckerBuilder;
-use anyhow::{format_err, Error};
-use const_format::concatcp;
-use regex::Regex;
-use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
-
-use proxmox_apt::repositories;
-use proxmox_apt_api_types::{APTRepositoryFile, APTRepositoryPackageType};
-use proxmox_backup::api2::node::apt;
-
-const OLD_SUITE: &str = "bookworm";
-const NEW_SUITE: &str = "trixie";
-const PROXMOX_BACKUP_META: &str = "proxmox-backup";
-const MIN_PBS_MAJOR: u8 = 3;
-const MIN_PBS_MINOR: u8 = 4;
-const MIN_PBS_PKGREL: u8 = 0;
-
-fn main() -> Result<(), Error> {
- let mut checker = Checker::new();
- checker.check_pbs_packages()?;
- checker.check_misc()?;
- checker.summary()?;
- Ok(())
-}
-
-struct Checker {
- output: ConsoleOutput,
- upgraded: bool,
-}
-
-impl Checker {
- pub fn new() -> Self {
- Self {
- output: ConsoleOutput::new(),
- upgraded: false,
- }
- }
-
- pub fn check_pbs_packages(&mut self) -> Result<(), Error> {
- self.output
- .print_header("CHECKING VERSION INFORMATION FOR PBS PACKAGES")?;
-
- self.check_upgradable_packages()?;
- let pkg_versions = apt::get_versions()?;
- self.check_meta_package_version(&pkg_versions)?;
- self.check_kernel_compat(&pkg_versions)?;
- Ok(())
- }
-
- fn check_upgradable_packages(&mut self) -> Result<(), Error> {
- self.output.log_info("Checking for package updates..")?;
-
- let result = apt::apt_update_available();
- match result {
- Err(err) => {
- self.output.log_warn(format!("{err}"))?;
- self.output
- .log_fail("unable to retrieve list of package updates!")?;
- }
- Ok(package_status) => {
- if package_status.is_empty() {
- self.output.log_pass("all packages up-to-date")?;
- } else {
- let pkgs = package_status
- .iter()
- .map(|pkg| pkg.package.clone())
- .collect::<Vec<String>>()
- .join(", ");
- self.output.log_warn(format!(
- "updates for the following packages are available:\n {pkgs}",
- ))?;
- }
- }
- }
- Ok(())
- }
-
- fn check_meta_package_version(
- &mut self,
- pkg_versions: &[pbs_api_types::APTUpdateInfo],
- ) -> Result<(), Error> {
- self.output
- .log_info("Checking proxmox backup server package version..")?;
-
- let pbs_meta_pkg = pkg_versions
- .iter()
- .find(|pkg| pkg.package.as_str() == PROXMOX_BACKUP_META);
-
- if let Some(pbs_meta_pkg) = pbs_meta_pkg {
- let pkg_version = Regex::new(r"^(\d+)\.(\d+)[.-](\d+)")?;
- let captures = pkg_version.captures(&pbs_meta_pkg.old_version);
- if let Some(captures) = captures {
- let maj = Self::extract_version_from_captures(1, &captures)?;
- let min = Self::extract_version_from_captures(2, &captures)?;
- let pkgrel = Self::extract_version_from_captures(3, &captures)?;
-
- let min_version = format!("{MIN_PBS_MAJOR}.{MIN_PBS_MINOR}.{MIN_PBS_PKGREL}");
-
- if maj > MIN_PBS_MAJOR {
- self.output
- .log_pass(format!("Already upgraded to Proxmox Backup Server {maj}"))?;
- self.upgraded = true;
- } else if maj >= MIN_PBS_MAJOR && min >= MIN_PBS_MINOR && pkgrel >= MIN_PBS_PKGREL {
- self.output.log_pass(format!(
- "'{PROXMOX_BACKUP_META}' has version >= {min_version}"
- ))?;
- } else {
- self.output.log_fail(format!(
- "'{PROXMOX_BACKUP_META}' package is too old, please upgrade to >= {min_version}"
- ))?;
- }
- } else {
- self.output.log_fail(format!(
- "could not match the '{PROXMOX_BACKUP_META}' package version, \
- is it installed?",
- ))?;
- }
- } else {
- self.output
- .log_fail(format!("'{PROXMOX_BACKUP_META}' package not found!"))?;
- }
- Ok(())
- }
-
- fn is_kernel_version_compatible(&self, running_version: &str) -> bool {
- // TODO: rework this to parse out maj.min.patch and do numerical comparission and detect
- // those with a "bpo12" as backport kernels from the older release.
- const MINIMUM_RE: &str = r"6\.(?:14\.(?:[1-9]\d+|[6-9])|1[5-9])[^~]*";
- const ARBITRARY_RE: &str = r"(?:1[4-9]|2\d+)\.(?:[0-9]|\d{2,})[^~]*-pve";
-
- let re = if self.upgraded {
- concatcp!(r"^(?:", MINIMUM_RE, r"|", ARBITRARY_RE, r")$")
- } else {
- r"^(?:6\.(?:2|5|8|11|14))"
- };
- let re = Regex::new(re).expect("failed to compile kernel compat regex");
-
- re.is_match(running_version)
- }
-
- fn check_kernel_compat(
- &mut self,
- pkg_versions: &[pbs_api_types::APTUpdateInfo],
- ) -> Result<(), Error> {
- self.output.log_info("Check running kernel version..")?;
-
- let kinstalled = if self.upgraded {
- "proxmox-kernel-6.14"
- } else {
- "proxmox-kernel-6.8"
- };
-
- let output = std::process::Command::new("uname").arg("-r").output();
- match output {
- Err(_err) => self
- .output
- .log_fail("unable to determine running kernel version.")?,
- Ok(ret) => {
- let running_version = std::str::from_utf8(&ret.stdout[..ret.stdout.len() - 1])?;
- if self.is_kernel_version_compatible(running_version) {
- if self.upgraded {
- self.output.log_pass(format!(
- "running new kernel '{running_version}' after upgrade."
- ))?;
- } else {
- self.output.log_pass(format!(
- "running kernel '{running_version}' is considered suitable for \
- upgrade."
- ))?;
- }
- } else {
- let installed_kernel = pkg_versions
- .iter()
- .find(|pkg| pkg.package.as_str() == kinstalled);
- if installed_kernel.is_some() {
- self.output.log_warn(format!(
- "a suitable kernel '{kinstalled}' is installed, but an \
- unsuitable '{running_version}' is booted, missing reboot?!",
- ))?;
- } else {
- self.output.log_warn(format!(
- "unexpected running and installed kernel '{running_version}'.",
- ))?;
- }
- }
- }
- }
- Ok(())
- }
-
- fn extract_version_from_captures(
- index: usize,
- captures: ®ex::Captures,
- ) -> Result<u8, Error> {
- if let Some(capture) = captures.get(index) {
- let val = capture.as_str().parse::<u8>()?;
- Ok(val)
- } else {
- Ok(0)
- }
- }
-
- fn check_bootloader(&mut self) -> Result<(), Error> {
- self.output
- .log_info("Checking bootloader configuration...")?;
-
- let sd_boot_installed =
- Path::new("/usr/share/doc/systemd-boot/changelog.Debian.gz").is_file();
-
- if !Path::new("/sys/firmware/efi").is_dir() {
- if sd_boot_installed {
- self.output.log_warn(
- "systemd-boot package installed on legacy-boot system is not \
- necessary, consider removing it",
- )?;
- return Ok(());
- }
- self.output
- .log_skip("System booted in legacy-mode - no need for additional packages.")?;
- return Ok(());
- }
-
- let mut boot_ok = true;
- if Path::new("/etc/kernel/proxmox-boot-uuids").is_file() {
- // PBS packages version check needs to be run before
- if !self.upgraded {
- let output = std::process::Command::new("proxmox-boot-tool")
- .arg("status")
- .output()
- .map_err(|err| {
- format_err!("failed to retrieve proxmox-boot-tool status - {err}")
- })?;
- let re = Regex::new(r"configured with:.* (uefi|systemd-boot) \(versions:")
- .expect("failed to proxmox-boot-tool status");
- if re.is_match(std::str::from_utf8(&output.stdout)?) {
- self.output
- .log_skip("not yet upgraded, systemd-boot still needed for bootctl")?;
- return Ok(());
- }
- }
- } else {
- if !Path::new("/usr/share/doc/grub-efi-amd64/changelog.Debian.gz").is_file() {
- self.output.log_warn(
- "System booted in uefi mode but grub-efi-amd64 meta-package not installed, \
- new grub versions will not be installed to /boot/efi!
- Install grub-efi-amd64.",
- )?;
- boot_ok = false;
- }
- if Path::new("/boot/efi/EFI/BOOT/BOOTX64.efi").is_file() {
- let output = std::process::Command::new("debconf-show")
- .arg("--db")
- .arg("configdb")
- .arg("grub-efi-amd64")
- .arg("grub-pc")
- .output()
- .map_err(|err| format_err!("failed to retrieve debconf settings - {err}"))?;
- let re = Regex::new(r"grub2/force_efi_extra_removable: +true(?:\n|$)")
- .expect("failed to compile dbconfig regex");
- if !re.is_match(std::str::from_utf8(&output.stdout)?) {
- self.output.log_warn(format!(
- "Removable bootloader found at '/boot/efi/EFI/BOOT/BOOTX64.efi', but GRUB packages \
- not set up to update it!\nRun the following command:\n\
- echo 'grub-efi-amd64 grub2/force_efi_extra_removable boolean true' | debconf-set-selections -v -u\n\
- Then reinstall GRUB with 'apt install --reinstall grub-efi-amd64'"
- ))?;
- boot_ok = false;
- }
- }
- }
- if sd_boot_installed {
- self.output.log_fail(
- "systemd-boot meta-package installed. This will cause problems on upgrades of other \
- boot-related packages.\n\
- Remove the 'systemd-boot' package.\n\
- See https://pbs.proxmox.com/wiki/Upgrade_from_3_to_4#sd-boot-warning for more information."
- )?;
- boot_ok = false;
- }
- if boot_ok {
- self.output
- .log_pass("bootloader packages installed correctly")?;
- }
- Ok(())
- }
-
- fn check_apt_repos(&mut self) -> Result<(), Error> {
- self.output
- .log_info("Checking for package repository suite mismatches..")?;
-
- let mut strange_suite = false;
- let mut mismatches = Vec::new();
- let mut found_suite: Option<(String, String)> = None;
-
- let (repo_files, _repo_errors, _digest) = repositories::repositories()?;
- for repo_file in repo_files {
- self.check_repo_file(
- &mut found_suite,
- &mut mismatches,
- &mut strange_suite,
- repo_file,
- )?;
- }
-
- match (mismatches.is_empty(), strange_suite) {
- (true, false) => self.output.log_pass("found no suite mismatch")?,
- (true, true) => self
- .output
- .log_notice("found no suite mismatches, but found at least one strange suite")?,
- (false, _) => {
- let mut message = String::from(
- "Found mixed old and new packages repository suites, fix before upgrading!\
- \n Mismatches:",
- );
- for (suite, location) in mismatches.iter() {
- message.push_str(
- format!("\n found suite '{suite}' at '{location}'").as_str(),
- );
- }
- message.push('\n');
- self.output.log_fail(message)?
- }
- }
-
- Ok(())
- }
-
- fn check_dkms_modules(&mut self) -> Result<(), Error> {
- let kver = std::process::Command::new("uname")
- .arg("-r")
- .output()
- .map_err(|err| format_err!("failed to retrieve running kernel version - {err}"))?;
-
- let output = std::process::Command::new("dkms")
- .arg("status")
- .arg("-k")
- .arg(std::str::from_utf8(&kver.stdout)?)
- .output();
- match output {
- Err(_err) => self.output.log_skip("could not get dkms status")?,
- Ok(ret) => {
- let num_dkms_modules = std::str::from_utf8(&ret.stdout)?.lines().count();
- if num_dkms_modules == 0 {
- self.output.log_pass("no dkms modules found")?;
- } else {
- self.output
- .log_warn("dkms modules found, this might cause issues during upgrade.")?;
- }
- }
- }
- Ok(())
- }
-
- pub fn check_misc(&mut self) -> Result<(), Error> {
- self.output.print_header("MISCELLANEOUS CHECKS")?;
- self.check_pbs_services()?;
- self.check_time_sync()?;
- self.check_apt_repos()?;
- self.check_bootloader()?;
- self.check_dkms_modules()?;
- Ok(())
- }
-
- pub fn summary(&mut self) -> Result<(), Error> {
- self.output.print_summary()
- }
-
- fn check_repo_file(
- &mut self,
- found_suite: &mut Option<(String, String)>,
- mismatches: &mut Vec<(String, String)>,
- strange_suite: &mut bool,
- repo_file: APTRepositoryFile,
- ) -> Result<(), Error> {
- for repo in repo_file.repositories {
- if !repo.enabled || repo.types == [APTRepositoryPackageType::DebSrc] {
- continue;
- }
- for suite in &repo.suites {
- let suite = match suite.find(&['-', '/'][..]) {
- Some(n) => &suite[0..n],
- None => suite,
- };
-
- if suite != OLD_SUITE && suite != NEW_SUITE {
- let location = repo_file.path.clone().unwrap_or_default();
- self.output.log_notice(format!(
- "found unusual suite '{suite}', neither old '{OLD_SUITE}' nor new \
- '{NEW_SUITE}'..\n Affected file {location}\n Please \
- assure this is shipping compatible packages for the upgrade!"
- ))?;
- *strange_suite = true;
- continue;
- }
-
- if let Some((ref current_suite, ref current_location)) = found_suite {
- let location = repo_file.path.clone().unwrap_or_default();
- if suite != current_suite {
- if mismatches.is_empty() {
- mismatches.push((current_suite.clone(), current_location.clone()));
- mismatches.push((suite.to_string(), location));
- } else {
- mismatches.push((suite.to_string(), location));
- }
- }
- } else {
- let location = repo_file.path.clone().unwrap_or_default();
- *found_suite = Some((suite.to_string(), location));
- }
- }
- }
- Ok(())
- }
-
- fn get_systemd_unit_state(
- &mut self,
- unit: &str,
- ) -> Result<(SystemdUnitState, SystemdUnitState), Error> {
- let output = std::process::Command::new("systemctl")
- .arg("is-enabled")
- .arg(unit)
- .output()
- .map_err(|err| format_err!("failed to execute - {err}"))?;
-
- let enabled_state = match output.stdout.as_slice() {
- b"enabled\n" => SystemdUnitState::Enabled,
- b"disabled\n" => SystemdUnitState::Disabled,
- _ => SystemdUnitState::Unknown,
- };
-
- let output = std::process::Command::new("systemctl")
- .arg("is-active")
- .arg(unit)
- .output()
- .map_err(|err| format_err!("failed to execute - {err}"))?;
-
- let active_state = match output.stdout.as_slice() {
- b"active\n" => SystemdUnitState::Active,
- b"inactive\n" => SystemdUnitState::Inactive,
- b"failed\n" => SystemdUnitState::Failed,
- _ => SystemdUnitState::Unknown,
- };
- Ok((enabled_state, active_state))
- }
-
- fn check_pbs_services(&mut self) -> Result<(), Error> {
- self.output.log_info("Checking PBS daemon services..")?;
-
- for service in ["proxmox-backup.service", "proxmox-backup-proxy.service"] {
- match self.get_systemd_unit_state(service)? {
- (_, SystemdUnitState::Active) => {
- self.output
- .log_pass(format!("systemd unit '{service}' is in state 'active'"))?;
- }
- (_, SystemdUnitState::Inactive) => {
- self.output.log_fail(format!(
- "systemd unit '{service}' is in state 'inactive'\
- \n Please check the service for errors and start it.",
- ))?;
- }
- (_, SystemdUnitState::Failed) => {
- self.output.log_fail(format!(
- "systemd unit '{service}' is in state 'failed'\
- \n Please check the service for errors and start it.",
- ))?;
- }
- (_, _) => {
- self.output.log_fail(format!(
- "systemd unit '{service}' is not in state 'active'\
- \n Please check the service for errors and start it.",
- ))?;
- }
- }
- }
- Ok(())
- }
-
- fn check_time_sync(&mut self) -> Result<(), Error> {
- self.output
- .log_info("Checking for supported & active NTP service..")?;
- if self.get_systemd_unit_state("systemd-timesyncd.service")?.1 == SystemdUnitState::Active {
- self.output.log_warn(
- "systemd-timesyncd is not the best choice for time-keeping on servers, due to only \
- applying updates on boot.\
- \n While not necessary for the upgrade it's recommended to use one of:\
- \n * chrony (Default in new Proxmox Backup Server installations)\
- \n * ntpsec\
- \n * openntpd"
- )?;
- } else if self.get_systemd_unit_state("ntp.service")?.1 == SystemdUnitState::Active {
- self.output.log_info(
- "Debian deprecated and removed the ntp package for Bookworm, but the system \
- will automatically migrate to the 'ntpsec' replacement package on upgrade.",
- )?;
- } else if self.get_systemd_unit_state("chrony.service")?.1 == SystemdUnitState::Active
- || self.get_systemd_unit_state("openntpd.service")?.1 == SystemdUnitState::Active
- || self.get_systemd_unit_state("ntpsec.service")?.1 == SystemdUnitState::Active
- {
- self.output
- .log_pass("Detected active time synchronisation unit")?;
- } else {
- self.output.log_warn(
- "No (active) time synchronisation daemon (NTP) detected, but synchronized systems \
- are important!",
- )?;
- }
- Ok(())
- }
-}
-
-#[derive(PartialEq)]
-enum SystemdUnitState {
- Active,
- Enabled,
- Disabled,
- Failed,
- Inactive,
- Unknown,
-}
-
-#[derive(Default)]
-struct Counters {
- pass: u64,
- skip: u64,
- notice: u64,
- warn: u64,
- fail: u64,
-}
-
-enum LogLevel {
- Pass,
- Info,
- Skip,
- Notice,
- Warn,
- Fail,
-}
-
-struct ConsoleOutput {
- stream: StandardStream,
- first_header: bool,
- counters: Counters,
-}
-
-impl ConsoleOutput {
- pub fn new() -> Self {
- Self {
- stream: StandardStream::stdout(ColorChoice::Always),
- first_header: true,
- counters: Counters::default(),
- }
- }
-
- pub fn print_header(&mut self, message: &str) -> Result<(), Error> {
- if !self.first_header {
- writeln!(&mut self.stream)?;
- }
- self.first_header = false;
- writeln!(&mut self.stream, "= {message} =\n")?;
- Ok(())
- }
-
- pub fn set_color(&mut self, color: Color, bold: bool) -> Result<(), Error> {
- self.stream
- .set_color(ColorSpec::new().set_fg(Some(color)).set_bold(bold))?;
- Ok(())
- }
-
- pub fn reset(&mut self) -> Result<(), std::io::Error> {
- self.stream.reset()
- }
-
- pub fn log_line(&mut self, level: LogLevel, message: &str) -> Result<(), Error> {
- match level {
- LogLevel::Pass => {
- self.counters.pass += 1;
- self.set_color(Color::Green, false)?;
- writeln!(&mut self.stream, "PASS: {}", message)?;
- }
- LogLevel::Info => {
- writeln!(&mut self.stream, "INFO: {}", message)?;
- }
- LogLevel::Skip => {
- self.counters.skip += 1;
- writeln!(&mut self.stream, "SKIP: {}", message)?;
- }
- LogLevel::Notice => {
- self.counters.notice += 1;
- self.set_color(Color::White, true)?;
- writeln!(&mut self.stream, "NOTICE: {}", message)?;
- }
- LogLevel::Warn => {
- self.counters.warn += 1;
- self.set_color(Color::Yellow, false)?;
- writeln!(&mut self.stream, "WARN: {}", message)?;
- }
- LogLevel::Fail => {
- self.counters.fail += 1;
- self.set_color(Color::Red, true)?;
- writeln!(&mut self.stream, "FAIL: {}", message)?;
- }
- }
- self.reset()?;
- Ok(())
- }
-
- pub fn log_pass<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
- self.log_line(LogLevel::Pass, message.as_ref())
- }
-
- pub fn log_info<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
- self.log_line(LogLevel::Info, message.as_ref())
- }
-
- pub fn log_skip<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
- self.log_line(LogLevel::Skip, message.as_ref())
- }
-
- pub fn log_notice<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
- self.log_line(LogLevel::Notice, message.as_ref())
- }
-
- pub fn log_warn<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
- self.log_line(LogLevel::Warn, message.as_ref())
- }
-
- pub fn log_fail<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
- self.log_line(LogLevel::Fail, message.as_ref())
- }
-
- pub fn print_summary(&mut self) -> Result<(), Error> {
- self.print_header("SUMMARY")?;
-
- let total = self.counters.fail
- + self.counters.pass
- + self.counters.notice
- + self.counters.skip
- + self.counters.warn;
-
- writeln!(&mut self.stream, "TOTAL: {total}")?;
- self.set_color(Color::Green, false)?;
- writeln!(&mut self.stream, "PASSED: {}", self.counters.pass)?;
- self.reset()?;
- writeln!(&mut self.stream, "SKIPPED: {}", self.counters.skip)?;
- writeln!(&mut self.stream, "NOTICE: {}", self.counters.notice)?;
- if self.counters.warn > 0 {
- self.set_color(Color::Yellow, false)?;
- writeln!(&mut self.stream, "WARNINGS: {}", self.counters.warn)?;
- }
- if self.counters.fail > 0 {
- self.set_color(Color::Red, true)?;
- writeln!(&mut self.stream, "FAILURES: {}", self.counters.fail)?;
- }
- if self.counters.warn > 0 || self.counters.fail > 0 {
- let (color, bold) = if self.counters.fail > 0 {
- (Color::Red, true)
- } else {
- (Color::Yellow, false)
- };
-
- self.set_color(color, bold)?;
- writeln!(
- &mut self.stream,
- "\nATTENTION: Please check the output for detailed information!",
- )?;
- if self.counters.fail > 0 {
- writeln!(
- &mut self.stream,
- "Try to solve the problems one at a time and rerun this checklist tool again.",
- )?;
- }
- }
- self.reset()?;
- Ok(())
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- fn test_is_kernel_version_compatible(
- expected_versions: &[&str],
- unexpected_versions: &[&str],
- upgraded: bool,
- ) {
- let checker = Checker {
- output: ConsoleOutput::new(),
- upgraded,
- };
-
- for version in expected_versions {
- assert!(
- checker.is_kernel_version_compatible(version),
- "compatible kernel version '{version}' did not pass as expected!"
- );
- }
- for version in unexpected_versions {
- assert!(
- !checker.is_kernel_version_compatible(version),
- "incompatible kernel version '{version}' passed as expected!"
- );
- }
- }
-
- #[test]
- fn test_before_upgrade_kernel_version_compatibility() {
- let expected_versions = &["6.2.16-20-pve", "6.5.13-6-pve", "6.8.12-1-pve"];
- let unexpected_versions = &["6.1.10-1-pve", "5.19.17-2-pve"];
-
- test_is_kernel_version_compatible(expected_versions, unexpected_versions, false);
- }
-
- #[test]
- fn test_after_upgrade_kernel_version_compatibility() {
- let expected_versions = &["6.14.6-1-pve", "6.17.0-1-pve"];
- let unexpected_versions = &["6.12.1-1-pve", "6.2.1-1-pve"];
-
- test_is_kernel_version_compatible(expected_versions, unexpected_versions, true);
- }
+fn main() -> Result<(), anyhow::Error> {
+ UpgradeCheckerBuilder::new(
+ "bookworm",
+ "trixie",
+ "proxmox-backup",
+ 3,
+ 4,
+ 0,
+ &format!("{PROXMOX_PKG_VERSION}.{PROXMOX_PKG_RELEASE}"),
+ )
+ .apt_state_file_location(APT_PKG_STATE_FN)
+ .add_service_to_checks("proxmox-backup.service")
+ .add_service_to_checks("proxmox-backup-proxy.service")
+ .build()
+ .run()
}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 4+ messages in thread
* [pdm-devel] [PATCH datacenter-manager 1/1] lib: move proxmox-upgrade-checks into common crate and depend on that
2025-09-17 12:18 [pdm-devel] [PATCH datacenter-manager/proxmox{, -backup} 0/3] move upgrade checks to a common crate under proxmox-rs Shannon Sterz
2025-09-17 12:18 ` [pdm-devel] [PATCH proxmox 1/1] upgrade-checks: add upgrade checker crate Shannon Sterz
2025-09-17 12:18 ` [pdm-devel] [PATCH proxmox-backup 1/1] pbs3to4: move upgrade check script to use the new common crate Shannon Sterz
@ 2025-09-17 12:18 ` Shannon Sterz
2 siblings, 0 replies; 4+ messages in thread
From: Shannon Sterz @ 2025-09-17 12:18 UTC (permalink / raw)
To: pdm-devel
instead of keeping a local crate here
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
Cargo.toml | 4 +-
lib/proxmox-upgrade-checks/Cargo.toml | 19 -
lib/proxmox-upgrade-checks/src/lib.rs | 847 --------------------------
3 files changed, 2 insertions(+), 868 deletions(-)
delete mode 100644 lib/proxmox-upgrade-checks/Cargo.toml
delete mode 100644 lib/proxmox-upgrade-checks/src/lib.rs
diff --git a/Cargo.toml b/Cargo.toml
index 3146e7d..e063d40 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -21,7 +21,6 @@ members = [
"lib/pdm-config",
"lib/pdm-search",
"lib/pdm-ui-shared",
- "lib/proxmox-upgrade-checks",
"cli/client",
"cli/admin",
@@ -63,6 +62,7 @@ proxmox-sys = "1"
proxmox-systemd = "1"
proxmox-tfa = { version = "6", features = [ "api-types" ], default-features = false }
proxmox-time = "2"
+proxmox-upgrade-checks = "1"
proxmox-uuid = "1"
# other proxmox crates
@@ -92,7 +92,6 @@ pdm-client = { version = "0.9", path = "lib/pdm-client" }
pdm-search = { version = "0.2", path = "lib/pdm-search" }
pdm-ui-shared = { version = "0.9", path = "lib/pdm-ui-shared" }
proxmox-fido2 = { path = "cli/proxmox-fido2" }
-proxmox-upgrade-checks = { path = "lib/proxmox-upgrade-checks" }
# regular crates
anyhow = "1.0"
@@ -181,5 +180,6 @@ zstd = { version = "0.13" }
# proxmox-tfa = { path = "../proxmox/proxmox-tfa" }
# proxmox-time-api = { path = "../proxmox/proxmox-time-api" }
# proxmox-time = { path = "../proxmox/proxmox-time" }
+# proxmox-upgrade-checks = { path = "../proxmox/proxmox-upgrade-checks" }
# proxmox-uuid = { path = "../proxmox/proxmox-uuid" }
# proxmox-worker-task = { path = "../proxmox/proxmox-worker-task" }
diff --git a/lib/proxmox-upgrade-checks/Cargo.toml b/lib/proxmox-upgrade-checks/Cargo.toml
deleted file mode 100644
index 574582e..0000000
--- a/lib/proxmox-upgrade-checks/Cargo.toml
+++ /dev/null
@@ -1,19 +0,0 @@
-[package]
-name = "proxmox-upgrade-checks"
-description = "Helpers to implement upgrade checks for Proxmox products."
-version = "0.1.0"
-
-authors.workspace = true
-edition.workspace = true
-license.workspace = true
-repository.workspace = true
-exclude.workspace = true
-
-[dependencies]
-anyhow.workspace = true
-const_format.workspace = true
-regex.workspace = true
-termcolor.workspace = true
-
-proxmox-apt = { workspace = true, features = [ "cache" ] }
-proxmox-apt-api-types.workspace = true
diff --git a/lib/proxmox-upgrade-checks/src/lib.rs b/lib/proxmox-upgrade-checks/src/lib.rs
deleted file mode 100644
index d894f77..0000000
--- a/lib/proxmox-upgrade-checks/src/lib.rs
+++ /dev/null
@@ -1,847 +0,0 @@
-use std::path::Path;
-use std::{io::Write, path::PathBuf};
-
-use anyhow::{format_err, Error};
-use const_format::concatcp;
-use regex::Regex;
-use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
-
-use proxmox_apt::repositories;
-use proxmox_apt_api_types::{APTRepositoryFile, APTRepositoryPackageType};
-
-/// Easily create and configure an upgrade checker for Proxmox products.
-pub struct UpgradeCheckerBuilder {
- old_suite: String,
- new_suite: String,
- meta_package_name: String,
- minimum_major_version: u8,
- minimum_minor_version: u8,
- minimum_pkgrel: u8,
- apt_state_file: Option<PathBuf>,
- api_server_package: Option<String>,
- running_api_server_version: String,
- services_list: Vec<String>,
-}
-
-impl UpgradeCheckerBuilder {
- /// Create a new UpgradeCheckerBuilder
- ///
- /// * `old_suite`: The Debian suite before the upgrade.
- /// * `new_suite`: The Debian suite after the upgrade.
- /// * `meta_package_name`: The name of the product's meta package.
- /// * `minimum_major_version`: The minimum major version before the upgrade.
- /// * `minimum_minor_version`: The minimum minor version before the upgrade.
- /// * `minimum_pkgrel`: The minimum package release before the upgrade.
- /// * `running_api_server_version`: The currently running API server version.
- pub fn new(
- old_suite: &str,
- new_suite: &str,
- meta_package_name: &str,
- minimum_major_version: u8,
- minimum_minor_version: u8,
- minimum_pkgrel: u8,
- running_api_server_version: &str,
- ) -> UpgradeCheckerBuilder {
- UpgradeCheckerBuilder {
- old_suite: old_suite.into(),
- new_suite: new_suite.into(),
- meta_package_name: meta_package_name.into(),
- minimum_major_version,
- minimum_minor_version,
- minimum_pkgrel,
- apt_state_file: None,
- api_server_package: None,
- running_api_server_version: running_api_server_version.into(),
- services_list: Vec::new(),
- }
- }
-
- /// Builder-style method to set the location of the APT state file.
- pub fn apt_state_file_location<P: AsRef<Path>>(mut self, path: P) -> Self {
- self.apt_state_file = Some(path.as_ref().into());
- self
- }
-
- /// Builder-style method to set the API server package name.
- pub fn api_server_package(mut self, api_server_package: &str) -> Self {
- self.api_server_package = Some(api_server_package.into());
- self
- }
-
- /// Add a service to the list of services that will be checked.
- pub fn add_service_to_checks(mut self, service_name: &str) -> Self {
- self.services_list.push(service_name.into());
- self
- }
-
- /// Construct the UpgradeChecker, consumes the UpgradeCheckerBuilder
- pub fn build(mut self) -> UpgradeChecker {
- UpgradeChecker {
- output: ConsoleOutput::new(),
- upgraded: false,
- old_suite: self.old_suite,
- new_suite: self.new_suite,
- minimum_major_version: self.minimum_major_version,
- minimum_minor_version: self.minimum_minor_version,
- minimum_pkgrel: self.minimum_pkgrel,
- apt_state_file: self.apt_state_file.take().unwrap_or_else(|| {
- PathBuf::from(format!(
- "/var/lib/{}/pkg-state.json",
- &self.meta_package_name
- ))
- }),
- api_server_package: self
- .api_server_package
- .unwrap_or_else(|| self.meta_package_name.clone()),
- running_api_server_version: self.running_api_server_version,
- meta_package_name: self.meta_package_name,
- services_list: self.services_list,
- }
- }
-}
-
-/// Helpers to easily construct a set of upgrade checks.
-pub struct UpgradeChecker {
- output: ConsoleOutput,
- upgraded: bool,
- old_suite: String,
- new_suite: String,
- meta_package_name: String,
- minimum_major_version: u8,
- minimum_minor_version: u8,
- minimum_pkgrel: u8,
- apt_state_file: PathBuf,
- api_server_package: String,
- running_api_server_version: String,
- services_list: Vec<String>,
-}
-
-impl UpgradeChecker {
- /// Run all checks.
- pub fn run(&mut self) -> Result<(), Error> {
- self.check_packages()?;
- self.check_misc()?;
- self.summary()
- }
-
- /// Run miscellaneous checks.
- pub fn check_misc(&mut self) -> Result<(), Error> {
- self.output.print_header("MISCELLANEOUS CHECKS")?;
- self.check_services()?;
- self.check_time_sync()?;
- self.check_apt_repos()?;
- self.check_bootloader()?;
- self.check_dkms_modules()?;
- Ok(())
- }
-
- /// Print a summary of all checks run so far.
- pub fn summary(&mut self) -> Result<(), Error> {
- self.output.print_summary()
- }
-
- /// Run all package related checks.
- pub fn check_packages(&mut self) -> Result<(), Error> {
- self.output.print_header(&format!(
- "CHECKING VERSION INFORMATION FOR {} PACKAGES",
- self.meta_package_name.to_uppercase()
- ))?;
-
- self.check_upgradable_packages()?;
-
- let pkg_versions = proxmox_apt::get_package_versions(
- &self.meta_package_name,
- &self.api_server_package,
- &self.running_api_server_version,
- &[],
- )?;
-
- self.check_meta_package_version(&pkg_versions)?;
- self.check_kernel_compat(&pkg_versions)?;
- Ok(())
- }
-
- fn check_upgradable_packages(&mut self) -> Result<(), Error> {
- self.output.log_info("Checking for package updates..")?;
-
- let result = proxmox_apt::list_available_apt_update(&self.apt_state_file);
- match result {
- Err(err) => {
- self.output.log_warn(format!("{err}"))?;
- self.output
- .log_fail("unable to retrieve list of package updates!")?;
- }
- Ok(package_status) => {
- if package_status.is_empty() {
- self.output.log_pass("all packages up-to-date")?;
- } else {
- let pkgs = package_status
- .iter()
- .map(|pkg| pkg.package.clone())
- .collect::<Vec<String>>()
- .join(", ");
- self.output.log_warn(format!(
- "updates for the following packages are available:\n {pkgs}",
- ))?;
- }
- }
- }
- Ok(())
- }
-
- fn check_meta_package_version(
- &mut self,
- pkg_versions: &[proxmox_apt_api_types::APTUpdateInfo],
- ) -> Result<(), Error> {
- self.output.log_info(format!(
- "Checking {} package version..",
- self.meta_package_name
- ))?;
-
- let meta_pkg = pkg_versions
- .iter()
- .find(|pkg| pkg.package.as_str() == self.meta_package_name);
-
- if let Some(meta_pkg) = meta_pkg {
- let pkg_version = Regex::new(r"^(\d+)\.(\d+)[.-](\d+)")?;
- let captures = pkg_version.captures(&meta_pkg.old_version);
- if let Some(captures) = captures {
- let maj = Self::extract_version_from_captures(1, &captures)?;
- let min = Self::extract_version_from_captures(2, &captures)?;
- let pkgrel = Self::extract_version_from_captures(3, &captures)?;
-
- let min_version = format!(
- "{}.{}.{}",
- self.minimum_major_version, self.minimum_minor_version, self.minimum_pkgrel
- );
-
- if (maj > self.minimum_major_version && self.minimum_major_version != 0)
- // Handle alpha and beta version upgrade checks:
- || (self.minimum_major_version == 0 && min > self.minimum_minor_version)
- {
- self.output
- .log_pass(format!("Already upgraded to {maj}.{min}"))?;
- self.upgraded = true;
- } else if maj >= self.minimum_major_version
- && min >= self.minimum_minor_version
- && pkgrel >= self.minimum_pkgrel
- {
- self.output.log_pass(format!(
- "'{}' has version >= {min_version}",
- self.meta_package_name
- ))?;
- } else {
- self.output.log_fail(format!(
- "'{}' package is too old, please upgrade to >= {min_version}",
- self.meta_package_name
- ))?;
- }
- } else {
- self.output.log_fail(format!(
- "could not match the '{}' package version, \
- is it installed?",
- self.meta_package_name
- ))?;
- }
- } else {
- self.output
- .log_fail(format!("'{}' package not found!", self.meta_package_name))?;
- }
- Ok(())
- }
-
- fn is_kernel_version_compatible(&self, running_version: &str) -> bool {
- // TODO: rework this to parse out maj.min.patch and do numerical comparison and detect
- // those with a "bpo12" as backport kernels from the older release.
- const MINIMUM_RE: &str = r"6\.(?:14\.(?:[1-9]\d+|[6-9])|1[5-9])[^~]*";
- const ARBITRARY_RE: &str = r"(?:1[4-9]|2\d+)\.(?:[0-9]|\d{2,})[^~]*-pve";
-
- let re = if self.upgraded {
- concatcp!(r"^(?:", MINIMUM_RE, r"|", ARBITRARY_RE, r")$")
- } else {
- r"^(?:6\.(?:2|5|8|11|14))"
- };
- let re = Regex::new(re).expect("failed to compile kernel compat regex");
-
- re.is_match(running_version)
- }
-
- fn check_kernel_compat(
- &mut self,
- pkg_versions: &[proxmox_apt_api_types::APTUpdateInfo],
- ) -> Result<(), Error> {
- self.output.log_info("Check running kernel version..")?;
-
- let kinstalled = if self.upgraded {
- "proxmox-kernel-6.14"
- } else {
- "proxmox-kernel-6.8"
- };
-
- let output = std::process::Command::new("uname").arg("-r").output();
- match output {
- Err(_err) => self
- .output
- .log_fail("unable to determine running kernel version.")?,
- Ok(ret) => {
- let running_version = std::str::from_utf8(&ret.stdout[..ret.stdout.len() - 1])?;
- if self.is_kernel_version_compatible(running_version) {
- if self.upgraded {
- self.output.log_pass(format!(
- "running new kernel '{running_version}' after upgrade."
- ))?;
- } else {
- self.output.log_pass(format!(
- "running kernel '{running_version}' is considered suitable for \
- upgrade."
- ))?;
- }
- } else {
- let installed_kernel = pkg_versions
- .iter()
- .find(|pkg| pkg.package.as_str() == kinstalled);
- if installed_kernel.is_some() {
- self.output.log_warn(format!(
- "a suitable kernel '{kinstalled}' is installed, but an \
- unsuitable '{running_version}' is booted, missing reboot?!",
- ))?;
- } else {
- self.output.log_warn(format!(
- "unexpected running and installed kernel '{running_version}'.",
- ))?;
- }
- }
- }
- }
- Ok(())
- }
-
- fn extract_version_from_captures(
- index: usize,
- captures: ®ex::Captures,
- ) -> Result<u8, Error> {
- if let Some(capture) = captures.get(index) {
- let val = capture.as_str().parse::<u8>()?;
- Ok(val)
- } else {
- Ok(0)
- }
- }
-
- fn check_bootloader(&mut self) -> Result<(), Error> {
- self.output
- .log_info("Checking bootloader configuration...")?;
-
- let sd_boot_installed =
- Path::new("/usr/share/doc/systemd-boot/changelog.Debian.gz").is_file();
-
- if !Path::new("/sys/firmware/efi").is_dir() {
- if sd_boot_installed {
- self.output.log_warn(
- "systemd-boot package installed on legacy-boot system is not \
- necessary, consider removing it",
- )?;
- return Ok(());
- }
- self.output
- .log_skip("System booted in legacy-mode - no need for additional packages.")?;
- return Ok(());
- }
-
- let mut boot_ok = true;
- if Path::new("/etc/kernel/proxmox-boot-uuids").is_file() {
- // Package version check needs to be run before
- if !self.upgraded {
- let output = std::process::Command::new("proxmox-boot-tool")
- .arg("status")
- .output()
- .map_err(|err| {
- format_err!("failed to retrieve proxmox-boot-tool status - {err}")
- })?;
- let re = Regex::new(r"configured with:.* (uefi|systemd-boot) \(versions:")
- .expect("failed to proxmox-boot-tool status");
- if re.is_match(std::str::from_utf8(&output.stdout)?) {
- self.output
- .log_skip("not yet upgraded, systemd-boot still needed for bootctl")?;
- return Ok(());
- }
- }
- } else {
- if !Path::new("/usr/share/doc/grub-efi-amd64/changelog.Debian.gz").is_file() {
- self.output.log_warn(
- "System booted in uefi mode but grub-efi-amd64 meta-package not installed, \
- new grub versions will not be installed to /boot/efi!
- Install grub-efi-amd64.",
- )?;
- boot_ok = false;
- }
- if Path::new("/boot/efi/EFI/BOOT/BOOTX64.efi").is_file() {
- let output = std::process::Command::new("debconf-show")
- .arg("--db")
- .arg("configdb")
- .arg("grub-efi-amd64")
- .arg("grub-pc")
- .output()
- .map_err(|err| format_err!("failed to retrieve debconf settings - {err}"))?;
- let re = Regex::new(r"grub2/force_efi_extra_removable: +true(?:\n|$)")
- .expect("failed to compile dbconfig regex");
- if !re.is_match(std::str::from_utf8(&output.stdout)?) {
- self.output.log_warn(
- "Removable bootloader found at '/boot/efi/EFI/BOOT/BOOTX64.efi', but GRUB packages \
- not set up to update it!\nRun the following command:\n\
- echo 'grub-efi-amd64 grub2/force_efi_extra_removable boolean true' | debconf-set-selections -v -u\n\
- Then reinstall GRUB with 'apt install --reinstall grub-efi-amd64'"
- )?;
- boot_ok = false;
- }
- }
- }
- if sd_boot_installed {
- self.output.log_fail(
- "systemd-boot meta-package installed. This will cause problems on upgrades of other \
- boot-related packages.\n\
- Remove the 'systemd-boot' package.\n\
- Please consult the upgrade guide for further information!"
- )?;
- boot_ok = false;
- }
- if boot_ok {
- self.output
- .log_pass("bootloader packages installed correctly")?;
- }
- Ok(())
- }
-
- fn check_apt_repos(&mut self) -> Result<(), Error> {
- self.output
- .log_info("Checking for package repository suite mismatches..")?;
-
- let mut strange_suite = false;
- let mut mismatches = Vec::new();
- let mut found_suite: Option<(String, String)> = None;
-
- let (repo_files, _repo_errors, _digest) = repositories::repositories()?;
- for repo_file in repo_files {
- self.check_repo_file(
- &mut found_suite,
- &mut mismatches,
- &mut strange_suite,
- repo_file,
- )?;
- }
-
- match (mismatches.is_empty(), strange_suite) {
- (true, false) => self.output.log_pass("found no suite mismatch")?,
- (true, true) => self
- .output
- .log_notice("found no suite mismatches, but found at least one strange suite")?,
- (false, _) => {
- let mut message = String::from(
- "Found mixed old and new packages repository suites, fix before upgrading!\
- \n Mismatches:",
- );
- for (suite, location) in mismatches.iter() {
- message.push_str(
- format!("\n found suite '{suite}' at '{location}'").as_str(),
- );
- }
- message.push('\n');
- self.output.log_fail(message)?
- }
- }
-
- Ok(())
- }
-
- fn check_dkms_modules(&mut self) -> Result<(), Error> {
- let kver = std::process::Command::new("uname")
- .arg("-r")
- .output()
- .map_err(|err| format_err!("failed to retrieve running kernel version - {err}"))?;
-
- let output = std::process::Command::new("dkms")
- .arg("status")
- .arg("-k")
- .arg(std::str::from_utf8(&kver.stdout)?)
- .output();
- match output {
- Err(_err) => self.output.log_skip("could not get dkms status")?,
- Ok(ret) => {
- let num_dkms_modules = std::str::from_utf8(&ret.stdout)?.lines().count();
- if num_dkms_modules == 0 {
- self.output.log_pass("no dkms modules found")?;
- } else {
- self.output
- .log_warn("dkms modules found, this might cause issues during upgrade.")?;
- }
- }
- }
- Ok(())
- }
-
- fn check_repo_file(
- &mut self,
- found_suite: &mut Option<(String, String)>,
- mismatches: &mut Vec<(String, String)>,
- strange_suite: &mut bool,
- repo_file: APTRepositoryFile,
- ) -> Result<(), Error> {
- for repo in repo_file.repositories {
- if !repo.enabled || repo.types == [APTRepositoryPackageType::DebSrc] {
- continue;
- }
- for suite in &repo.suites {
- let suite = match suite.find(&['-', '/'][..]) {
- Some(n) => &suite[0..n],
- None => suite,
- };
-
- if suite != self.old_suite && suite != self.new_suite {
- let location = repo_file.path.clone().unwrap_or_default();
- self.output.log_notice(format!(
- "found unusual suite '{suite}', neither old '{}' nor new \
- '{}'..\n Affected file {location}\n Please \
- assure this is shipping compatible packages for the upgrade!",
- self.old_suite, self.new_suite
- ))?;
- *strange_suite = true;
- continue;
- }
-
- if let Some((ref current_suite, ref current_location)) = found_suite {
- let location = repo_file.path.clone().unwrap_or_default();
- if suite != current_suite {
- if mismatches.is_empty() {
- mismatches.push((current_suite.clone(), current_location.clone()));
- mismatches.push((suite.to_string(), location));
- } else {
- mismatches.push((suite.to_string(), location));
- }
- }
- } else {
- let location = repo_file.path.clone().unwrap_or_default();
- *found_suite = Some((suite.to_string(), location));
- }
- }
- }
- Ok(())
- }
-
- fn get_systemd_unit_state(
- &self,
- unit: &str,
- ) -> Result<(SystemdUnitState, SystemdUnitState), Error> {
- let output = std::process::Command::new("systemctl")
- .arg("is-enabled")
- .arg(unit)
- .output()
- .map_err(|err| format_err!("failed to execute - {err}"))?;
-
- let enabled_state = match output.stdout.as_slice() {
- b"enabled\n" => SystemdUnitState::Enabled,
- b"disabled\n" => SystemdUnitState::Disabled,
- _ => SystemdUnitState::Unknown,
- };
-
- let output = std::process::Command::new("systemctl")
- .arg("is-active")
- .arg(unit)
- .output()
- .map_err(|err| format_err!("failed to execute - {err}"))?;
-
- let active_state = match output.stdout.as_slice() {
- b"active\n" => SystemdUnitState::Active,
- b"inactive\n" => SystemdUnitState::Inactive,
- b"failed\n" => SystemdUnitState::Failed,
- _ => SystemdUnitState::Unknown,
- };
- Ok((enabled_state, active_state))
- }
-
- fn check_services(&mut self) -> Result<(), Error> {
- self.output.log_info(format!(
- "Checking {} daemon services..",
- self.meta_package_name
- ))?;
-
- for service in self.services_list.as_slice() {
- match self.get_systemd_unit_state(service)? {
- (_, SystemdUnitState::Active) => {
- self.output
- .log_pass(format!("systemd unit '{service}' is in state 'active'"))?;
- }
- (_, SystemdUnitState::Inactive) => {
- self.output.log_fail(format!(
- "systemd unit '{service}' is in state 'inactive'\
- \n Please check the service for errors and start it.",
- ))?;
- }
- (_, SystemdUnitState::Failed) => {
- self.output.log_fail(format!(
- "systemd unit '{service}' is in state 'failed'\
- \n Please check the service for errors and start it.",
- ))?;
- }
- (_, _) => {
- self.output.log_fail(format!(
- "systemd unit '{service}' is not in state 'active'\
- \n Please check the service for errors and start it.",
- ))?;
- }
- }
- }
- Ok(())
- }
-
- fn check_time_sync(&mut self) -> Result<(), Error> {
- self.output
- .log_info("Checking for supported & active NTP service..")?;
- if self.get_systemd_unit_state("systemd-timesyncd.service")?.1 == SystemdUnitState::Active {
- self.output.log_warn(
- "systemd-timesyncd is not the best choice for time-keeping on servers, due to only \
- applying updates on boot.\
- \n While not necessary for the upgrade it's recommended to use one of:\
- \n * chrony (Default in new Proxmox product installations)\
- \n * ntpsec\
- \n * openntpd"
- )?;
- } else if self.get_systemd_unit_state("ntp.service")?.1 == SystemdUnitState::Active {
- self.output.log_info(
- "Debian deprecated and removed the ntp package for Bookworm, but the system \
- will automatically migrate to the 'ntpsec' replacement package on upgrade.",
- )?;
- } else if self.get_systemd_unit_state("chrony.service")?.1 == SystemdUnitState::Active
- || self.get_systemd_unit_state("openntpd.service")?.1 == SystemdUnitState::Active
- || self.get_systemd_unit_state("ntpsec.service")?.1 == SystemdUnitState::Active
- {
- self.output
- .log_pass("Detected active time synchronisation unit")?;
- } else {
- self.output.log_warn(
- "No (active) time synchronisation daemon (NTP) detected, but synchronized systems \
- are important!",
- )?;
- }
- Ok(())
- }
-}
-
-#[derive(PartialEq)]
-enum SystemdUnitState {
- Active,
- Enabled,
- Disabled,
- Failed,
- Inactive,
- Unknown,
-}
-
-#[derive(Default)]
-struct Counters {
- pass: u64,
- skip: u64,
- notice: u64,
- warn: u64,
- fail: u64,
-}
-
-enum LogLevel {
- Pass,
- Info,
- Skip,
- Notice,
- Warn,
- Fail,
-}
-
-struct ConsoleOutput {
- stream: StandardStream,
- first_header: bool,
- counters: Counters,
-}
-
-impl ConsoleOutput {
- fn new() -> Self {
- Self {
- stream: StandardStream::stdout(ColorChoice::Always),
- first_header: true,
- counters: Counters::default(),
- }
- }
-
- fn print_header(&mut self, message: &str) -> Result<(), Error> {
- if !self.first_header {
- writeln!(&mut self.stream)?;
- }
- self.first_header = false;
- writeln!(&mut self.stream, "= {message} =\n")?;
- Ok(())
- }
-
- fn set_color(&mut self, color: Color, bold: bool) -> Result<(), Error> {
- self.stream
- .set_color(ColorSpec::new().set_fg(Some(color)).set_bold(bold))?;
- Ok(())
- }
-
- fn reset(&mut self) -> Result<(), std::io::Error> {
- self.stream.reset()
- }
-
- fn log_line(&mut self, level: LogLevel, message: &str) -> Result<(), Error> {
- match level {
- LogLevel::Pass => {
- self.counters.pass += 1;
- self.set_color(Color::Green, false)?;
- writeln!(&mut self.stream, "PASS: {}", message)?;
- }
- LogLevel::Info => {
- writeln!(&mut self.stream, "INFO: {}", message)?;
- }
- LogLevel::Skip => {
- self.counters.skip += 1;
- writeln!(&mut self.stream, "SKIP: {}", message)?;
- }
- LogLevel::Notice => {
- self.counters.notice += 1;
- self.set_color(Color::White, true)?;
- writeln!(&mut self.stream, "NOTICE: {}", message)?;
- }
- LogLevel::Warn => {
- self.counters.warn += 1;
- self.set_color(Color::Yellow, false)?;
- writeln!(&mut self.stream, "WARN: {}", message)?;
- }
- LogLevel::Fail => {
- self.counters.fail += 1;
- self.set_color(Color::Red, true)?;
- writeln!(&mut self.stream, "FAIL: {}", message)?;
- }
- }
- self.reset()?;
- Ok(())
- }
-
- fn log_pass<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
- self.log_line(LogLevel::Pass, message.as_ref())
- }
-
- fn log_info<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
- self.log_line(LogLevel::Info, message.as_ref())
- }
-
- fn log_skip<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
- self.log_line(LogLevel::Skip, message.as_ref())
- }
-
- fn log_notice<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
- self.log_line(LogLevel::Notice, message.as_ref())
- }
-
- fn log_warn<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
- self.log_line(LogLevel::Warn, message.as_ref())
- }
-
- fn log_fail<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
- self.log_line(LogLevel::Fail, message.as_ref())
- }
-
- fn print_summary(&mut self) -> Result<(), Error> {
- self.print_header("SUMMARY")?;
-
- let total = self.counters.fail
- + self.counters.pass
- + self.counters.notice
- + self.counters.skip
- + self.counters.warn;
-
- writeln!(&mut self.stream, "TOTAL: {total}")?;
- self.set_color(Color::Green, false)?;
- writeln!(&mut self.stream, "PASSED: {}", self.counters.pass)?;
- self.reset()?;
- writeln!(&mut self.stream, "SKIPPED: {}", self.counters.skip)?;
- writeln!(&mut self.stream, "NOTICE: {}", self.counters.notice)?;
- if self.counters.warn > 0 {
- self.set_color(Color::Yellow, false)?;
- writeln!(&mut self.stream, "WARNINGS: {}", self.counters.warn)?;
- }
- if self.counters.fail > 0 {
- self.set_color(Color::Red, true)?;
- writeln!(&mut self.stream, "FAILURES: {}", self.counters.fail)?;
- }
- if self.counters.warn > 0 || self.counters.fail > 0 {
- let (color, bold) = if self.counters.fail > 0 {
- (Color::Red, true)
- } else {
- (Color::Yellow, false)
- };
-
- self.set_color(color, bold)?;
- writeln!(
- &mut self.stream,
- "\nATTENTION: Please check the output for detailed information!",
- )?;
- if self.counters.fail > 0 {
- writeln!(
- &mut self.stream,
- "Try to solve the problems one at a time and rerun this checklist tool again.",
- )?;
- }
- }
- self.reset()?;
- Ok(())
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- fn test_is_kernel_version_compatible(
- expected_versions: &[&str],
- unexpected_versions: &[&str],
- upgraded: bool,
- ) {
- let mut checker = UpgradeCheckerBuilder::new(
- "bookworm",
- "trixie",
- "proxmox-backup",
- 3,
- 4,
- 0,
- "running version: 3.4",
- )
- .build();
-
- checker.upgraded = upgraded;
-
- for version in expected_versions {
- assert!(
- checker.is_kernel_version_compatible(version),
- "compatible kernel version '{version}' did not pass as expected!"
- );
- }
- for version in unexpected_versions {
- assert!(
- !checker.is_kernel_version_compatible(version),
- "incompatible kernel version '{version}' passed as expected!"
- );
- }
- }
-
- #[test]
- fn test_before_upgrade_kernel_version_compatibility() {
- let expected_versions = &["6.2.16-20-pve", "6.5.13-6-pve", "6.8.12-1-pve"];
- let unexpected_versions = &["6.1.10-1-pve", "5.19.17-2-pve"];
-
- test_is_kernel_version_compatible(expected_versions, unexpected_versions, false);
- }
-
- #[test]
- fn test_after_upgrade_kernel_version_compatibility() {
- let expected_versions = &["6.14.6-1-pve", "6.17.0-1-pve"];
- let unexpected_versions = &["6.12.1-1-pve", "6.2.1-1-pve"];
-
- test_is_kernel_version_compatible(expected_versions, unexpected_versions, true);
- }
-}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 4+ messages in thread