* [pdm-devel] [PATCH datacenter-manager/proxmox 0/3] version command and upgrade checks for pdm @ 2025-09-05 9:59 Shannon Sterz 2025-09-05 9:59 ` [pdm-devel] [PATCH proxmox 1/1] upgrade-checks: add upgrade checker crate Shannon Sterz ` (3 more replies) 0 siblings, 4 replies; 7+ messages in thread From: Shannon Sterz @ 2025-09-05 9:59 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 bookworm based releases, so that users can check the prerequisites before upgrading to a trixie based release. a similar series for the current trixie-based master branches will follow shortly. 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 paches are intended to be applied against the following trees: * in the proxmox repository this should be applied against the `stable-bookworm` branch. * in the proxmox-datacenter-manager repository the two commits should be applied against the commit f6ca650d (ui: add woff2 roboto flex variant to ui packaging) the latter was chosen as it seems to be the last commit in the proxmox-datacenter-manager repo that was intended for bookworm, since that repo is missing a `stable-bookworm` branch so far. 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 | 856 ++++++++++++++++++++++++++++++ 3 files changed, 880 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, 972 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] 7+ messages in thread
* [pdm-devel] [PATCH proxmox 1/1] upgrade-checks: add upgrade checker crate 2025-09-05 9:59 [pdm-devel] [PATCH datacenter-manager/proxmox 0/3] version command and upgrade checks for pdm Shannon Sterz @ 2025-09-05 9:59 ` Shannon Sterz 2025-09-05 14:08 ` Michael Köppl 2025-09-05 9:59 ` [pdm-devel] [PATCH datacenter-manager 1/2] server: add pdmAtoB upgrade checker script Shannon Sterz ` (2 subsequent siblings) 3 siblings, 1 reply; 7+ messages in thread From: Shannon Sterz @ 2025-09-05 9:59 UTC (permalink / raw) To: pdm-devel that allows easily creating upgrade checks for proxmox products Signed-off-by: Shannon Sterz <s.sterz@proxmox.com> --- Cargo.toml | 3 + proxmox-upgrade-checks/Cargo.toml | 21 + proxmox-upgrade-checks/src/lib.rs | 856 ++++++++++++++++++++++++++++++ 3 files changed, 880 insertions(+) create mode 100644 proxmox-upgrade-checks/Cargo.toml create mode 100644 proxmox-upgrade-checks/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 8f185b09..c91c1647 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ members = [ "proxmox-tfa", "proxmox-time", "proxmox-time-api", + "proxmox-upgrade-checks", "proxmox-uuid", "proxmox-worker-task", "pbs-api-types", @@ -106,6 +107,7 @@ serde_json = "1.0" serde_plain = "1.0" syn = { version = "2", features = [ "full", "visit-mut" ] } tar = "0.4" +termcolor = "1.1.2" tokio = "1.6" tokio-openssl = "0.6.1" tokio-stream = "0.1.0" @@ -122,6 +124,7 @@ zstd = { version = "0.12", features = [ "bindgen" ] } # workspace dependencies proxmox-acme = { version = "0.5.3", path = "proxmox-acme", default-features = false } proxmox-api-macro = { version = "1.3.2", path = "proxmox-api-macro" } +proxmox-apt = { version = "0.11.7", path = "proxmox-apt" } proxmox-apt-api-types = { version = "1.0.2", path = "proxmox-apt-api-types" } proxmox-auth-api = { version = "0.4.0", path = "proxmox-auth-api" } proxmox-async = { version = "0.4.1", path = "proxmox-async" } diff --git a/proxmox-upgrade-checks/Cargo.toml b/proxmox-upgrade-checks/Cargo.toml new file mode 100644 index 00000000..acd3e837 --- /dev/null +++ b/proxmox-upgrade-checks/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "proxmox-upgrade-checks" +description = "Helpers to implement upgrade checks for Proxmox products." +version = "0.1.0" + +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +exclude.workspace = true +rust-version.workspace = true + +[dependencies] +anyhow.workspace = true +const_format.workspace = true +regex.workspace = true +termcolor.workspace = true + +proxmox-apt = { workspace = true, features = [ "cache" ] } +proxmox-apt-api-types.workspace = true diff --git a/proxmox-upgrade-checks/src/lib.rs b/proxmox-upgrade-checks/src/lib.rs new file mode 100644 index 00000000..9d7566a1 --- /dev/null +++ b/proxmox-upgrade-checks/src/lib.rs @@ -0,0 +1,856 @@ +use std::path::Path; +use std::{io::Write, path::PathBuf}; + +use anyhow::{format_err, Error}; +use const_format::concatcp; +use regex::Regex; +use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; + +use proxmox_apt::repositories; +use proxmox_apt_api_types::{APTRepositoryFile, APTRepositoryPackageType}; + +/// Easily create and configure an upgrade checker for Proxmox products. +pub struct UpgradeCheckerBuilder { + old_suite: String, + new_suite: String, + meta_package_name: String, + minimum_major_version: u8, + minimum_minor_version: u8, + minimum_pkgrel: u8, + apt_state_file: Option<PathBuf>, + api_server_package: Option<String>, + running_api_server_version: String, + services_list: Vec<String>, +} + +impl UpgradeCheckerBuilder { + /// Create a new UpgradeCheckerBuilder + /// + /// * `old_suite`: The Debian suite before the upgrade. + /// * `new_suite`: The Debian suite after the upgrade. + /// * `meta_package_name`: The name of the product's meta package. + /// * `minimum_major_version`: The minimum major version before the upgrade. + /// * `minimum_minor_version`: The minimum minor version before the upgrade. + /// * `minimum_pkgrel`: The minimum package release before the upgrade. + /// * `running_api_server_version`: The currently running API server version. + pub fn new( + old_suite: &str, + new_suite: &str, + meta_package_name: &str, + minimum_major_version: u8, + minimum_minor_version: u8, + minimum_pkgrel: u8, + running_api_server_version: &str, + ) -> UpgradeCheckerBuilder { + UpgradeCheckerBuilder { + old_suite: old_suite.into(), + new_suite: new_suite.into(), + meta_package_name: meta_package_name.into(), + minimum_major_version, + minimum_minor_version, + minimum_pkgrel, + apt_state_file: None, + api_server_package: None, + running_api_server_version: running_api_server_version.into(), + services_list: Vec::new(), + } + } + + /// Set the location of the APT state file. + pub fn set_apt_state_file_location<P: AsRef<Path>>(&mut self, path: P) { + self.apt_state_file = Some(path.as_ref().into()); + } + + /// Builder-style method to set the location of the APT state file. + pub fn with_apt_state_file_location<P: AsRef<Path>>(mut self, path: P) -> Self { + self.set_apt_state_file_location(path); + self + } + + /// Set the API server package name. + pub fn set_api_server_package(&mut self, api_server_package: &str) { + self.apt_state_file = Some(api_server_package.into()); + } + + /// Builder-style method to set the API server package name. + pub fn with_api_server_package(mut self, api_server_package: &str) -> Self { + self.set_api_server_package(api_server_package); + self + } + + /// Add a service to the list of services that will be checked. + pub fn add_service_to_checks(mut self, service_name: &str) -> Self { + self.services_list.push(service_name.into()); + self + } + + /// Construct the UpgradeChecker, consumes the UpgradeCheckerBuilder + pub fn build(mut self) -> UpgradeChecker { + UpgradeChecker { + output: ConsoleOutput::new(), + upgraded: false, + old_suite: self.old_suite, + new_suite: self.new_suite, + minimum_major_version: self.minimum_major_version, + minimum_minor_version: self.minimum_minor_version, + minimum_pkgrel: self.minimum_pkgrel, + apt_state_file: self.apt_state_file.take().unwrap_or_else(|| { + PathBuf::from(format!( + "/var/lib/{}/pkg-state.json", + &self.meta_package_name + )) + }), + api_server_package: self + .api_server_package + .unwrap_or_else(|| self.meta_package_name.clone()), + running_api_server_version: self.running_api_server_version, + meta_package_name: self.meta_package_name, + services_list: self.services_list, + } + } +} + +/// Helpers to easily construct a set of upgrade checks. +pub struct UpgradeChecker { + output: ConsoleOutput, + upgraded: bool, + old_suite: String, + new_suite: String, + meta_package_name: String, + minimum_major_version: u8, + minimum_minor_version: u8, + minimum_pkgrel: u8, + apt_state_file: PathBuf, + api_server_package: String, + running_api_server_version: String, + services_list: Vec<String>, +} + +impl UpgradeChecker { + /// Run all checks. + pub fn run(&mut self) -> Result<(), Error> { + self.check_packages()?; + self.check_misc()?; + self.summary() + } + + /// Run miscellaneous checks. + pub fn check_misc(&mut self) -> Result<(), Error> { + self.output.print_header("MISCELLANEOUS CHECKS")?; + self.check_services()?; + self.check_time_sync()?; + self.check_apt_repos()?; + self.check_bootloader()?; + self.check_dkms_modules()?; + Ok(()) + } + + /// Print a summary of all checks run so far. + pub fn summary(&mut self) -> Result<(), Error> { + self.output.print_summary() + } + + /// Run all package related checks. + pub fn check_packages(&mut self) -> Result<(), Error> { + self.output.print_header(&format!( + "CHECKING VERSION INFORMATION FOR {} PACKAGES", + self.meta_package_name.to_uppercase() + ))?; + + self.check_upgradable_packages()?; + + let pkg_versions = proxmox_apt::get_package_versions( + &self.meta_package_name, + &self.api_server_package, + &self.running_api_server_version, + &[], + )?; + + self.check_meta_package_version(&pkg_versions)?; + self.check_kernel_compat(&pkg_versions)?; + Ok(()) + } + + fn check_upgradable_packages(&mut self) -> Result<(), Error> { + self.output.log_info("Checking for package updates..")?; + + let result = proxmox_apt::list_available_apt_update(&self.apt_state_file); + match result { + Err(err) => { + self.output.log_warn(format!("{err}"))?; + self.output + .log_fail("unable to retrieve list of package updates!")?; + } + Ok(package_status) => { + if package_status.is_empty() { + self.output.log_pass("all packages up-to-date")?; + } else { + let pkgs = package_status + .iter() + .map(|pkg| pkg.package.clone()) + .collect::<Vec<String>>() + .join(", "); + self.output.log_warn(format!( + "updates for the following packages are available:\n {pkgs}", + ))?; + } + } + } + Ok(()) + } + + fn check_meta_package_version( + &mut self, + pkg_versions: &[proxmox_apt_api_types::APTUpdateInfo], + ) -> Result<(), Error> { + self.output.log_info(format!( + "Checking {} package version..", + self.meta_package_name + ))?; + + let meta_pkg = pkg_versions + .iter() + .find(|pkg| pkg.package.as_str() == self.meta_package_name); + + if let Some(meta_pkg) = meta_pkg { + let pkg_version = Regex::new(r"^(\d+)\.(\d+)[.-](\d+)")?; + let captures = pkg_version.captures(&meta_pkg.old_version); + if let Some(captures) = captures { + let maj = Self::extract_version_from_captures(1, &captures)?; + let min = Self::extract_version_from_captures(2, &captures)?; + let pkgrel = Self::extract_version_from_captures(3, &captures)?; + + let min_version = format!( + "{}.{}.{}", + self.minimum_major_version, self.minimum_minor_version, self.minimum_pkgrel + ); + + if (maj > self.minimum_major_version && self.minimum_major_version != 0) + // Handle alpha and beta version upgrade checks: + || (self.minimum_major_version == 0 && min > self.minimum_minor_version) + { + self.output.log_pass(format!("Already upgraded to {maj}.{min}"))?; + self.upgraded = true; + } else if maj >= self.minimum_major_version + && min >= self.minimum_minor_version + && pkgrel >= self.minimum_pkgrel + { + self.output.log_pass(format!( + "'{}' has version >= {min_version}", + self.meta_package_name + ))?; + } else { + self.output.log_fail(format!( + "'{}' package is too old, please upgrade to >= {min_version}", + self.meta_package_name + ))?; + } + } else { + self.output.log_fail(format!( + "could not match the '{}' package version, \ + is it installed?", + self.meta_package_name + ))?; + } + } else { + self.output + .log_fail(format!("'{}' package not found!", self.meta_package_name))?; + } + Ok(()) + } + + fn is_kernel_version_compatible(&self, running_version: &str) -> bool { + // TODO: rework this to parse out maj.min.patch and do numerical comparison and detect + // those with a "bpo12" as backport kernels from the older release. + const MINIMUM_RE: &str = r"6\.(?:14\.(?:[1-9]\d+|[6-9])|1[5-9])[^~]*"; + const ARBITRARY_RE: &str = r"(?:1[4-9]|2\d+)\.(?:[0-9]|\d{2,})[^~]*-pve"; + + let re = if self.upgraded { + concatcp!(r"^(?:", MINIMUM_RE, r"|", ARBITRARY_RE, r")$") + } else { + r"^(?:6\.(?:2|5|8|11|14))" + }; + let re = Regex::new(re).expect("failed to compile kernel compat regex"); + + re.is_match(running_version) + } + + fn check_kernel_compat( + &mut self, + pkg_versions: &[proxmox_apt_api_types::APTUpdateInfo], + ) -> Result<(), Error> { + self.output.log_info("Check running kernel version..")?; + + let kinstalled = if self.upgraded { + "proxmox-kernel-6.14" + } else { + "proxmox-kernel-6.8" + }; + + let output = std::process::Command::new("uname").arg("-r").output(); + match output { + Err(_err) => self + .output + .log_fail("unable to determine running kernel version.")?, + Ok(ret) => { + let running_version = std::str::from_utf8(&ret.stdout[..ret.stdout.len() - 1])?; + if self.is_kernel_version_compatible(running_version) { + if self.upgraded { + self.output.log_pass(format!( + "running new kernel '{running_version}' after upgrade." + ))?; + } else { + self.output.log_pass(format!( + "running kernel '{running_version}' is considered suitable for \ + upgrade." + ))?; + } + } else { + let installed_kernel = pkg_versions + .iter() + .find(|pkg| pkg.package.as_str() == kinstalled); + if installed_kernel.is_some() { + self.output.log_warn(format!( + "a suitable kernel '{kinstalled}' is installed, but an \ + unsuitable '{running_version}' is booted, missing reboot?!", + ))?; + } else { + self.output.log_warn(format!( + "unexpected running and installed kernel '{running_version}'.", + ))?; + } + } + } + } + Ok(()) + } + + fn extract_version_from_captures( + index: usize, + captures: ®ex::Captures, + ) -> Result<u8, Error> { + if let Some(capture) = captures.get(index) { + let val = capture.as_str().parse::<u8>()?; + Ok(val) + } else { + Ok(0) + } + } + + fn check_bootloader(&mut self) -> Result<(), Error> { + self.output + .log_info("Checking bootloader configuration...")?; + + let sd_boot_installed = + Path::new("/usr/share/doc/systemd-boot/changelog.Debian.gz").is_file(); + + if !Path::new("/sys/firmware/efi").is_dir() { + if sd_boot_installed { + self.output.log_warn( + "systemd-boot package installed on legacy-boot system is not \ + necessary, consider removing it", + )?; + return Ok(()); + } + self.output + .log_skip("System booted in legacy-mode - no need for additional packages.")?; + return Ok(()); + } + + let mut boot_ok = true; + if Path::new("/etc/kernel/proxmox-boot-uuids").is_file() { + // Package version check needs to be run before + if !self.upgraded { + let output = std::process::Command::new("proxmox-boot-tool") + .arg("status") + .output() + .map_err(|err| { + format_err!("failed to retrieve proxmox-boot-tool status - {err}") + })?; + let re = Regex::new(r"configured with:.* (uefi|systemd-boot) \(versions:") + .expect("failed to proxmox-boot-tool status"); + if re.is_match(std::str::from_utf8(&output.stdout)?) { + self.output + .log_skip("not yet upgraded, systemd-boot still needed for bootctl")?; + return Ok(()); + } + } + } else { + if !Path::new("/usr/share/doc/grub-efi-amd64/changelog.Debian.gz").is_file() { + self.output.log_warn( + "System booted in uefi mode but grub-efi-amd64 meta-package not installed, \ + new grub versions will not be installed to /boot/efi! + Install grub-efi-amd64.", + )?; + boot_ok = false; + } + if Path::new("/boot/efi/EFI/BOOT/BOOTX64.efi").is_file() { + let output = std::process::Command::new("debconf-show") + .arg("--db") + .arg("configdb") + .arg("grub-efi-amd64") + .arg("grub-pc") + .output() + .map_err(|err| format_err!("failed to retrieve debconf settings - {err}"))?; + let re = Regex::new(r"grub2/force_efi_extra_removable: +true(?:\n|$)") + .expect("failed to compile dbconfig regex"); + if !re.is_match(std::str::from_utf8(&output.stdout)?) { + self.output.log_warn( + "Removable bootloader found at '/boot/efi/EFI/BOOT/BOOTX64.efi', but GRUB packages \ + not set up to update it!\nRun the following command:\n\ + echo 'grub-efi-amd64 grub2/force_efi_extra_removable boolean true' | debconf-set-selections -v -u\n\ + Then reinstall GRUB with 'apt install --reinstall grub-efi-amd64'" + )?; + boot_ok = false; + } + } + } + if sd_boot_installed { + self.output.log_fail( + "systemd-boot meta-package installed. This will cause problems on upgrades of other \ + boot-related packages.\n\ + Remove the 'systemd-boot' package.\n\ + Please consult the upgrade guide for further information!" + )?; + boot_ok = false; + } + if boot_ok { + self.output + .log_pass("bootloader packages installed correctly")?; + } + Ok(()) + } + + fn check_apt_repos(&mut self) -> Result<(), Error> { + self.output + .log_info("Checking for package repository suite mismatches..")?; + + let mut strange_suite = false; + let mut mismatches = Vec::new(); + let mut found_suite: Option<(String, String)> = None; + + let (repo_files, _repo_errors, _digest) = repositories::repositories()?; + for repo_file in repo_files { + self.check_repo_file( + &mut found_suite, + &mut mismatches, + &mut strange_suite, + repo_file, + )?; + } + + match (mismatches.is_empty(), strange_suite) { + (true, false) => self.output.log_pass("found no suite mismatch")?, + (true, true) => self + .output + .log_notice("found no suite mismatches, but found at least one strange suite")?, + (false, _) => { + let mut message = String::from( + "Found mixed old and new packages repository suites, fix before upgrading!\ + \n Mismatches:", + ); + for (suite, location) in mismatches.iter() { + message.push_str( + format!("\n found suite '{suite}' at '{location}'").as_str(), + ); + } + message.push('\n'); + self.output.log_fail(message)? + } + } + + Ok(()) + } + + fn check_dkms_modules(&mut self) -> Result<(), Error> { + let kver = std::process::Command::new("uname") + .arg("-r") + .output() + .map_err(|err| format_err!("failed to retrieve running kernel version - {err}"))?; + + let output = std::process::Command::new("dkms") + .arg("status") + .arg("-k") + .arg(std::str::from_utf8(&kver.stdout)?) + .output(); + match output { + Err(_err) => self.output.log_skip("could not get dkms status")?, + Ok(ret) => { + let num_dkms_modules = std::str::from_utf8(&ret.stdout)?.lines().count(); + if num_dkms_modules == 0 { + self.output.log_pass("no dkms modules found")?; + } else { + self.output + .log_warn("dkms modules found, this might cause issues during upgrade.")?; + } + } + } + Ok(()) + } + + fn check_repo_file( + &mut self, + found_suite: &mut Option<(String, String)>, + mismatches: &mut Vec<(String, String)>, + strange_suite: &mut bool, + repo_file: APTRepositoryFile, + ) -> Result<(), Error> { + for repo in repo_file.repositories { + if !repo.enabled || repo.types == [APTRepositoryPackageType::DebSrc] { + continue; + } + for suite in &repo.suites { + let suite = match suite.find(&['-', '/'][..]) { + Some(n) => &suite[0..n], + None => suite, + }; + + if suite != self.old_suite && suite != self.new_suite { + let location = repo_file.path.clone().unwrap_or_default(); + self.output.log_notice(format!( + "found unusual suite '{suite}', neither old '{}' nor new \ + '{}'..\n Affected file {location}\n Please \ + assure this is shipping compatible packages for the upgrade!", + self.old_suite, self.new_suite + ))?; + *strange_suite = true; + continue; + } + + if let Some((ref current_suite, ref current_location)) = found_suite { + let location = repo_file.path.clone().unwrap_or_default(); + if suite != current_suite { + if mismatches.is_empty() { + mismatches.push((current_suite.clone(), current_location.clone())); + mismatches.push((suite.to_string(), location)); + } else { + mismatches.push((suite.to_string(), location)); + } + } + } else { + let location = repo_file.path.clone().unwrap_or_default(); + *found_suite = Some((suite.to_string(), location)); + } + } + } + Ok(()) + } + + fn get_systemd_unit_state( + &self, + unit: &str, + ) -> Result<(SystemdUnitState, SystemdUnitState), Error> { + let output = std::process::Command::new("systemctl") + .arg("is-enabled") + .arg(unit) + .output() + .map_err(|err| format_err!("failed to execute - {err}"))?; + + let enabled_state = match output.stdout.as_slice() { + b"enabled\n" => SystemdUnitState::Enabled, + b"disabled\n" => SystemdUnitState::Disabled, + _ => SystemdUnitState::Unknown, + }; + + let output = std::process::Command::new("systemctl") + .arg("is-active") + .arg(unit) + .output() + .map_err(|err| format_err!("failed to execute - {err}"))?; + + let active_state = match output.stdout.as_slice() { + b"active\n" => SystemdUnitState::Active, + b"inactive\n" => SystemdUnitState::Inactive, + b"failed\n" => SystemdUnitState::Failed, + _ => SystemdUnitState::Unknown, + }; + Ok((enabled_state, active_state)) + } + + fn check_services(&mut self) -> Result<(), Error> { + self.output.log_info(format!( + "Checking {} daemon services..", + self.meta_package_name + ))?; + + for service in self.services_list.as_slice() { + match self.get_systemd_unit_state(service)? { + (_, SystemdUnitState::Active) => { + self.output + .log_pass(format!("systemd unit '{service}' is in state 'active'"))?; + } + (_, SystemdUnitState::Inactive) => { + self.output.log_fail(format!( + "systemd unit '{service}' is in state 'inactive'\ + \n Please check the service for errors and start it.", + ))?; + } + (_, SystemdUnitState::Failed) => { + self.output.log_fail(format!( + "systemd unit '{service}' is in state 'failed'\ + \n Please check the service for errors and start it.", + ))?; + } + (_, _) => { + self.output.log_fail(format!( + "systemd unit '{service}' is not in state 'active'\ + \n Please check the service for errors and start it.", + ))?; + } + } + } + Ok(()) + } + + fn check_time_sync(&mut self) -> Result<(), Error> { + self.output + .log_info("Checking for supported & active NTP service..")?; + if self.get_systemd_unit_state("systemd-timesyncd.service")?.1 == SystemdUnitState::Active { + self.output.log_warn( + "systemd-timesyncd is not the best choice for time-keeping on servers, due to only \ + applying updates on boot.\ + \n While not necessary for the upgrade it's recommended to use one of:\ + \n * chrony (Default in new Proxmox product installations)\ + \n * ntpsec\ + \n * openntpd" + )?; + } else if self.get_systemd_unit_state("ntp.service")?.1 == SystemdUnitState::Active { + self.output.log_info( + "Debian deprecated and removed the ntp package for Bookworm, but the system \ + will automatically migrate to the 'ntpsec' replacement package on upgrade.", + )?; + } else if self.get_systemd_unit_state("chrony.service")?.1 == SystemdUnitState::Active + || self.get_systemd_unit_state("openntpd.service")?.1 == SystemdUnitState::Active + || self.get_systemd_unit_state("ntpsec.service")?.1 == SystemdUnitState::Active + { + self.output + .log_pass("Detected active time synchronisation unit")?; + } else { + self.output.log_warn( + "No (active) time synchronisation daemon (NTP) detected, but synchronized systems \ + are important!", + )?; + } + Ok(()) + } +} + +#[derive(PartialEq)] +enum SystemdUnitState { + Active, + Enabled, + Disabled, + Failed, + Inactive, + Unknown, +} + +#[derive(Default)] +struct Counters { + pass: u64, + skip: u64, + notice: u64, + warn: u64, + fail: u64, +} + +enum LogLevel { + Pass, + Info, + Skip, + Notice, + Warn, + Fail, +} + +struct ConsoleOutput { + stream: StandardStream, + first_header: bool, + counters: Counters, +} + +impl ConsoleOutput { + fn new() -> Self { + Self { + stream: StandardStream::stdout(ColorChoice::Always), + first_header: true, + counters: Counters::default(), + } + } + + fn print_header(&mut self, message: &str) -> Result<(), Error> { + if !self.first_header { + writeln!(&mut self.stream)?; + } + self.first_header = false; + writeln!(&mut self.stream, "= {message} =\n")?; + Ok(()) + } + + fn set_color(&mut self, color: Color, bold: bool) -> Result<(), Error> { + self.stream + .set_color(ColorSpec::new().set_fg(Some(color)).set_bold(bold))?; + Ok(()) + } + + fn reset(&mut self) -> Result<(), std::io::Error> { + self.stream.reset() + } + + fn log_line(&mut self, level: LogLevel, message: &str) -> Result<(), Error> { + match level { + LogLevel::Pass => { + self.counters.pass += 1; + self.set_color(Color::Green, false)?; + writeln!(&mut self.stream, "PASS: {}", message)?; + } + LogLevel::Info => { + writeln!(&mut self.stream, "INFO: {}", message)?; + } + LogLevel::Skip => { + self.counters.skip += 1; + writeln!(&mut self.stream, "SKIP: {}", message)?; + } + LogLevel::Notice => { + self.counters.notice += 1; + self.set_color(Color::White, true)?; + writeln!(&mut self.stream, "NOTICE: {}", message)?; + } + LogLevel::Warn => { + self.counters.warn += 1; + self.set_color(Color::Yellow, false)?; + writeln!(&mut self.stream, "WARN: {}", message)?; + } + LogLevel::Fail => { + self.counters.fail += 1; + self.set_color(Color::Red, true)?; + writeln!(&mut self.stream, "FAIL: {}", message)?; + } + } + self.reset()?; + Ok(()) + } + + fn log_pass<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> { + self.log_line(LogLevel::Pass, message.as_ref()) + } + + fn log_info<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> { + self.log_line(LogLevel::Info, message.as_ref()) + } + + fn log_skip<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> { + self.log_line(LogLevel::Skip, message.as_ref()) + } + + fn log_notice<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> { + self.log_line(LogLevel::Notice, message.as_ref()) + } + + fn log_warn<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> { + self.log_line(LogLevel::Warn, message.as_ref()) + } + + fn log_fail<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> { + self.log_line(LogLevel::Fail, message.as_ref()) + } + + fn print_summary(&mut self) -> Result<(), Error> { + self.print_header("SUMMARY")?; + + let total = self.counters.fail + + self.counters.pass + + self.counters.notice + + self.counters.skip + + self.counters.warn; + + writeln!(&mut self.stream, "TOTAL: {total}")?; + self.set_color(Color::Green, false)?; + writeln!(&mut self.stream, "PASSED: {}", self.counters.pass)?; + self.reset()?; + writeln!(&mut self.stream, "SKIPPED: {}", self.counters.skip)?; + writeln!(&mut self.stream, "NOTICE: {}", self.counters.notice)?; + if self.counters.warn > 0 { + self.set_color(Color::Yellow, false)?; + writeln!(&mut self.stream, "WARNINGS: {}", self.counters.warn)?; + } + if self.counters.fail > 0 { + self.set_color(Color::Red, true)?; + writeln!(&mut self.stream, "FAILURES: {}", self.counters.fail)?; + } + if self.counters.warn > 0 || self.counters.fail > 0 { + let (color, bold) = if self.counters.fail > 0 { + (Color::Red, true) + } else { + (Color::Yellow, false) + }; + + self.set_color(color, bold)?; + writeln!( + &mut self.stream, + "\nATTENTION: Please check the output for detailed information!", + )?; + if self.counters.fail > 0 { + writeln!( + &mut self.stream, + "Try to solve the problems one at a time and rerun this checklist tool again.", + )?; + } + } + self.reset()?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_is_kernel_version_compatible( + expected_versions: &[&str], + unexpected_versions: &[&str], + upgraded: bool, + ) { + let mut checker = UpgradeCheckerBuilder::new( + "bookworm", + "trixie", + "proxmox-backup", + 3, + 4, + 0, + "running version: 3.4", + ) + .build(); + + checker.upgraded = upgraded; + + for version in expected_versions { + assert!( + checker.is_kernel_version_compatible(version), + "compatible kernel version '{version}' did not pass as expected!" + ); + } + for version in unexpected_versions { + assert!( + !checker.is_kernel_version_compatible(version), + "incompatible kernel version '{version}' passed as expected!" + ); + } + } + + #[test] + fn test_before_upgrade_kernel_version_compatibility() { + let expected_versions = &["6.2.16-20-pve", "6.5.13-6-pve", "6.8.12-1-pve"]; + let unexpected_versions = &["6.1.10-1-pve", "5.19.17-2-pve"]; + + test_is_kernel_version_compatible(expected_versions, unexpected_versions, false); + } + + #[test] + fn test_after_upgrade_kernel_version_compatibility() { + let expected_versions = &["6.14.6-1-pve", "6.17.0-1-pve"]; + let unexpected_versions = &["6.12.1-1-pve", "6.2.1-1-pve"]; + + test_is_kernel_version_compatible(expected_versions, unexpected_versions, true); + } +} -- 2.47.2 _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel ^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [pdm-devel] [PATCH proxmox 1/1] upgrade-checks: add upgrade checker crate 2025-09-05 9:59 ` [pdm-devel] [PATCH proxmox 1/1] upgrade-checks: add upgrade checker crate Shannon Sterz @ 2025-09-05 14:08 ` Michael Köppl 0 siblings, 0 replies; 7+ messages in thread From: Michael Köppl @ 2025-09-05 14:08 UTC (permalink / raw) To: Proxmox Datacenter Manager development discussion; +Cc: pdm-devel This seems to be missing the directory and files for Debian packaging. Or is this not supposed to be packaged (at least for now)? On Fri Sep 5, 2025 at 11:59 AM CEST, Shannon Sterz wrote: > that allows easily creating upgrade checks for proxmox products > > Signed-off-by: Shannon Sterz <s.sterz@proxmox.com> > --- > Cargo.toml | 3 + > proxmox-upgrade-checks/Cargo.toml | 21 + > proxmox-upgrade-checks/src/lib.rs | 856 ++++++++++++++++++++++++++++++ > 3 files changed, 880 insertions(+) > create mode 100644 proxmox-upgrade-checks/Cargo.toml > create mode 100644 proxmox-upgrade-checks/src/lib.rs > > diff --git a/Cargo.toml b/Cargo.toml > index 8f185b09..c91c1647 100644 > --- a/Cargo.toml > +++ b/Cargo.toml > @@ -46,6 +46,7 @@ members = [ > "proxmox-tfa", > "proxmox-time", > "proxmox-time-api", > + "proxmox-upgrade-checks", > "proxmox-uuid", > "proxmox-worker-task", > "pbs-api-types", > @@ -106,6 +107,7 @@ serde_json = "1.0" > serde_plain = "1.0" > syn = { version = "2", features = [ "full", "visit-mut" ] } > tar = "0.4" > +termcolor = "1.1.2" > tokio = "1.6" > tokio-openssl = "0.6.1" > tokio-stream = "0.1.0" > @@ -122,6 +124,7 @@ zstd = { version = "0.12", features = [ "bindgen" ] } > # workspace dependencies > proxmox-acme = { version = "0.5.3", path = "proxmox-acme", default-features = false } > proxmox-api-macro = { version = "1.3.2", path = "proxmox-api-macro" } > +proxmox-apt = { version = "0.11.7", path = "proxmox-apt" } > proxmox-apt-api-types = { version = "1.0.2", path = "proxmox-apt-api-types" } > proxmox-auth-api = { version = "0.4.0", path = "proxmox-auth-api" } > proxmox-async = { version = "0.4.1", path = "proxmox-async" } > diff --git a/proxmox-upgrade-checks/Cargo.toml b/proxmox-upgrade-checks/Cargo.toml > new file mode 100644 > index 00000000..acd3e837 > --- /dev/null > +++ b/proxmox-upgrade-checks/Cargo.toml > @@ -0,0 +1,21 @@ > +[package] > +name = "proxmox-upgrade-checks" > +description = "Helpers to implement upgrade checks for Proxmox products." > +version = "0.1.0" > + > +authors.workspace = true > +edition.workspace = true > +license.workspace = true > +repository.workspace = true > +homepage.workspace = true > +exclude.workspace = true > +rust-version.workspace = true > + > +[dependencies] > +anyhow.workspace = true > +const_format.workspace = true > +regex.workspace = true > +termcolor.workspace = true > + > +proxmox-apt = { workspace = true, features = [ "cache" ] } > +proxmox-apt-api-types.workspace = true > diff --git a/proxmox-upgrade-checks/src/lib.rs b/proxmox-upgrade-checks/src/lib.rs > new file mode 100644 > index 00000000..9d7566a1 > --- /dev/null > +++ b/proxmox-upgrade-checks/src/lib.rs > @@ -0,0 +1,856 @@ > +use std::path::Path; > +use std::{io::Write, path::PathBuf}; > + > +use anyhow::{format_err, Error}; > +use const_format::concatcp; > +use regex::Regex; > +use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; > + > +use proxmox_apt::repositories; > +use proxmox_apt_api_types::{APTRepositoryFile, APTRepositoryPackageType}; > + > +/// Easily create and configure an upgrade checker for Proxmox products. > +pub struct UpgradeCheckerBuilder { > + old_suite: String, > + new_suite: String, > + meta_package_name: String, > + minimum_major_version: u8, > + minimum_minor_version: u8, > + minimum_pkgrel: u8, > + apt_state_file: Option<PathBuf>, > + api_server_package: Option<String>, > + running_api_server_version: String, > + services_list: Vec<String>, > +} > + > +impl UpgradeCheckerBuilder { > + /// Create a new UpgradeCheckerBuilder > + /// > + /// * `old_suite`: The Debian suite before the upgrade. > + /// * `new_suite`: The Debian suite after the upgrade. > + /// * `meta_package_name`: The name of the product's meta package. > + /// * `minimum_major_version`: The minimum major version before the upgrade. > + /// * `minimum_minor_version`: The minimum minor version before the upgrade. > + /// * `minimum_pkgrel`: The minimum package release before the upgrade. > + /// * `running_api_server_version`: The currently running API server version. > + pub fn new( > + old_suite: &str, > + new_suite: &str, > + meta_package_name: &str, > + minimum_major_version: u8, > + minimum_minor_version: u8, > + minimum_pkgrel: u8, > + running_api_server_version: &str, > + ) -> UpgradeCheckerBuilder { > + UpgradeCheckerBuilder { > + old_suite: old_suite.into(), > + new_suite: new_suite.into(), > + meta_package_name: meta_package_name.into(), > + minimum_major_version, > + minimum_minor_version, > + minimum_pkgrel, > + apt_state_file: None, > + api_server_package: None, > + running_api_server_version: running_api_server_version.into(), > + services_list: Vec::new(), > + } > + } > + > + /// Set the location of the APT state file. > + pub fn set_apt_state_file_location<P: AsRef<Path>>(&mut self, path: P) { > + self.apt_state_file = Some(path.as_ref().into()); > + } > + > + /// Builder-style method to set the location of the APT state file. > + pub fn with_apt_state_file_location<P: AsRef<Path>>(mut self, path: P) -> Self { > + self.set_apt_state_file_location(path); > + self > + } > + > + /// Set the API server package name. > + pub fn set_api_server_package(&mut self, api_server_package: &str) { > + self.apt_state_file = Some(api_server_package.into()); > + } > + > + /// Builder-style method to set the API server package name. > + pub fn with_api_server_package(mut self, api_server_package: &str) -> Self { > + self.set_api_server_package(api_server_package); > + self > + } > + > + /// Add a service to the list of services that will be checked. > + pub fn add_service_to_checks(mut self, service_name: &str) -> Self { > + self.services_list.push(service_name.into()); > + self > + } > + > + /// Construct the UpgradeChecker, consumes the UpgradeCheckerBuilder > + pub fn build(mut self) -> UpgradeChecker { > + UpgradeChecker { > + output: ConsoleOutput::new(), > + upgraded: false, > + old_suite: self.old_suite, > + new_suite: self.new_suite, > + minimum_major_version: self.minimum_major_version, > + minimum_minor_version: self.minimum_minor_version, > + minimum_pkgrel: self.minimum_pkgrel, > + apt_state_file: self.apt_state_file.take().unwrap_or_else(|| { > + PathBuf::from(format!( > + "/var/lib/{}/pkg-state.json", > + &self.meta_package_name > + )) > + }), > + api_server_package: self > + .api_server_package > + .unwrap_or_else(|| self.meta_package_name.clone()), > + running_api_server_version: self.running_api_server_version, > + meta_package_name: self.meta_package_name, > + services_list: self.services_list, > + } > + } > +} > + > +/// Helpers to easily construct a set of upgrade checks. > +pub struct UpgradeChecker { > + output: ConsoleOutput, > + upgraded: bool, > + old_suite: String, > + new_suite: String, > + meta_package_name: String, > + minimum_major_version: u8, > + minimum_minor_version: u8, > + minimum_pkgrel: u8, > + apt_state_file: PathBuf, > + api_server_package: String, > + running_api_server_version: String, > + services_list: Vec<String>, > +} > + > +impl UpgradeChecker { > + /// Run all checks. > + pub fn run(&mut self) -> Result<(), Error> { > + self.check_packages()?; > + self.check_misc()?; > + self.summary() > + } > + > + /// Run miscellaneous checks. > + pub fn check_misc(&mut self) -> Result<(), Error> { > + self.output.print_header("MISCELLANEOUS CHECKS")?; > + self.check_services()?; > + self.check_time_sync()?; > + self.check_apt_repos()?; > + self.check_bootloader()?; > + self.check_dkms_modules()?; > + Ok(()) > + } > + > + /// Print a summary of all checks run so far. > + pub fn summary(&mut self) -> Result<(), Error> { > + self.output.print_summary() > + } > + > + /// Run all package related checks. > + pub fn check_packages(&mut self) -> Result<(), Error> { > + self.output.print_header(&format!( > + "CHECKING VERSION INFORMATION FOR {} PACKAGES", > + self.meta_package_name.to_uppercase() > + ))?; > + > + self.check_upgradable_packages()?; > + > + let pkg_versions = proxmox_apt::get_package_versions( > + &self.meta_package_name, > + &self.api_server_package, > + &self.running_api_server_version, > + &[], > + )?; > + > + self.check_meta_package_version(&pkg_versions)?; > + self.check_kernel_compat(&pkg_versions)?; > + Ok(()) > + } > + > + fn check_upgradable_packages(&mut self) -> Result<(), Error> { > + self.output.log_info("Checking for package updates..")?; > + > + let result = proxmox_apt::list_available_apt_update(&self.apt_state_file); > + match result { > + Err(err) => { > + self.output.log_warn(format!("{err}"))?; > + self.output > + .log_fail("unable to retrieve list of package updates!")?; > + } > + Ok(package_status) => { > + if package_status.is_empty() { > + self.output.log_pass("all packages up-to-date")?; > + } else { > + let pkgs = package_status > + .iter() > + .map(|pkg| pkg.package.clone()) > + .collect::<Vec<String>>() > + .join(", "); > + self.output.log_warn(format!( > + "updates for the following packages are available:\n {pkgs}", > + ))?; > + } > + } > + } > + Ok(()) > + } > + > + fn check_meta_package_version( > + &mut self, > + pkg_versions: &[proxmox_apt_api_types::APTUpdateInfo], > + ) -> Result<(), Error> { > + self.output.log_info(format!( > + "Checking {} package version..", > + self.meta_package_name > + ))?; > + > + let meta_pkg = pkg_versions > + .iter() > + .find(|pkg| pkg.package.as_str() == self.meta_package_name); > + > + if let Some(meta_pkg) = meta_pkg { > + let pkg_version = Regex::new(r"^(\d+)\.(\d+)[.-](\d+)")?; > + let captures = pkg_version.captures(&meta_pkg.old_version); > + if let Some(captures) = captures { > + let maj = Self::extract_version_from_captures(1, &captures)?; > + let min = Self::extract_version_from_captures(2, &captures)?; > + let pkgrel = Self::extract_version_from_captures(3, &captures)?; > + > + let min_version = format!( > + "{}.{}.{}", > + self.minimum_major_version, self.minimum_minor_version, self.minimum_pkgrel > + ); > + > + if (maj > self.minimum_major_version && self.minimum_major_version != 0) > + // Handle alpha and beta version upgrade checks: > + || (self.minimum_major_version == 0 && min > self.minimum_minor_version) > + { > + self.output.log_pass(format!("Already upgraded to {maj}.{min}"))?; > + self.upgraded = true; > + } else if maj >= self.minimum_major_version > + && min >= self.minimum_minor_version > + && pkgrel >= self.minimum_pkgrel > + { > + self.output.log_pass(format!( > + "'{}' has version >= {min_version}", > + self.meta_package_name > + ))?; > + } else { > + self.output.log_fail(format!( > + "'{}' package is too old, please upgrade to >= {min_version}", > + self.meta_package_name > + ))?; > + } > + } else { > + self.output.log_fail(format!( > + "could not match the '{}' package version, \ > + is it installed?", > + self.meta_package_name > + ))?; > + } > + } else { > + self.output > + .log_fail(format!("'{}' package not found!", self.meta_package_name))?; > + } > + Ok(()) > + } > + > + fn is_kernel_version_compatible(&self, running_version: &str) -> bool { > + // TODO: rework this to parse out maj.min.patch and do numerical comparison and detect > + // those with a "bpo12" as backport kernels from the older release. > + const MINIMUM_RE: &str = r"6\.(?:14\.(?:[1-9]\d+|[6-9])|1[5-9])[^~]*"; > + const ARBITRARY_RE: &str = r"(?:1[4-9]|2\d+)\.(?:[0-9]|\d{2,})[^~]*-pve"; > + > + let re = if self.upgraded { > + concatcp!(r"^(?:", MINIMUM_RE, r"|", ARBITRARY_RE, r")$") > + } else { > + r"^(?:6\.(?:2|5|8|11|14))" > + }; > + let re = Regex::new(re).expect("failed to compile kernel compat regex"); > + > + re.is_match(running_version) > + } > + > + fn check_kernel_compat( > + &mut self, > + pkg_versions: &[proxmox_apt_api_types::APTUpdateInfo], > + ) -> Result<(), Error> { > + self.output.log_info("Check running kernel version..")?; > + > + let kinstalled = if self.upgraded { > + "proxmox-kernel-6.14" > + } else { > + "proxmox-kernel-6.8" > + }; > + > + let output = std::process::Command::new("uname").arg("-r").output(); > + match output { > + Err(_err) => self > + .output > + .log_fail("unable to determine running kernel version.")?, > + Ok(ret) => { > + let running_version = std::str::from_utf8(&ret.stdout[..ret.stdout.len() - 1])?; > + if self.is_kernel_version_compatible(running_version) { > + if self.upgraded { > + self.output.log_pass(format!( > + "running new kernel '{running_version}' after upgrade." > + ))?; > + } else { > + self.output.log_pass(format!( > + "running kernel '{running_version}' is considered suitable for \ > + upgrade." > + ))?; > + } > + } else { > + let installed_kernel = pkg_versions > + .iter() > + .find(|pkg| pkg.package.as_str() == kinstalled); > + if installed_kernel.is_some() { > + self.output.log_warn(format!( > + "a suitable kernel '{kinstalled}' is installed, but an \ > + unsuitable '{running_version}' is booted, missing reboot?!", > + ))?; > + } else { > + self.output.log_warn(format!( > + "unexpected running and installed kernel '{running_version}'.", > + ))?; > + } > + } > + } > + } > + Ok(()) > + } > + > + fn extract_version_from_captures( > + index: usize, > + captures: ®ex::Captures, > + ) -> Result<u8, Error> { > + if let Some(capture) = captures.get(index) { > + let val = capture.as_str().parse::<u8>()?; > + Ok(val) > + } else { > + Ok(0) > + } > + } > + > + fn check_bootloader(&mut self) -> Result<(), Error> { > + self.output > + .log_info("Checking bootloader configuration...")?; > + > + let sd_boot_installed = > + Path::new("/usr/share/doc/systemd-boot/changelog.Debian.gz").is_file(); > + > + if !Path::new("/sys/firmware/efi").is_dir() { > + if sd_boot_installed { > + self.output.log_warn( > + "systemd-boot package installed on legacy-boot system is not \ > + necessary, consider removing it", > + )?; > + return Ok(()); > + } > + self.output > + .log_skip("System booted in legacy-mode - no need for additional packages.")?; > + return Ok(()); > + } > + > + let mut boot_ok = true; > + if Path::new("/etc/kernel/proxmox-boot-uuids").is_file() { > + // Package version check needs to be run before > + if !self.upgraded { > + let output = std::process::Command::new("proxmox-boot-tool") > + .arg("status") > + .output() > + .map_err(|err| { > + format_err!("failed to retrieve proxmox-boot-tool status - {err}") > + })?; > + let re = Regex::new(r"configured with:.* (uefi|systemd-boot) \(versions:") > + .expect("failed to proxmox-boot-tool status"); > + if re.is_match(std::str::from_utf8(&output.stdout)?) { > + self.output > + .log_skip("not yet upgraded, systemd-boot still needed for bootctl")?; > + return Ok(()); > + } > + } > + } else { > + if !Path::new("/usr/share/doc/grub-efi-amd64/changelog.Debian.gz").is_file() { > + self.output.log_warn( > + "System booted in uefi mode but grub-efi-amd64 meta-package not installed, \ > + new grub versions will not be installed to /boot/efi! > + Install grub-efi-amd64.", > + )?; > + boot_ok = false; > + } > + if Path::new("/boot/efi/EFI/BOOT/BOOTX64.efi").is_file() { > + let output = std::process::Command::new("debconf-show") > + .arg("--db") > + .arg("configdb") > + .arg("grub-efi-amd64") > + .arg("grub-pc") > + .output() > + .map_err(|err| format_err!("failed to retrieve debconf settings - {err}"))?; > + let re = Regex::new(r"grub2/force_efi_extra_removable: +true(?:\n|$)") > + .expect("failed to compile dbconfig regex"); > + if !re.is_match(std::str::from_utf8(&output.stdout)?) { > + self.output.log_warn( > + "Removable bootloader found at '/boot/efi/EFI/BOOT/BOOTX64.efi', but GRUB packages \ > + not set up to update it!\nRun the following command:\n\ > + echo 'grub-efi-amd64 grub2/force_efi_extra_removable boolean true' | debconf-set-selections -v -u\n\ > + Then reinstall GRUB with 'apt install --reinstall grub-efi-amd64'" > + )?; > + boot_ok = false; > + } > + } > + } > + if sd_boot_installed { > + self.output.log_fail( > + "systemd-boot meta-package installed. This will cause problems on upgrades of other \ > + boot-related packages.\n\ > + Remove the 'systemd-boot' package.\n\ > + Please consult the upgrade guide for further information!" > + )?; > + boot_ok = false; > + } > + if boot_ok { > + self.output > + .log_pass("bootloader packages installed correctly")?; > + } > + Ok(()) > + } > + > + fn check_apt_repos(&mut self) -> Result<(), Error> { > + self.output > + .log_info("Checking for package repository suite mismatches..")?; > + > + let mut strange_suite = false; > + let mut mismatches = Vec::new(); > + let mut found_suite: Option<(String, String)> = None; > + > + let (repo_files, _repo_errors, _digest) = repositories::repositories()?; > + for repo_file in repo_files { > + self.check_repo_file( > + &mut found_suite, > + &mut mismatches, > + &mut strange_suite, > + repo_file, > + )?; > + } > + > + match (mismatches.is_empty(), strange_suite) { > + (true, false) => self.output.log_pass("found no suite mismatch")?, > + (true, true) => self > + .output > + .log_notice("found no suite mismatches, but found at least one strange suite")?, > + (false, _) => { > + let mut message = String::from( > + "Found mixed old and new packages repository suites, fix before upgrading!\ > + \n Mismatches:", > + ); > + for (suite, location) in mismatches.iter() { > + message.push_str( > + format!("\n found suite '{suite}' at '{location}'").as_str(), > + ); > + } > + message.push('\n'); > + self.output.log_fail(message)? > + } > + } > + > + Ok(()) > + } > + > + fn check_dkms_modules(&mut self) -> Result<(), Error> { > + let kver = std::process::Command::new("uname") > + .arg("-r") > + .output() > + .map_err(|err| format_err!("failed to retrieve running kernel version - {err}"))?; > + > + let output = std::process::Command::new("dkms") > + .arg("status") > + .arg("-k") > + .arg(std::str::from_utf8(&kver.stdout)?) > + .output(); > + match output { > + Err(_err) => self.output.log_skip("could not get dkms status")?, > + Ok(ret) => { > + let num_dkms_modules = std::str::from_utf8(&ret.stdout)?.lines().count(); > + if num_dkms_modules == 0 { > + self.output.log_pass("no dkms modules found")?; > + } else { > + self.output > + .log_warn("dkms modules found, this might cause issues during upgrade.")?; > + } > + } > + } > + Ok(()) > + } > + > + fn check_repo_file( > + &mut self, > + found_suite: &mut Option<(String, String)>, > + mismatches: &mut Vec<(String, String)>, > + strange_suite: &mut bool, > + repo_file: APTRepositoryFile, > + ) -> Result<(), Error> { > + for repo in repo_file.repositories { > + if !repo.enabled || repo.types == [APTRepositoryPackageType::DebSrc] { > + continue; > + } > + for suite in &repo.suites { > + let suite = match suite.find(&['-', '/'][..]) { > + Some(n) => &suite[0..n], > + None => suite, > + }; > + > + if suite != self.old_suite && suite != self.new_suite { > + let location = repo_file.path.clone().unwrap_or_default(); > + self.output.log_notice(format!( > + "found unusual suite '{suite}', neither old '{}' nor new \ > + '{}'..\n Affected file {location}\n Please \ > + assure this is shipping compatible packages for the upgrade!", > + self.old_suite, self.new_suite > + ))?; > + *strange_suite = true; > + continue; > + } > + > + if let Some((ref current_suite, ref current_location)) = found_suite { > + let location = repo_file.path.clone().unwrap_or_default(); > + if suite != current_suite { > + if mismatches.is_empty() { > + mismatches.push((current_suite.clone(), current_location.clone())); > + mismatches.push((suite.to_string(), location)); > + } else { > + mismatches.push((suite.to_string(), location)); > + } > + } > + } else { > + let location = repo_file.path.clone().unwrap_or_default(); > + *found_suite = Some((suite.to_string(), location)); > + } > + } > + } > + Ok(()) > + } > + > + fn get_systemd_unit_state( > + &self, > + unit: &str, > + ) -> Result<(SystemdUnitState, SystemdUnitState), Error> { > + let output = std::process::Command::new("systemctl") > + .arg("is-enabled") > + .arg(unit) > + .output() > + .map_err(|err| format_err!("failed to execute - {err}"))?; > + > + let enabled_state = match output.stdout.as_slice() { > + b"enabled\n" => SystemdUnitState::Enabled, > + b"disabled\n" => SystemdUnitState::Disabled, > + _ => SystemdUnitState::Unknown, > + }; > + > + let output = std::process::Command::new("systemctl") > + .arg("is-active") > + .arg(unit) > + .output() > + .map_err(|err| format_err!("failed to execute - {err}"))?; > + > + let active_state = match output.stdout.as_slice() { > + b"active\n" => SystemdUnitState::Active, > + b"inactive\n" => SystemdUnitState::Inactive, > + b"failed\n" => SystemdUnitState::Failed, > + _ => SystemdUnitState::Unknown, > + }; > + Ok((enabled_state, active_state)) > + } > + > + fn check_services(&mut self) -> Result<(), Error> { > + self.output.log_info(format!( > + "Checking {} daemon services..", > + self.meta_package_name > + ))?; > + > + for service in self.services_list.as_slice() { > + match self.get_systemd_unit_state(service)? { > + (_, SystemdUnitState::Active) => { > + self.output > + .log_pass(format!("systemd unit '{service}' is in state 'active'"))?; > + } > + (_, SystemdUnitState::Inactive) => { > + self.output.log_fail(format!( > + "systemd unit '{service}' is in state 'inactive'\ > + \n Please check the service for errors and start it.", > + ))?; > + } > + (_, SystemdUnitState::Failed) => { > + self.output.log_fail(format!( > + "systemd unit '{service}' is in state 'failed'\ > + \n Please check the service for errors and start it.", > + ))?; > + } > + (_, _) => { > + self.output.log_fail(format!( > + "systemd unit '{service}' is not in state 'active'\ > + \n Please check the service for errors and start it.", > + ))?; > + } > + } > + } > + Ok(()) > + } > + > + fn check_time_sync(&mut self) -> Result<(), Error> { > + self.output > + .log_info("Checking for supported & active NTP service..")?; > + if self.get_systemd_unit_state("systemd-timesyncd.service")?.1 == SystemdUnitState::Active { > + self.output.log_warn( > + "systemd-timesyncd is not the best choice for time-keeping on servers, due to only \ > + applying updates on boot.\ > + \n While not necessary for the upgrade it's recommended to use one of:\ > + \n * chrony (Default in new Proxmox product installations)\ > + \n * ntpsec\ > + \n * openntpd" > + )?; > + } else if self.get_systemd_unit_state("ntp.service")?.1 == SystemdUnitState::Active { > + self.output.log_info( > + "Debian deprecated and removed the ntp package for Bookworm, but the system \ > + will automatically migrate to the 'ntpsec' replacement package on upgrade.", > + )?; > + } else if self.get_systemd_unit_state("chrony.service")?.1 == SystemdUnitState::Active > + || self.get_systemd_unit_state("openntpd.service")?.1 == SystemdUnitState::Active > + || self.get_systemd_unit_state("ntpsec.service")?.1 == SystemdUnitState::Active > + { > + self.output > + .log_pass("Detected active time synchronisation unit")?; > + } else { > + self.output.log_warn( > + "No (active) time synchronisation daemon (NTP) detected, but synchronized systems \ > + are important!", > + )?; > + } > + Ok(()) > + } > +} > + > +#[derive(PartialEq)] > +enum SystemdUnitState { > + Active, > + Enabled, > + Disabled, > + Failed, > + Inactive, > + Unknown, > +} > + > +#[derive(Default)] > +struct Counters { > + pass: u64, > + skip: u64, > + notice: u64, > + warn: u64, > + fail: u64, > +} > + > +enum LogLevel { > + Pass, > + Info, > + Skip, > + Notice, > + Warn, > + Fail, > +} > + > +struct ConsoleOutput { > + stream: StandardStream, > + first_header: bool, > + counters: Counters, > +} > + > +impl ConsoleOutput { > + fn new() -> Self { > + Self { > + stream: StandardStream::stdout(ColorChoice::Always), > + first_header: true, > + counters: Counters::default(), > + } > + } > + > + fn print_header(&mut self, message: &str) -> Result<(), Error> { > + if !self.first_header { > + writeln!(&mut self.stream)?; > + } > + self.first_header = false; > + writeln!(&mut self.stream, "= {message} =\n")?; > + Ok(()) > + } > + > + fn set_color(&mut self, color: Color, bold: bool) -> Result<(), Error> { > + self.stream > + .set_color(ColorSpec::new().set_fg(Some(color)).set_bold(bold))?; > + Ok(()) > + } > + > + fn reset(&mut self) -> Result<(), std::io::Error> { > + self.stream.reset() > + } > + > + fn log_line(&mut self, level: LogLevel, message: &str) -> Result<(), Error> { > + match level { > + LogLevel::Pass => { > + self.counters.pass += 1; > + self.set_color(Color::Green, false)?; > + writeln!(&mut self.stream, "PASS: {}", message)?; > + } > + LogLevel::Info => { > + writeln!(&mut self.stream, "INFO: {}", message)?; > + } > + LogLevel::Skip => { > + self.counters.skip += 1; > + writeln!(&mut self.stream, "SKIP: {}", message)?; > + } > + LogLevel::Notice => { > + self.counters.notice += 1; > + self.set_color(Color::White, true)?; > + writeln!(&mut self.stream, "NOTICE: {}", message)?; > + } > + LogLevel::Warn => { > + self.counters.warn += 1; > + self.set_color(Color::Yellow, false)?; > + writeln!(&mut self.stream, "WARN: {}", message)?; > + } > + LogLevel::Fail => { > + self.counters.fail += 1; > + self.set_color(Color::Red, true)?; > + writeln!(&mut self.stream, "FAIL: {}", message)?; > + } > + } > + self.reset()?; > + Ok(()) > + } > + > + fn log_pass<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> { > + self.log_line(LogLevel::Pass, message.as_ref()) > + } > + > + fn log_info<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> { > + self.log_line(LogLevel::Info, message.as_ref()) > + } > + > + fn log_skip<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> { > + self.log_line(LogLevel::Skip, message.as_ref()) > + } > + > + fn log_notice<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> { > + self.log_line(LogLevel::Notice, message.as_ref()) > + } > + > + fn log_warn<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> { > + self.log_line(LogLevel::Warn, message.as_ref()) > + } > + > + fn log_fail<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> { > + self.log_line(LogLevel::Fail, message.as_ref()) > + } > + > + fn print_summary(&mut self) -> Result<(), Error> { > + self.print_header("SUMMARY")?; > + > + let total = self.counters.fail > + + self.counters.pass > + + self.counters.notice > + + self.counters.skip > + + self.counters.warn; > + > + writeln!(&mut self.stream, "TOTAL: {total}")?; > + self.set_color(Color::Green, false)?; > + writeln!(&mut self.stream, "PASSED: {}", self.counters.pass)?; > + self.reset()?; > + writeln!(&mut self.stream, "SKIPPED: {}", self.counters.skip)?; > + writeln!(&mut self.stream, "NOTICE: {}", self.counters.notice)?; > + if self.counters.warn > 0 { > + self.set_color(Color::Yellow, false)?; > + writeln!(&mut self.stream, "WARNINGS: {}", self.counters.warn)?; > + } > + if self.counters.fail > 0 { > + self.set_color(Color::Red, true)?; > + writeln!(&mut self.stream, "FAILURES: {}", self.counters.fail)?; > + } > + if self.counters.warn > 0 || self.counters.fail > 0 { > + let (color, bold) = if self.counters.fail > 0 { > + (Color::Red, true) > + } else { > + (Color::Yellow, false) > + }; > + > + self.set_color(color, bold)?; > + writeln!( > + &mut self.stream, > + "\nATTENTION: Please check the output for detailed information!", > + )?; > + if self.counters.fail > 0 { > + writeln!( > + &mut self.stream, > + "Try to solve the problems one at a time and rerun this checklist tool again.", > + )?; > + } > + } > + self.reset()?; > + Ok(()) > + } > +} > + > +#[cfg(test)] > +mod tests { > + use super::*; > + > + fn test_is_kernel_version_compatible( > + expected_versions: &[&str], > + unexpected_versions: &[&str], > + upgraded: bool, > + ) { > + let mut checker = UpgradeCheckerBuilder::new( > + "bookworm", > + "trixie", > + "proxmox-backup", > + 3, > + 4, > + 0, > + "running version: 3.4", > + ) > + .build(); > + > + checker.upgraded = upgraded; > + > + for version in expected_versions { > + assert!( > + checker.is_kernel_version_compatible(version), > + "compatible kernel version '{version}' did not pass as expected!" > + ); > + } > + for version in unexpected_versions { > + assert!( > + !checker.is_kernel_version_compatible(version), > + "incompatible kernel version '{version}' passed as expected!" > + ); > + } > + } > + > + #[test] > + fn test_before_upgrade_kernel_version_compatibility() { > + let expected_versions = &["6.2.16-20-pve", "6.5.13-6-pve", "6.8.12-1-pve"]; > + let unexpected_versions = &["6.1.10-1-pve", "5.19.17-2-pve"]; > + > + test_is_kernel_version_compatible(expected_versions, unexpected_versions, false); > + } > + > + #[test] > + fn test_after_upgrade_kernel_version_compatibility() { > + let expected_versions = &["6.14.6-1-pve", "6.17.0-1-pve"]; > + let unexpected_versions = &["6.12.1-1-pve", "6.2.1-1-pve"]; > + > + test_is_kernel_version_compatible(expected_versions, unexpected_versions, true); > + } > +} _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel ^ permalink raw reply [flat|nested] 7+ messages in thread
* [pdm-devel] [PATCH datacenter-manager 1/2] server: add pdmAtoB upgrade checker script 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 9:59 ` 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 3 siblings, 0 replies; 7+ messages in thread From: Shannon Sterz @ 2025-09-05 9:59 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 1e6f5a3..dce7052 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ members = [ "cli/client", "cli/admin", "cli/proxmox-fido2", + "cli/pdmAtoB", ] [workspace.dependencies] @@ -64,6 +65,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 = "0.1" @@ -175,4 +177,5 @@ zstd = { version = "0.12", features = [ "bindgen" ] } # 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 71f8da9..c12a196 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] 7+ 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 ` [pdm-devel] [PATCH proxmox 1/1] upgrade-checks: add upgrade checker crate Shannon Sterz 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 ` 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 3 siblings, 0 replies; 7+ 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(¶m); + + 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] 7+ messages in thread
* Re: [pdm-devel] [PATCH datacenter-manager/proxmox 0/3] version command and upgrade checks for pdm 2025-09-05 9:59 [pdm-devel] [PATCH datacenter-manager/proxmox 0/3] version command and upgrade checks for pdm Shannon Sterz ` (2 preceding siblings ...) 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 ` Michael Köppl 3 siblings, 0 replies; 7+ messages in thread From: Michael Köppl @ 2025-09-05 15:18 UTC (permalink / raw) To: Proxmox Datacenter Manager development discussion; +Cc: pdm-devel Tested the upgrade script on a bookworm-based PDM install - Checked that installed systemd-boot package was detected and warning shown - Forced time sync unit checks to warn by installing timesyncd, ntp, uninstalling chrony, etc. scenarios - Having timesyncd installed and active produced the expected warning - Having ntp installed produced the expected info message - Checked the various version mismatch cases and if they're reported correctly Also gave the `versions` command a spin. The only thing I noticed there is that the output started with the following line for me: proxmox-datacenter unknown running kernel: 6.8.12-9-pve Is "unknown" expected here? Other than that, I didn't notice anything off. Seems to work as advertised. Left 1 comment on the 1/1 patch that adds the proxmox-upgrade-checks crate. Tested-by: Michael Köppl <m.koeppl@proxmox.com> On Fri Sep 5, 2025 at 11:59 AM CEST, Shannon Sterz wrote: > 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 > bookworm based releases, so that users can check the prerequisites > before upgrading to a trixie based release. a similar series for the > current trixie-based master branches will follow shortly. > > 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 paches are intended to be applied against the following trees: > > * in the proxmox repository this should be applied against the > `stable-bookworm` branch. > * in the proxmox-datacenter-manager repository the two commits should be > applied against the commit f6ca650d (ui: add woff2 roboto flex variant > to ui packaging) > > the latter was chosen as it seems to be the last commit in the > proxmox-datacenter-manager repo that was intended for bookworm, since > that repo is missing a `stable-bookworm` branch so far. > > 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 | 856 ++++++++++++++++++++++++++++++ > 3 files changed, 880 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, 972 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 _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel ^ permalink raw reply [flat|nested] 7+ messages in thread
* [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 datacenter-manager 2/2] cli/admin: add a versions command to show current package versions Shannon Sterz 0 siblings, 1 reply; 7+ 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] 7+ 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 Shannon Sterz @ 2025-09-05 10:04 ` Shannon Sterz 0 siblings, 0 replies; 7+ 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(¶m); + + 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] 7+ messages in thread
end of thread, other threads:[~2025-09-05 15:19 UTC | newest] Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 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-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-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
This is an external index of several public inboxes, see mirroring instructions on how to clone and mirror all data and code used by this external index.