From: Kefu Chai <k.chai@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH manager 1/5] pve8to9: extract ceph checks into PVE::Ceph::UpgradeCheck
Date: Tue, 28 Apr 2026 10:45:34 +0800 [thread overview]
Message-ID: <20260428024538.3559017-2-k.chai@proxmox.com> (raw)
In-Reply-To: <20260428024538.3559017-1-k.chai@proxmox.com>
Move the body of check_ceph() into a new PVE::Ceph::UpgradeCheck module.
The module exposes run_checks() which returns an arrayref of
{ level, msg } records, and each caller formats the records with its
own log_* helpers. This matches the idiomatic PVE pattern where modules
return data and callers handle presentation.
Prepares the ground for adding more ceph upgrade checks and for
exposing the same checks via a standalone 'pveceph upgrade-check'
subcommand in a follow-up. No behaviour change: pve8to9 emits the
same messages, in the same order, through the same log_* helpers.
Signed-off-by: Kefu Chai <k.chai@proxmox.com>
---
PVE/CLI/pve8to9.pm | 203 +++--------------------
PVE/Ceph/Makefile | 1 +
PVE/Ceph/UpgradeCheck.pm | 342 +++++++++++++++++++++++++++++++++++++++
3 files changed, 363 insertions(+), 183 deletions(-)
create mode 100644 PVE/Ceph/UpgradeCheck.pm
diff --git a/PVE/CLI/pve8to9.pm b/PVE/CLI/pve8to9.pm
index afc4785e..06dde101 100644
--- a/PVE/CLI/pve8to9.pm
+++ b/PVE/CLI/pve8to9.pm
@@ -14,6 +14,7 @@ use PVE::API2::Cluster::Ceph;
use PVE::AccessControl;
use PVE::Ceph::Tools;
+use PVE::Ceph::UpgradeCheck;
use PVE::Cluster;
use PVE::Corosync;
use PVE::INotify;
@@ -61,21 +62,6 @@ my $older_suites = {
my ($min_pve_major, $min_pve_minor, $min_pve_pkgrel) = (8, 4, 0);
-my $ceph_release2code = {
- '12' => 'Luminous',
- '13' => 'Mimic',
- '14' => 'Nautilus',
- '15' => 'Octopus',
- '16' => 'Pacific',
- '17' => 'Quincy',
- '18' => 'Reef',
- '19' => 'Squid',
- '20' => 'Tentacle',
-};
-my $ceph_supported_release = 19; # the version we support for upgrading (i.e., available on both)
-my $ceph_supported_code_name = $ceph_release2code->{"$ceph_supported_release"}
- or die "inconsistent source code, could not map expected ceph version to code name!";
-
my $forced_legacy_cgroup = 0;
my $counters = {
@@ -588,180 +574,31 @@ sub check_cluster_corosync {
}
}
-sub check_ceph {
- print_header("CHECKING HYPER-CONVERGED CEPH STATUS");
-
- if (PVE::Ceph::Tools::check_ceph_inited(1)) {
- log_info("hyper-converged ceph setup detected!");
- } else {
- log_skip("no hyper-converged ceph setup detected!");
- return;
- }
-
- log_info("getting Ceph status/health information..");
- my $ceph_status = eval { PVE::API2::Ceph->status({ node => $nodename }); };
- my $noout = eval { PVE::API2::Cluster::Ceph->get_flag({ flag => "noout" }); };
- if ($@) {
- log_fail("failed to get 'noout' flag status - $@");
- }
-
- my $noout_wanted = 1;
-
- if (!$ceph_status || !$ceph_status->{health}) {
- log_fail("unable to determine Ceph status!");
- } else {
- my $ceph_health = $ceph_status->{health}->{status};
- if (!$ceph_health) {
- log_fail("unable to determine Ceph health!");
- } elsif ($ceph_health eq 'HEALTH_OK') {
- log_pass("Ceph health reported as 'HEALTH_OK'.");
- } elsif (
- $ceph_health eq 'HEALTH_WARN'
- && $noout
- && (keys %{ $ceph_status->{health}->{checks} } == 1)
- ) {
- log_pass(
- "Ceph health reported as 'HEALTH_WARN' with a single failing check and 'noout' flag set."
- );
- } else {
- log_warn(
- "Ceph health reported as '$ceph_health'.\n Use the PVE dashboard or 'ceph -s'"
- . " to determine the specific issues and try to resolve them.");
- }
- }
-
- # TODO: check OSD min-required version, if to low it breaks stuff!
-
- log_info("checking local Ceph version..");
- if (my $release = eval { PVE::Ceph::Tools::get_local_version(1) }) {
- my $code_name = $ceph_release2code->{"$release"} || 'unknown';
- if ($release == $ceph_supported_release) {
- log_pass(
- "found expected Ceph $ceph_supported_release $ceph_supported_code_name release.");
- } elsif ($release > $ceph_supported_release) {
- log_warn(
- "found newer Ceph release $release $code_name as the expected $ceph_supported_release"
- . " $ceph_supported_code_name, installed third party repos?!");
- } else {
- log_fail("Hyper-converged Ceph $release $code_name is to old for upgrade!\n"
- . " Upgrade Ceph first to $ceph_supported_code_name following our how-to:\n"
- . " <https://pve.proxmox.com/wiki/Category:Ceph_Upgrade>");
- }
- } else {
- log_fail("unable to determine local Ceph version!");
- }
-
- log_info("getting Ceph daemon versions..");
- my $ceph_versions = eval { PVE::Ceph::Tools::get_cluster_versions(undef, 1); };
- if (!$ceph_versions) {
- log_fail("unable to determine Ceph daemon versions!");
- } else {
- my $services = [
- { 'key' => 'mon', 'name' => 'monitor' },
- { 'key' => 'mgr', 'name' => 'manager' },
- { 'key' => 'mds', 'name' => 'MDS' },
- { 'key' => 'osd', 'name' => 'OSD' },
- ];
-
- my $ceph_versions_simple = {};
- my $ceph_versions_commits = {};
- for my $type (keys %$ceph_versions) {
- for my $full_version (keys $ceph_versions->{$type}->%*) {
- if ($full_version =~ m/^(.*) \((.*)\).*\(.*\)$/) {
- # String is in the form of
- # ceph version 17.2.6 (810db68029296377607028a6c6da1ec06f5a2b27) quincy (stable)
- # only check the first part, e.g. 'ceph version 17.2.6', the commit hash can
- # be different
- $ceph_versions_simple->{$type}->{$1} = 1;
- $ceph_versions_commits->{$type}->{$2} = 1;
- }
- }
- }
-
- for my $service (@$services) {
- my ($name, $key) = $service->@{ 'name', 'key' };
- if (my $service_versions = $ceph_versions_simple->{$key}) {
- if (keys %$service_versions == 0) {
- log_skip("no running instances detected for daemon type $name.");
- } elsif (keys %$service_versions == 1) {
- log_pass("single running version detected for daemon type $name.");
- } else {
- log_warn("multiple running versions detected for daemon type $name!");
- }
- } else {
- log_skip("unable to determine versions of running Ceph $name instances.");
- }
- my $service_commits = $ceph_versions_commits->{$key};
- log_info(
- "different builds of same version detected for an $name. Are you in the middle of the upgrade?"
- ) if $service_commits && keys %$service_commits > 1;
- }
+sub log_ceph_upgrade_message {
+ my ($message) = @_;
- my $overall_versions = $ceph_versions->{overall};
- if (!$overall_versions) {
- log_warn("unable to determine overall Ceph daemon versions!");
- } elsif (keys %$overall_versions == 1) {
- log_pass("single running overall version detected for all Ceph daemon types.");
- $noout_wanted = !$upgraded; # off post-upgrade, on pre-upgrade
- } elsif (keys $ceph_versions_simple->{overall}->%* != 1) {
- log_warn(
- "overall version mismatch detected, check 'ceph versions' output for details!");
- }
- }
-
- if ($noout) {
- if ($noout_wanted) {
- log_pass("'noout' flag set to prevent rebalancing during cluster-wide upgrades.");
- } else {
- log_warn("'noout' flag set, Ceph cluster upgrade seems finished.");
- }
- } elsif ($noout_wanted) {
- log_warn("'noout' flag not set - recommended to prevent rebalancing during upgrades.");
- }
+ my ($level, $msg) = $message->@{qw(level msg)};
- log_info("checking Ceph config..");
- my $conf = PVE::Cluster::cfs_read_file('ceph.conf');
- if (%$conf) {
- my $global = $conf->{global};
+ return log_pass($msg) if $level eq 'pass';
+ return log_info($msg) if $level eq 'info';
+ return log_notice($msg) if $level eq 'notice';
+ return log_warn($msg) if $level eq 'warn';
+ return log_fail($msg) if $level eq 'fail';
+ return log_skip($msg) if $level eq 'skip';
- my $global_monhost = $global->{mon_host} // $global->{"mon host"} // $global->{"mon-host"};
- if (!defined($global_monhost)) {
- log_warn(
- "No 'mon_host' entry found in ceph config.\n It's recommended to add mon_host with"
- . " all monitor addresses (without ports) to the global section.");
- }
-
- my $ipv6 = $global->{ms_bind_ipv6} // $global->{"ms bind ipv6"}
- // $global->{"ms-bind-ipv6"};
- if ($ipv6) {
- my $ipv4 = $global->{ms_bind_ipv4} // $global->{"ms bind ipv4"}
- // $global->{"ms-bind-ipv4"};
- if ($ipv6 eq 'true' && (!defined($ipv4) || $ipv4 ne 'false')) {
- log_warn(
- "'ms_bind_ipv6' is enabled but 'ms_bind_ipv4' is not disabled.\n Make sure to"
- . " disable 'ms_bind_ipv4' for ipv6 only clusters, or add an ipv4 network to public/cluster network."
- );
- }
- }
+ return log_info($msg);
+}
- if (defined($global->{keyring})) {
- log_warn(
- "[global] config section contains 'keyring' option, which will prevent services from"
- . " starting with Nautilus.\n Move 'keyring' option to [client] section instead."
- );
- }
+sub check_ceph {
+ print_header("CHECKING HYPER-CONVERGED CEPH STATUS");
- } else {
- log_warn("Empty ceph config found");
- }
+ my $messages = PVE::Ceph::UpgradeCheck::run_checks(
+ nodename => $nodename,
+ upgraded => $upgraded,
+ );
- my $local_ceph_ver = PVE::Ceph::Tools::get_local_version(1);
- if (defined($local_ceph_ver)) {
- if ($local_ceph_ver <= 14) {
- log_fail("local Ceph version too low, at least Octopus required..");
- }
- } else {
- log_fail("unable to determine local Ceph version.");
+ for my $m ($messages->@*) {
+ log_ceph_upgrade_message($m);
}
}
diff --git a/PVE/Ceph/Makefile b/PVE/Ceph/Makefile
index 2901ebe5..b64912bb 100644
--- a/PVE/Ceph/Makefile
+++ b/PVE/Ceph/Makefile
@@ -4,6 +4,7 @@ PERLSOURCE = \
Releases.pm \
Services.pm \
Tools.pm \
+ UpgradeCheck.pm \
all:
diff --git a/PVE/Ceph/UpgradeCheck.pm b/PVE/Ceph/UpgradeCheck.pm
new file mode 100644
index 00000000..6998caf2
--- /dev/null
+++ b/PVE/Ceph/UpgradeCheck.pm
@@ -0,0 +1,342 @@
+package PVE::Ceph::UpgradeCheck;
+
+# Produces advisory messages about a Ceph cluster's upgrade-readiness.
+#
+# Callers (PVE::CLI::pve8to9, 'pveceph upgrade-check') invoke run_checks()
+# and format the returned records with their own log_* helpers.
+#
+# Each record is a hashref of the form:
+# { level => 'pass'|'info'|'notice'|'warn'|'fail'|'skip', msg => 'text' }
+
+use strict;
+use warnings;
+
+use PVE::API2::Ceph;
+use PVE::API2::Cluster::Ceph;
+use PVE::Ceph::Tools;
+use PVE::Cluster;
+
+my $ceph_release2code = {
+ '12' => 'Luminous',
+ '13' => 'Mimic',
+ '14' => 'Nautilus',
+ '15' => 'Octopus',
+ '16' => 'Pacific',
+ '17' => 'Quincy',
+ '18' => 'Reef',
+ '19' => 'Squid',
+ '20' => 'Tentacle',
+};
+my $default_supported_release = 19; # available before and after the current major upgrade
+my $default_supported_code_name = $ceph_release2code->{"$default_supported_release"}
+ or die "inconsistent source code, could not map expected ceph version to code name!";
+
+sub run_checks {
+ my (%args) = @_;
+
+ my $nodename = $args{nodename}
+ or die "run_checks: 'nodename' argument is required\n";
+ my $supported_release = $args{supported_release} // $default_supported_release;
+ my $upgraded = $args{upgraded} // 0;
+
+ my @messages;
+
+ if (!PVE::Ceph::Tools::check_ceph_inited(1)) {
+ push @messages, { level => 'skip', msg => "no hyper-converged ceph setup detected!" };
+ return \@messages;
+ }
+ push @messages, { level => 'info', msg => "hyper-converged ceph setup detected!" };
+
+ my ($health_msgs, $noout) = check_health($nodename);
+ push @messages, $health_msgs->@*;
+
+ # TODO: check OSD min-required version, if to low it breaks stuff!
+
+ my ($version_msgs, $noout_wanted) = check_versions($supported_release, $upgraded);
+ push @messages, $version_msgs->@*;
+
+ push @messages, check_noout_flag($noout, $noout_wanted)->@*;
+
+ push @messages, check_config()->@*;
+
+ push @messages, check_local_version_minimum()->@*;
+
+ return \@messages;
+}
+
+sub check_health {
+ my ($nodename) = @_;
+
+ my @out;
+ push @out, { level => 'info', msg => "getting Ceph status/health information.." };
+
+ my $ceph_status = eval { PVE::API2::Ceph->status({ node => $nodename }); };
+ my $noout = eval { PVE::API2::Cluster::Ceph->get_flag({ flag => "noout" }); };
+ if ($@) {
+ push @out, { level => 'fail', msg => "failed to get 'noout' flag status - $@" };
+ }
+
+ if (!$ceph_status || !$ceph_status->{health}) {
+ push @out, { level => 'fail', msg => "unable to determine Ceph status!" };
+ return (\@out, $noout);
+ }
+
+ my $ceph_health = $ceph_status->{health}->{status};
+ if (!$ceph_health) {
+ push @out, { level => 'fail', msg => "unable to determine Ceph health!" };
+ } elsif ($ceph_health eq 'HEALTH_OK') {
+ push @out, { level => 'pass', msg => "Ceph health reported as 'HEALTH_OK'." };
+ } elsif (
+ $ceph_health eq 'HEALTH_WARN'
+ && $noout
+ && (keys %{ $ceph_status->{health}->{checks} } == 1)
+ ) {
+ push @out,
+ {
+ level => 'pass',
+ msg =>
+ "Ceph health reported as 'HEALTH_WARN' with a single failing check and 'noout' flag set.",
+ };
+ } else {
+ push @out,
+ {
+ level => 'warn',
+ msg =>
+ "Ceph health reported as '$ceph_health'.\n Use the PVE dashboard or 'ceph -s'"
+ . " to determine the specific issues and try to resolve them.",
+ };
+ }
+
+ return (\@out, $noout);
+}
+
+sub check_versions {
+ my ($supported_release, $upgraded) = @_;
+
+ my @out;
+ my $noout_wanted = 1;
+
+ my $supported_code_name = $supported_release == $default_supported_release
+ ? $default_supported_code_name
+ : ($ceph_release2code->{"$supported_release"} // 'unknown');
+
+ push @out, { level => 'info', msg => "checking local Ceph version.." };
+ if (my $release = eval { PVE::Ceph::Tools::get_local_version(1) }) {
+ my $code_name = $ceph_release2code->{"$release"} || 'unknown';
+ if ($release == $supported_release) {
+ push @out,
+ {
+ level => 'pass',
+ msg => "found expected Ceph $supported_release $supported_code_name release.",
+ };
+ } elsif ($release > $supported_release) {
+ push @out,
+ {
+ level => 'warn',
+ msg => "found newer Ceph release $release $code_name as the expected"
+ . " $supported_release $supported_code_name, installed third party repos?!",
+ };
+ } else {
+ push @out,
+ {
+ level => 'fail',
+ msg => "Hyper-converged Ceph $release $code_name is to old for upgrade!\n"
+ . " Upgrade Ceph first to $supported_code_name following our how-to:\n"
+ . " <https://pve.proxmox.com/wiki/Category:Ceph_Upgrade>",
+ };
+ }
+ } else {
+ push @out, { level => 'fail', msg => "unable to determine local Ceph version!" };
+ }
+
+ push @out, { level => 'info', msg => "getting Ceph daemon versions.." };
+ my $ceph_versions = eval { PVE::Ceph::Tools::get_cluster_versions(undef, 1); };
+ if (!$ceph_versions) {
+ push @out, { level => 'fail', msg => "unable to determine Ceph daemon versions!" };
+ return (\@out, $noout_wanted);
+ }
+
+ my $services = [
+ { 'key' => 'mon', 'name' => 'monitor' },
+ { 'key' => 'mgr', 'name' => 'manager' },
+ { 'key' => 'mds', 'name' => 'MDS' },
+ { 'key' => 'osd', 'name' => 'OSD' },
+ ];
+
+ my $ceph_versions_simple = {};
+ my $ceph_versions_commits = {};
+ for my $type (keys %$ceph_versions) {
+ for my $full_version (keys $ceph_versions->{$type}->%*) {
+ if ($full_version =~ m/^(.*) \((.*)\).*\(.*\)$/) {
+ # String is in the form of
+ # ceph version 17.2.6 (810db68029296377607028a6c6da1ec06f5a2b27) quincy (stable)
+ # only check the first part, e.g. 'ceph version 17.2.6', the commit hash can
+ # be different
+ $ceph_versions_simple->{$type}->{$1} = 1;
+ $ceph_versions_commits->{$type}->{$2} = 1;
+ }
+ }
+ }
+
+ for my $service (@$services) {
+ my ($name, $key) = $service->@{ 'name', 'key' };
+ if (my $service_versions = $ceph_versions_simple->{$key}) {
+ if (keys %$service_versions == 0) {
+ push @out,
+ {
+ level => 'skip',
+ msg => "no running instances detected for daemon type $name.",
+ };
+ } elsif (keys %$service_versions == 1) {
+ push @out,
+ {
+ level => 'pass',
+ msg => "single running version detected for daemon type $name.",
+ };
+ } else {
+ push @out,
+ {
+ level => 'warn',
+ msg => "multiple running versions detected for daemon type $name!",
+ };
+ }
+ } else {
+ push @out,
+ {
+ level => 'skip',
+ msg => "unable to determine versions of running Ceph $name instances.",
+ };
+ }
+ my $service_commits = $ceph_versions_commits->{$key};
+ if ($service_commits && keys %$service_commits > 1) {
+ push @out,
+ {
+ level => 'info',
+ msg =>
+ "different builds of same version detected for an $name. Are you in the middle of the upgrade?",
+ };
+ }
+ }
+
+ my $overall_versions = $ceph_versions->{overall};
+ if (!$overall_versions) {
+ push @out, { level => 'warn', msg => "unable to determine overall Ceph daemon versions!" };
+ } elsif (keys %$overall_versions == 1) {
+ push @out,
+ {
+ level => 'pass',
+ msg => "single running overall version detected for all Ceph daemon types.",
+ };
+ $noout_wanted = !$upgraded; # off post-upgrade, on pre-upgrade
+ } elsif (keys $ceph_versions_simple->{overall}->%* != 1) {
+ push @out,
+ {
+ level => 'warn',
+ msg =>
+ "overall version mismatch detected, check 'ceph versions' output for details!",
+ };
+ }
+
+ return (\@out, $noout_wanted);
+}
+
+sub check_noout_flag {
+ my ($noout, $noout_wanted) = @_;
+
+ my @out;
+ if ($noout) {
+ if ($noout_wanted) {
+ push @out,
+ {
+ level => 'pass',
+ msg => "'noout' flag set to prevent rebalancing during cluster-wide upgrades.",
+ };
+ } else {
+ push @out,
+ {
+ level => 'warn',
+ msg => "'noout' flag set, Ceph cluster upgrade seems finished.",
+ };
+ }
+ } elsif ($noout_wanted) {
+ push @out,
+ {
+ level => 'warn',
+ msg => "'noout' flag not set - recommended to prevent rebalancing during upgrades.",
+ };
+ }
+
+ return \@out;
+}
+
+sub check_config {
+ my @out;
+
+ push @out, { level => 'info', msg => "checking Ceph config.." };
+ my $conf = PVE::Cluster::cfs_read_file('ceph.conf');
+ if (!%$conf) {
+ push @out, { level => 'warn', msg => "Empty ceph config found" };
+ return \@out;
+ }
+
+ my $global = $conf->{global};
+
+ my $global_monhost = $global->{mon_host} // $global->{"mon host"} // $global->{"mon-host"};
+ if (!defined($global_monhost)) {
+ push @out,
+ {
+ level => 'warn',
+ msg =>
+ "No 'mon_host' entry found in ceph config.\n It's recommended to add mon_host with"
+ . " all monitor addresses (without ports) to the global section.",
+ };
+ }
+
+ my $ipv6 = $global->{ms_bind_ipv6} // $global->{"ms bind ipv6"} // $global->{"ms-bind-ipv6"};
+ if ($ipv6) {
+ my $ipv4 = $global->{ms_bind_ipv4} // $global->{"ms bind ipv4"}
+ // $global->{"ms-bind-ipv4"};
+ if ($ipv6 eq 'true' && (!defined($ipv4) || $ipv4 ne 'false')) {
+ push @out,
+ {
+ level => 'warn',
+ msg =>
+ "'ms_bind_ipv6' is enabled but 'ms_bind_ipv4' is not disabled.\n Make sure to"
+ . " disable 'ms_bind_ipv4' for ipv6 only clusters, or add an ipv4 network to public/cluster network.",
+ };
+ }
+ }
+
+ if (defined($global->{keyring})) {
+ push @out,
+ {
+ level => 'warn',
+ msg =>
+ "[global] config section contains 'keyring' option, which will prevent services from"
+ . " starting with Nautilus.\n Move 'keyring' option to [client] section instead.",
+ };
+ }
+
+ return \@out;
+}
+
+sub check_local_version_minimum {
+ my @out;
+
+ my $local_ceph_ver = PVE::Ceph::Tools::get_local_version(1);
+ if (defined($local_ceph_ver)) {
+ if ($local_ceph_ver <= 14) {
+ push @out,
+ {
+ level => 'fail',
+ msg => "local Ceph version too low, at least Octopus required..",
+ };
+ }
+ } else {
+ push @out, { level => 'fail', msg => "unable to determine local Ceph version." };
+ }
+
+ return \@out;
+}
+
+1;
--
2.47.3
next prev parent reply other threads:[~2026-04-28 2:46 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-04-28 2:45 [PATCH manager 0/5] ceph: add 'pveceph upgrade-check' subcommand Kefu Chai
2026-04-28 2:45 ` Kefu Chai [this message]
2026-04-28 2:45 ` [PATCH manager 2/5] ceph: add pveceph upgrade-check command Kefu Chai
2026-04-28 2:45 ` [PATCH manager 3/5] ceph: add require_osd_release upgrade check Kefu Chai
2026-04-28 2:45 ` [PATCH manager 4/5] ceph: add require_min_compat_client " Kefu Chai
2026-04-28 2:45 ` [PATCH manager 5/5] ceph: drop duplicate release-to-codename map in upgrade checks Kefu Chai
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260428024538.3559017-2-k.chai@proxmox.com \
--to=k.chai@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is 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.