* [pdm-devel] [PATCH datacenter-manager/proxmox{, -backup} 0/3] move upgrade checks to a common crate under proxmox-rs
@ 2025-09-17 12:18 Shannon Sterz
2025-09-17 12:18 ` [pdm-devel] [PATCH proxmox 1/1] upgrade-checks: add upgrade checker crate Shannon Sterz
` (2 more replies)
0 siblings, 3 replies; 9+ messages in thread
From: Shannon Sterz @ 2025-09-17 12:18 UTC (permalink / raw)
To: pdm-devel
these three patches move the largely identical upgrade checks between
pbs and pdm to a common crate in the proxmox repository. the first
check adds the `proxmox-upgrade-checks` crate there. following are two
commits adapting the pbs3to4 script to use the new common crate and
change pdm's dependencies to use the common crate.
i did not adapt pdm2to3 as pbs 2 is eol for over a year now and we may
want to think about removing pbs2to3 from the master branch outright.
proxmox:
Shannon Sterz (1):
upgrade-checks: add upgrade checker crate
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
proxmox-backup:
Shannon Sterz (1):
pbs3to4: move upgrade check script to use the new common crate
Cargo.toml | 3 +
src/bin/pbs3to4.rs | 738 ++-------------------------------------------
2 files changed, 20 insertions(+), 721 deletions(-)
proxmox-datacenter-manager:
Shannon Sterz (1):
lib: move proxmox-upgrade-checks into common crate and depend on that
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
Summary over all repositories:
12 files changed, 968 insertions(+), 1589 deletions(-)
--
Generated by git-murpp 0.8.1
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 9+ messages in thread
* [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; 9+ 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] 9+ 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; 9+ 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] 9+ 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; 9+ 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] 9+ messages in thread
* Re: [pdm-devel] [PATCH proxmox 1/1] upgrade-checks: add upgrade checker crate
2025-09-08 8:39 ` Shannon Sterz
@ 2025-09-08 9:03 ` Thomas Lamprecht
0 siblings, 0 replies; 9+ messages in thread
From: Thomas Lamprecht @ 2025-09-08 9:03 UTC (permalink / raw)
To: Proxmox Datacenter Manager development discussion, Shannon Sterz,
Michael Köppl
Am 08.09.25 um 10:39 schrieb Shannon Sterz:
> On Fri Sep 5, 2025 at 4:08 PM CEST, Michael Köppl wrote:
>> This seems to be missing the directory and files for Debian packaging.
>> Or is this not supposed to be packaged (at least for now)?
> ah sorry yeah missed that in my haste, will add that in a v2.
I do not want to encourage adding technical debt knowingly, but it
might be an option to add this crate to PDM as path dependency for
now and move it to proxmox-rs only when we switch the other projects
(for now mainly PBS, but PVE through perlmod would be an option)
to the common crate? No hard feelings though, uploading the package
to our devel repo is not _that_ much work anyway.
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [pdm-devel] [PATCH proxmox 1/1] upgrade-checks: add upgrade checker crate
2025-09-05 14:08 ` Michael Köppl
@ 2025-09-08 8:39 ` Shannon Sterz
2025-09-08 9:03 ` Thomas Lamprecht
0 siblings, 1 reply; 9+ messages in thread
From: Shannon Sterz @ 2025-09-08 8:39 UTC (permalink / raw)
To: Proxmox Datacenter Manager development discussion, Michael Köppl
On Fri Sep 5, 2025 at 4:08 PM CEST, Michael Köppl wrote:
> This seems to be missing the directory and files for Debian packaging.
> Or is this not supposed to be packaged (at least for now)?
ah sorry yeah missed that in my haste, will add that in a v2.
> On Fri Sep 5, 2025 at 11:59 AM CEST, Shannon Sterz wrote:
>> 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/src/lib.rs | 856 ++++++++++++++++++++++++++++++
>> 3 files changed, 880 insertions(+)
>> create mode 100644 proxmox-upgrade-checks/Cargo.toml
>> create mode 100644 proxmox-upgrade-checks/src/lib.rs
>>
>> diff --git a/Cargo.toml b/Cargo.toml
>> index 8f185b09..c91c1647 100644
>> --- a/Cargo.toml
>> +++ b/Cargo.toml
>> @@ -46,6 +46,7 @@ members = [
>> "proxmox-tfa",
>> "proxmox-time",
>> "proxmox-time-api",
>> + "proxmox-upgrade-checks",
>> "proxmox-uuid",
>> "proxmox-worker-task",
>> "pbs-api-types",
>> @@ -106,6 +107,7 @@ serde_json = "1.0"
>> serde_plain = "1.0"
>> syn = { version = "2", features = [ "full", "visit-mut" ] }
>> tar = "0.4"
>> +termcolor = "1.1.2"
>> tokio = "1.6"
>> tokio-openssl = "0.6.1"
>> tokio-stream = "0.1.0"
>> @@ -122,6 +124,7 @@ zstd = { version = "0.12", features = [ "bindgen" ] }
>> # workspace dependencies
>> proxmox-acme = { version = "0.5.3", path = "proxmox-acme", default-features = false }
>> proxmox-api-macro = { version = "1.3.2", path = "proxmox-api-macro" }
>> +proxmox-apt = { version = "0.11.7", path = "proxmox-apt" }
>> proxmox-apt-api-types = { version = "1.0.2", path = "proxmox-apt-api-types" }
>> proxmox-auth-api = { version = "0.4.0", path = "proxmox-auth-api" }
>> proxmox-async = { version = "0.4.1", path = "proxmox-async" }
>> diff --git a/proxmox-upgrade-checks/Cargo.toml b/proxmox-upgrade-checks/Cargo.toml
>> new file mode 100644
>> index 00000000..acd3e837
>> --- /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 = "0.1.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/src/lib.rs b/proxmox-upgrade-checks/src/lib.rs
>> new file mode 100644
>> index 00000000..9d7566a1
>> --- /dev/null
>> +++ b/proxmox-upgrade-checks/src/lib.rs
>> @@ -0,0 +1,856 @@
>> +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 set_apt_state_file_location<P: AsRef<Path>>(&mut self, path: P) {
>> + self.apt_state_file = Some(path.as_ref().into());
>> + }
>> +
>> + /// Builder-style method to set the location of the APT state file.
>> + pub fn with_apt_state_file_location<P: AsRef<Path>>(mut self, path: P) -> Self {
>> + self.set_apt_state_file_location(path);
>> + self
>> + }
>> +
>> + /// Set the API server package name.
>> + pub fn set_api_server_package(&mut self, api_server_package: &str) {
>> + self.apt_state_file = Some(api_server_package.into());
>> + }
>> +
>> + /// Builder-style method to set the API server package name.
>> + pub fn with_api_server_package(mut self, api_server_package: &str) -> Self {
>> + self.set_api_server_package(api_server_package);
>> + 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);
>> + }
>> +}
>
>
>
> _______________________________________________
> pdm-devel mailing list
> pdm-devel@lists.proxmox.com
> https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [pdm-devel] [PATCH proxmox 1/1] upgrade-checks: add upgrade checker crate
2025-09-05 9:59 ` [pdm-devel] [PATCH proxmox 1/1] upgrade-checks: add upgrade checker crate Shannon Sterz
@ 2025-09-05 14:08 ` Michael Köppl
2025-09-08 8:39 ` Shannon Sterz
0 siblings, 1 reply; 9+ messages in thread
From: Michael Köppl @ 2025-09-05 14:08 UTC (permalink / raw)
To: Proxmox Datacenter Manager development discussion; +Cc: pdm-devel
This seems to be missing the directory and files for Debian packaging.
Or is this not supposed to be packaged (at least for now)?
On Fri Sep 5, 2025 at 11:59 AM CEST, Shannon Sterz wrote:
> 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/src/lib.rs | 856 ++++++++++++++++++++++++++++++
> 3 files changed, 880 insertions(+)
> create mode 100644 proxmox-upgrade-checks/Cargo.toml
> create mode 100644 proxmox-upgrade-checks/src/lib.rs
>
> diff --git a/Cargo.toml b/Cargo.toml
> index 8f185b09..c91c1647 100644
> --- a/Cargo.toml
> +++ b/Cargo.toml
> @@ -46,6 +46,7 @@ members = [
> "proxmox-tfa",
> "proxmox-time",
> "proxmox-time-api",
> + "proxmox-upgrade-checks",
> "proxmox-uuid",
> "proxmox-worker-task",
> "pbs-api-types",
> @@ -106,6 +107,7 @@ serde_json = "1.0"
> serde_plain = "1.0"
> syn = { version = "2", features = [ "full", "visit-mut" ] }
> tar = "0.4"
> +termcolor = "1.1.2"
> tokio = "1.6"
> tokio-openssl = "0.6.1"
> tokio-stream = "0.1.0"
> @@ -122,6 +124,7 @@ zstd = { version = "0.12", features = [ "bindgen" ] }
> # workspace dependencies
> proxmox-acme = { version = "0.5.3", path = "proxmox-acme", default-features = false }
> proxmox-api-macro = { version = "1.3.2", path = "proxmox-api-macro" }
> +proxmox-apt = { version = "0.11.7", path = "proxmox-apt" }
> proxmox-apt-api-types = { version = "1.0.2", path = "proxmox-apt-api-types" }
> proxmox-auth-api = { version = "0.4.0", path = "proxmox-auth-api" }
> proxmox-async = { version = "0.4.1", path = "proxmox-async" }
> diff --git a/proxmox-upgrade-checks/Cargo.toml b/proxmox-upgrade-checks/Cargo.toml
> new file mode 100644
> index 00000000..acd3e837
> --- /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 = "0.1.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/src/lib.rs b/proxmox-upgrade-checks/src/lib.rs
> new file mode 100644
> index 00000000..9d7566a1
> --- /dev/null
> +++ b/proxmox-upgrade-checks/src/lib.rs
> @@ -0,0 +1,856 @@
> +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 set_apt_state_file_location<P: AsRef<Path>>(&mut self, path: P) {
> + self.apt_state_file = Some(path.as_ref().into());
> + }
> +
> + /// Builder-style method to set the location of the APT state file.
> + pub fn with_apt_state_file_location<P: AsRef<Path>>(mut self, path: P) -> Self {
> + self.set_apt_state_file_location(path);
> + self
> + }
> +
> + /// Set the API server package name.
> + pub fn set_api_server_package(&mut self, api_server_package: &str) {
> + self.apt_state_file = Some(api_server_package.into());
> + }
> +
> + /// Builder-style method to set the API server package name.
> + pub fn with_api_server_package(mut self, api_server_package: &str) -> Self {
> + self.set_api_server_package(api_server_package);
> + 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);
> + }
> +}
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 9+ messages in thread
* [pdm-devel] [PATCH proxmox 1/1] upgrade-checks: add upgrade checker crate
2025-09-05 10:04 [pdm-devel] [PATCH datacenter-manager/proxmox 0/3] version command and upgrade checks for pdm Shannon Sterz
@ 2025-09-05 10:04 ` Shannon Sterz
0 siblings, 0 replies; 9+ messages in thread
From: Shannon Sterz @ 2025-09-05 10:04 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/src/lib.rs | 857 ++++++++++++++++++++++++++++++
3 files changed, 881 insertions(+)
create mode 100644 proxmox-upgrade-checks/Cargo.toml
create mode 100644 proxmox-upgrade-checks/src/lib.rs
diff --git a/Cargo.toml b/Cargo.toml
index ce249371..254a2d51 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..acd3e837
--- /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 = "0.1.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/src/lib.rs b/proxmox-upgrade-checks/src/lib.rs
new file mode 100644
index 00000000..6b18526f
--- /dev/null
+++ b/proxmox-upgrade-checks/src/lib.rs
@@ -0,0 +1,857 @@
+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 set_apt_state_file_location<P: AsRef<Path>>(&mut self, path: P) {
+ self.apt_state_file = Some(path.as_ref().into());
+ }
+
+ /// Builder-style method to set the location of the APT state file.
+ pub fn with_apt_state_file_location<P: AsRef<Path>>(mut self, path: P) -> Self {
+ self.set_apt_state_file_location(path);
+ self
+ }
+
+ /// Set the API server package name.
+ pub fn set_api_server_package(&mut self, api_server_package: &str) {
+ self.apt_state_file = Some(api_server_package.into());
+ }
+
+ /// Builder-style method to set the API server package name.
+ pub fn with_api_server_package(mut self, api_server_package: &str) -> Self {
+ self.set_api_server_package(api_server_package);
+ 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.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 9+ messages in thread
* [pdm-devel] [PATCH proxmox 1/1] upgrade-checks: add upgrade checker crate
2025-09-05 9:59 [pdm-devel] [PATCH datacenter-manager/proxmox 0/3] version command and upgrade checks for pdm Shannon Sterz
@ 2025-09-05 9:59 ` Shannon Sterz
2025-09-05 14:08 ` Michael Köppl
0 siblings, 1 reply; 9+ messages in thread
From: Shannon Sterz @ 2025-09-05 9:59 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/src/lib.rs | 856 ++++++++++++++++++++++++++++++
3 files changed, 880 insertions(+)
create mode 100644 proxmox-upgrade-checks/Cargo.toml
create mode 100644 proxmox-upgrade-checks/src/lib.rs
diff --git a/Cargo.toml b/Cargo.toml
index 8f185b09..c91c1647 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -46,6 +46,7 @@ members = [
"proxmox-tfa",
"proxmox-time",
"proxmox-time-api",
+ "proxmox-upgrade-checks",
"proxmox-uuid",
"proxmox-worker-task",
"pbs-api-types",
@@ -106,6 +107,7 @@ serde_json = "1.0"
serde_plain = "1.0"
syn = { version = "2", features = [ "full", "visit-mut" ] }
tar = "0.4"
+termcolor = "1.1.2"
tokio = "1.6"
tokio-openssl = "0.6.1"
tokio-stream = "0.1.0"
@@ -122,6 +124,7 @@ zstd = { version = "0.12", features = [ "bindgen" ] }
# workspace dependencies
proxmox-acme = { version = "0.5.3", path = "proxmox-acme", default-features = false }
proxmox-api-macro = { version = "1.3.2", path = "proxmox-api-macro" }
+proxmox-apt = { version = "0.11.7", path = "proxmox-apt" }
proxmox-apt-api-types = { version = "1.0.2", path = "proxmox-apt-api-types" }
proxmox-auth-api = { version = "0.4.0", path = "proxmox-auth-api" }
proxmox-async = { version = "0.4.1", path = "proxmox-async" }
diff --git a/proxmox-upgrade-checks/Cargo.toml b/proxmox-upgrade-checks/Cargo.toml
new file mode 100644
index 00000000..acd3e837
--- /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 = "0.1.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/src/lib.rs b/proxmox-upgrade-checks/src/lib.rs
new file mode 100644
index 00000000..9d7566a1
--- /dev/null
+++ b/proxmox-upgrade-checks/src/lib.rs
@@ -0,0 +1,856 @@
+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 set_apt_state_file_location<P: AsRef<Path>>(&mut self, path: P) {
+ self.apt_state_file = Some(path.as_ref().into());
+ }
+
+ /// Builder-style method to set the location of the APT state file.
+ pub fn with_apt_state_file_location<P: AsRef<Path>>(mut self, path: P) -> Self {
+ self.set_apt_state_file_location(path);
+ self
+ }
+
+ /// Set the API server package name.
+ pub fn set_api_server_package(&mut self, api_server_package: &str) {
+ self.apt_state_file = Some(api_server_package.into());
+ }
+
+ /// Builder-style method to set the API server package name.
+ pub fn with_api_server_package(mut self, api_server_package: &str) -> Self {
+ self.set_api_server_package(api_server_package);
+ 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.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2025-09-17 12:19 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
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 ` [pdm-devel] [PATCH datacenter-manager 1/1] lib: move proxmox-upgrade-checks into common crate and depend on that Shannon Sterz
-- strict thread matches above, loose matches on Subject: below --
2025-09-05 10:04 [pdm-devel] [PATCH datacenter-manager/proxmox 0/3] version command and upgrade checks for pdm Shannon Sterz
2025-09-05 10:04 ` [pdm-devel] [PATCH proxmox 1/1] upgrade-checks: add upgrade checker crate Shannon Sterz
2025-09-05 9:59 [pdm-devel] [PATCH datacenter-manager/proxmox 0/3] version command and upgrade checks for pdm Shannon Sterz
2025-09-05 9:59 ` [pdm-devel] [PATCH proxmox 1/1] upgrade-checks: add upgrade checker crate Shannon Sterz
2025-09-05 14:08 ` Michael Köppl
2025-09-08 8:39 ` Shannon Sterz
2025-09-08 9:03 ` Thomas Lamprecht
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox