public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
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





  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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal