public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: "Shannon Sterz" <s.sterz@proxmox.com>
To: "Proxmox Datacenter Manager development discussion"
	<pdm-devel@lists.proxmox.com>,
	"Michael Köppl" <m.koeppl@proxmox.com>
Subject: Re: [pdm-devel] [PATCH proxmox 1/1] upgrade-checks: add upgrade checker crate
Date: Mon, 08 Sep 2025 10:39:23 +0200	[thread overview]
Message-ID: <DCNA7XTC2LNG.3J65OKXKWC8UX@proxmox.com> (raw)
In-Reply-To: <DCKXBYHQ8Q54.33LZXXHUOWGOO@proxmox.com>

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: &regex::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

  reply	other threads:[~2025-09-08  8:39 UTC|newest]

Thread overview: 10+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
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 [this message]
2025-09-08  9:03       ` Thomas Lamprecht
2025-09-05  9:59 ` [pdm-devel] [PATCH datacenter-manager 1/2] server: add pdmAtoB upgrade checker script Shannon Sterz
2025-09-05  9:59 ` [pdm-devel] [PATCH datacenter-manager 2/2] cli/admin: add a versions command to show current package versions Shannon Sterz
2025-09-05 15:18 ` [pdm-devel] [PATCH datacenter-manager/proxmox 0/3] version command and upgrade checks for pdm Michael Köppl
2025-09-08  8:39   ` Shannon Sterz
2025-09-05 10:04 Shannon Sterz
2025-09-05 10:04 ` [pdm-devel] [PATCH proxmox 1/1] upgrade-checks: add upgrade checker crate Shannon Sterz

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=DCNA7XTC2LNG.3J65OKXKWC8UX@proxmox.com \
    --to=s.sterz@proxmox.com \
    --cc=m.koeppl@proxmox.com \
    --cc=pdm-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal