public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pdm-devel] [PATCH datacenter-manager/proxmox 0/3] version command and upgrade checks for pdm
@ 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
                   ` (2 more replies)
  0 siblings, 3 replies; 5+ messages in thread
From: Shannon Sterz @ 2025-09-05 10:04 UTC (permalink / raw)
  To: pdm-devel

these three patches add an upgrade script to proxmox-datacenter-manager.
it also adds a `versions` subcommand to the
`proxmox-datacenter-manager-admin` command. they are intended for the
latest releases of proxmox-datacenter-manager, a series for the
bookworm-based releases was send previously [1].

the first patch moves the existing checks from pbs to the proxmox
repository and make them a little bit more configurable. the second
patch then uses these checks in the new `proxmox-upgrade-checks` crate
and implements the `pdmAtoB` helper that works the same as other upgrade
checking helpers.

finally the last commit implements the `versions` subcommand for
`proxmox-datacenter-manager-admin` so that it is easier to tell what the
currently running version of pdm is.

these commits are inteded to be applied against the current master
branches of both repositories.

[1]: https://lore.proxmox.com/pdm-devel/20250905095906.204396-1-s.sterz@proxmox.com/T/#t

proxmox:

Shannon Sterz (1):
  upgrade-checks: add upgrade checker crate

 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


proxmox-datacenter-manager:

Shannon Sterz (2):
  server: add pdmAtoB upgrade checker script
  cli/admin: add a versions command to show current package versions

 Cargo.toml                                |  3 ++
 Makefile                                  |  1 +
 cli/admin/src/main.rs                     | 53 ++++++++++++++++++++++-
 cli/pdmAtoB/Cargo.toml                    | 15 +++++++
 cli/pdmAtoB/src/main.rs                   | 19 ++++++++
 debian/proxmox-datacenter-manager.install |  3 ++
 6 files changed, 92 insertions(+), 2 deletions(-)
 create mode 100644 cli/pdmAtoB/Cargo.toml
 create mode 100644 cli/pdmAtoB/src/main.rs


Summary over all repositories:
  9 files changed, 973 insertions(+), 2 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] 5+ 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
  2025-09-05 10:04 ` [pdm-devel] [PATCH datacenter-manager 1/2] server: add pdmAtoB upgrade checker script Shannon Sterz
  2025-09-05 10:04 ` [pdm-devel] [PATCH datacenter-manager 2/2] cli/admin: add a versions command to show current package versions Shannon Sterz
  2 siblings, 0 replies; 5+ 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: &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);
+    }
+}
-- 
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] 5+ messages in thread

* [pdm-devel] [PATCH datacenter-manager 1/2] server: add pdmAtoB upgrade checker script
  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 10:04 ` Shannon Sterz
  2025-09-05 10:04 ` [pdm-devel] [PATCH datacenter-manager 2/2] cli/admin: add a versions command to show current package versions Shannon Sterz
  2 siblings, 0 replies; 5+ messages in thread
From: Shannon Sterz @ 2025-09-05 10:04 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
 Cargo.toml                                |  3 +++
 Makefile                                  |  1 +
 cli/pdmAtoB/Cargo.toml                    | 15 +++++++++++++++
 cli/pdmAtoB/src/main.rs                   | 19 +++++++++++++++++++
 debian/proxmox-datacenter-manager.install |  3 +++
 5 files changed, 41 insertions(+)
 create mode 100644 cli/pdmAtoB/Cargo.toml
 create mode 100644 cli/pdmAtoB/src/main.rs

diff --git a/Cargo.toml b/Cargo.toml
index 76c416e..33cb6bc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -25,6 +25,7 @@ members = [
     "cli/client",
     "cli/admin",
     "cli/proxmox-fido2",
+    "cli/pdmAtoB",
 ]
 
 [workspace.dependencies]
@@ -66,6 +67,7 @@ proxmox-uuid = "1"
 # other proxmox crates
 proxmox-acme = "0.5"
 proxmox-openid = "0.10"
+proxmox-upgrade-checks = "0.1.0"
 
 # api implementation creates
 proxmox-config-digest = "1"
@@ -179,4 +181,5 @@ zstd = { version = "0.13" }
 # proxmox-time-api = { path = "../proxmox/proxmox-time-api" }
 # proxmox-time = { path = "../proxmox/proxmox-time" }
 # proxmox-uuid = { path = "../proxmox/proxmox-uuid" }
+proxmox-upgrade-checks = { path = "../proxmox/proxmox-upgrade-checks" }
 # proxmox-worker-task = { path = "../proxmox/proxmox-worker-task" }
diff --git a/Makefile b/Makefile
index d18fb0d..fb2e9bd 100644
--- a/Makefile
+++ b/Makefile
@@ -42,6 +42,7 @@ USR_BIN := \
 
 USR_SBIN := \
 	proxmox-datacenter-manager-admin \
+	pdmAtoB \
 
 SERVICE_BIN := \
 	proxmox-datacenter-api \
diff --git a/cli/pdmAtoB/Cargo.toml b/cli/pdmAtoB/Cargo.toml
new file mode 100644
index 0000000..f05d839
--- /dev/null
+++ b/cli/pdmAtoB/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "pdmAtoB"
+description = "Proxmox Datacenter Upgrade Checks"
+homepage = "https://www.proxmox.com"
+version.workspace = true
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+exclude.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+proxmox-upgrade-checks.workspace = true
+pdm-buildcfg.workspace = true
diff --git a/cli/pdmAtoB/src/main.rs b/cli/pdmAtoB/src/main.rs
new file mode 100644
index 0000000..fdb6684
--- /dev/null
+++ b/cli/pdmAtoB/src/main.rs
@@ -0,0 +1,19 @@
+use pdm_buildcfg::{PROXMOX_PKG_RELEASE, PROXMOX_PKG_VERSION};
+use proxmox_upgrade_checks::UpgradeCheckerBuilder;
+
+fn main() -> Result<(), anyhow::Error> {
+    UpgradeCheckerBuilder::new(
+        "bookworm",
+        "trixie",
+        "proxmox-datacenter-manager",
+        0,
+        1,
+        11,
+        &format!("{PROXMOX_PKG_VERSION}.{PROXMOX_PKG_RELEASE}"),
+    )
+    .with_apt_state_file_location(pdm_buildcfg::APT_PKG_STATE_FN)
+    .add_service_to_checks("proxmox-datacenter-api")
+    .add_service_to_checks("proxmox-datacenter-privileged-api")
+    .build()
+    .run()
+}
diff --git a/debian/proxmox-datacenter-manager.install b/debian/proxmox-datacenter-manager.install
index 8df450e..64aa789 100644
--- a/debian/proxmox-datacenter-manager.install
+++ b/debian/proxmox-datacenter-manager.install
@@ -10,9 +10,12 @@ usr/libexec/proxmox/proxmox-datacenter-manager-banner
 usr/libexec/proxmox/proxmox-datacenter-manager-daily-update
 usr/libexec/proxmox/proxmox-datacenter-privileged-api
 usr/sbin/proxmox-datacenter-manager-admin
+usr/sbin/pdmAtoB
 usr/share/bash-completion/completions/proxmox-datacenter-api.bc
 usr/share/bash-completion/completions/proxmox-datacenter-manager-admin.bc
 usr/share/bash-completion/completions/proxmox-datacenter-privileged-api.bc
+usr/share/bash-completion/completions/pdmAtoB.bc
 usr/share/zsh/vendor-completions/_proxmox-datacenter-api
 usr/share/zsh/vendor-completions/_proxmox-datacenter-manager-admin
 usr/share/zsh/vendor-completions/_proxmox-datacenter-privileged-api
+usr/share/zsh/vendor-completions/_pdmAtoB
-- 
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] 5+ messages in thread

* [pdm-devel] [PATCH datacenter-manager 2/2] cli/admin: add a versions command to show current package versions
  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 10:04 ` [pdm-devel] [PATCH datacenter-manager 1/2] server: add pdmAtoB upgrade checker script Shannon Sterz
@ 2025-09-05 10:04 ` Shannon Sterz
  2 siblings, 0 replies; 5+ messages in thread
From: Shannon Sterz @ 2025-09-05 10:04 UTC (permalink / raw)
  To: pdm-devel

similar to other proxmox products this shows the currently running
version as well as offering a verbose option that can show versions
for other relevant packages as well.

Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
 cli/admin/src/main.rs | 53 +++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 51 insertions(+), 2 deletions(-)

diff --git a/cli/admin/src/main.rs b/cli/admin/src/main.rs
index 7170471..831b573 100644
--- a/cli/admin/src/main.rs
+++ b/cli/admin/src/main.rs
@@ -1,4 +1,11 @@
-use proxmox_router::cli::{run_cli_command, CliCommandMap, CliEnvironment};
+use serde_json::{json, Value};
+
+use proxmox_router::cli::{
+    default_table_format_options, format_and_print_result_full, get_output_format, run_cli_command,
+    CliCommand, CliCommandMap, CliEnvironment, ColumnConfig, OUTPUT_FORMAT,
+};
+
+use proxmox_schema::api;
 
 mod remotes;
 
@@ -16,7 +23,9 @@ fn main() {
 
     server::context::init().expect("could not set up server context");
 
-    let cmd_def = CliCommandMap::new().insert("remote", remotes::cli());
+    let cmd_def = CliCommandMap::new()
+        .insert("remote", remotes::cli())
+        .insert("versions", CliCommand::new(&API_METHOD_GET_VERSIONS));
 
     let rpcenv = CliEnvironment::new();
     run_cli_command(
@@ -25,3 +34,43 @@ fn main() {
         Some(|future| proxmox_async::runtime::main(future)),
     );
 }
+
+#[api(
+    input: {
+        properties: {
+            verbose: {
+                type: Boolean,
+                optional: true,
+                default: false,
+                description: "Output verbose package information. It is ignored if output-format is specified.",
+            },
+            "output-format": {
+                schema: OUTPUT_FORMAT,
+                optional: true,
+            }
+        }
+    }
+)]
+/// List package versions for important Proxmox Datacenter Manager packages.
+async fn get_versions(verbose: bool, param: Value) -> Result<Value, anyhow::Error> {
+    let output_format = get_output_format(&param);
+
+    let packages = server::api::nodes::apt::get_versions()?;
+    let mut packages = json!(if verbose {
+        &packages[..]
+    } else {
+        &packages[1..2]
+    });
+
+    let options = default_table_format_options()
+        .disable_sort()
+        .noborder(true) // just not helpful for version info which gets copy pasted often
+        .column(ColumnConfig::new("Package"))
+        .column(ColumnConfig::new("Version"))
+        .column(ColumnConfig::new("ExtraInfo").header("Extra Info"));
+    let return_type = &server::api::nodes::apt::API_METHOD_GET_VERSIONS.returns;
+
+    format_and_print_result_full(&mut packages, return_type, &output_format, &options);
+
+    Ok(Value::Null)
+}
-- 
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] 5+ messages in thread

* [pdm-devel] [PATCH datacenter-manager 2/2] cli/admin: add a versions command to show current package versions
  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
  0 siblings, 0 replies; 5+ messages in thread
From: Shannon Sterz @ 2025-09-05  9:59 UTC (permalink / raw)
  To: pdm-devel

similar to other proxmox products this shows the currently running
version as well as offering a verbose option that can show versions
for other relevant packages as well.

Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
 cli/admin/src/main.rs | 53 +++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 51 insertions(+), 2 deletions(-)

diff --git a/cli/admin/src/main.rs b/cli/admin/src/main.rs
index 91b93f3..aa0897a 100644
--- a/cli/admin/src/main.rs
+++ b/cli/admin/src/main.rs
@@ -1,4 +1,11 @@
-use proxmox_router::cli::{run_cli_command, CliCommandMap, CliEnvironment};
+use serde_json::{json, Value};
+
+use proxmox_router::cli::{
+    default_table_format_options, format_and_print_result_full, get_output_format, run_cli_command,
+    CliCommand, CliCommandMap, CliEnvironment, ColumnConfig, OUTPUT_FORMAT,
+};
+
+use proxmox_schema::api;
 
 mod remotes;
 
@@ -12,7 +19,9 @@ fn main() {
 
     server::context::init().expect("could not set up server context");
 
-    let cmd_def = CliCommandMap::new().insert("remote", remotes::cli());
+    let cmd_def = CliCommandMap::new()
+        .insert("remote", remotes::cli())
+        .insert("versions", CliCommand::new(&API_METHOD_GET_VERSIONS));
 
     let rpcenv = CliEnvironment::new();
     run_cli_command(
@@ -21,3 +30,43 @@ fn main() {
         Some(|future| proxmox_async::runtime::main(future)),
     );
 }
+
+#[api(
+    input: {
+        properties: {
+            verbose: {
+                type: Boolean,
+                optional: true,
+                default: false,
+                description: "Output verbose package information. It is ignored if output-format is specified.",
+            },
+            "output-format": {
+                schema: OUTPUT_FORMAT,
+                optional: true,
+            }
+        }
+    }
+)]
+/// List package versions for important Proxmox Datacenter Manager packages.
+async fn get_versions(verbose: bool, param: Value) -> Result<Value, anyhow::Error> {
+    let output_format = get_output_format(&param);
+
+    let packages = server::api::nodes::apt::get_versions()?;
+    let mut packages = json!(if verbose {
+        &packages[..]
+    } else {
+        &packages[1..2]
+    });
+
+    let options = default_table_format_options()
+        .disable_sort()
+        .noborder(true) // just not helpful for version info which gets copy pasted often
+        .column(ColumnConfig::new("Package"))
+        .column(ColumnConfig::new("Version"))
+        .column(ColumnConfig::new("ExtraInfo").header("Extra Info"));
+    let return_type = &server::api::nodes::apt::API_METHOD_GET_VERSIONS.returns;
+
+    format_and_print_result_full(&mut packages, return_type, &output_format, &options);
+
+    Ok(Value::Null)
+}
-- 
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] 5+ messages in thread

end of thread, other threads:[~2025-09-05 10:05 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
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 10:04 ` [pdm-devel] [PATCH datacenter-manager 1/2] server: add pdmAtoB upgrade checker script Shannon Sterz
2025-09-05 10:04 ` [pdm-devel] [PATCH datacenter-manager 2/2] cli/admin: add a versions command to show current package versions Shannon Sterz
  -- strict thread matches above, loose matches on Subject: below --
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 datacenter-manager 2/2] cli/admin: add a versions command to show current package versions Shannon Sterz

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