From: Thomas Lamprecht <t.lamprecht@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH ha-manager 3/3] api: status: add disarm-ha and arm-ha endpoints and CLI wiring
Date: Mon, 9 Mar 2026 22:57:10 +0100 [thread overview]
Message-ID: <20260309220128.973793-4-t.lamprecht@proxmox.com> (raw)
In-Reply-To: <20260309220128.973793-1-t.lamprecht@proxmox.com>
Expose the disarm/arm mechanism as two separate POST endpoints under
/cluster/ha/status/ and wire them into the crm-command CLI namespace.
Extend the fencing status entry with disarming/disarmed states and
the active resource mode. Each LRM entry shows 'watchdog released'
once in disarm mode. The master and service status lines include the
disarm state when applicable.
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
---
src/PVE/API2/HA/Status.pm | 141 ++++++++++++++++++++++++++++++++------
src/PVE/CLI/ha_manager.pm | 2 +
2 files changed, 122 insertions(+), 21 deletions(-)
diff --git a/src/PVE/API2/HA/Status.pm b/src/PVE/API2/HA/Status.pm
index a6b00b9..3082743 100644
--- a/src/PVE/API2/HA/Status.pm
+++ b/src/PVE/API2/HA/Status.pm
@@ -52,7 +52,10 @@ __PACKAGE__->register_method({
my ($param) = @_;
my $result = [
- { name => 'current' }, { name => 'manager_status' },
+ { name => 'current' },
+ { name => 'manager_status' },
+ { name => 'disarm-ha' },
+ { name => 'arm-ha' },
];
return $result;
@@ -144,10 +147,17 @@ __PACKAGE__->register_method({
optional => 1,
},
armed_state => {
- description => "For type 'fencing'. Whether HA fencing is armed"
- . " or on standby.",
+ description => "For type 'fencing'. Whether HA is armed, on standby,"
+ . " disarming or disarmed.",
type => "string",
- enum => ['armed', 'standby'],
+ enum => ['armed', 'standby', 'disarming', 'disarmed'],
+ optional => 1,
+ },
+ resource_mode => {
+ description =>
+ "For type 'fencing'. How resources are handled while disarmed.",
+ type => "string",
+ enum => ['freeze', 'ignore'],
optional => 1,
},
},
@@ -184,9 +194,13 @@ __PACKAGE__->register_method({
my $extra_status = '';
+ if (my $disarm = $status->{disarm}) {
+ $extra_status .= " - $disarm->{state}, resource mode: $disarm->{mode}";
+ }
my $datacenter_config = eval { cfs_read_file('datacenter.cfg') } // {};
if (my $crs = $datacenter_config->{crs}) {
- $extra_status = " - $crs->{ha} load CRS" if $crs->{ha} && $crs->{ha} ne 'basic';
+ $extra_status .= " - $crs->{ha} load CRS"
+ if $crs->{ha} && $crs->{ha} ne 'basic';
}
my $time_str = localtime($status->{timestamp});
my $status_text = "$master ($status_str, $time_str)$extra_status";
@@ -206,16 +220,32 @@ __PACKAGE__->register_method({
&& defined($status->{timestamp})
&& $timestamp_to_status->($ctime, $status->{timestamp}) eq 'active';
- my $armed_state = $crm_active ? 'armed' : 'standby';
- my $crm_wd = $crm_active ? "CRM watchdog active" : "CRM watchdog standby";
- push @$res,
- {
- id => 'fencing',
- type => 'fencing',
- node => $status->{master_node} // $nodename,
- status => "$armed_state ($crm_wd)",
- armed_state => $armed_state,
- };
+ if (my $disarm = $status->{disarm}) {
+ my $mode = $disarm->{mode} // 'unknown';
+ my $disarm_state = $disarm->{state} // 'unknown';
+ my $wd_released = $disarm_state eq 'disarmed';
+ my $crm_wd = $wd_released ? "CRM watchdog released" : "CRM watchdog active";
+ push @$res,
+ {
+ id => 'fencing',
+ type => 'fencing',
+ node => $status->{master_node} // $nodename,
+ status => "$disarm_state, resource mode: $mode ($crm_wd)",
+ armed_state => $disarm_state,
+ resource_mode => $mode,
+ };
+ } else {
+ my $armed_state = $crm_active ? 'armed' : 'standby';
+ my $crm_wd = $crm_active ? "CRM watchdog active" : "CRM watchdog standby";
+ push @$res,
+ {
+ id => 'fencing',
+ type => 'fencing',
+ node => $status->{master_node} // $nodename,
+ status => "$armed_state ($crm_wd)",
+ armed_state => $armed_state,
+ };
+ }
foreach my $node (sort keys %{ $status->{node_status} }) {
my $active_count =
@@ -236,11 +266,17 @@ __PACKAGE__->register_method({
my $lrm_state = $lrm_status->{state} || 'unknown';
# LRM holds its watchdog while it has the agent lock
- my $lrm_wd =
- ($status_str eq 'active'
- && ($lrm_state eq 'active' || $lrm_state eq 'maintenance'))
- ? 'watchdog active'
- : 'watchdog standby';
+ my $lrm_wd;
+ if (
+ $status_str eq 'active'
+ && ($lrm_state eq 'active' || $lrm_state eq 'maintenance')
+ ) {
+ $lrm_wd = 'watchdog active';
+ } elsif ($lrm_mode && $lrm_mode eq 'disarm') {
+ $lrm_wd = 'watchdog released';
+ } else {
+ $lrm_wd = 'watchdog standby';
+ }
if ($status_str eq 'active') {
$lrm_mode ||= 'active';
@@ -253,7 +289,7 @@ __PACKAGE__->register_method({
$status_str = $lrm_state;
}
}
- } elsif ($lrm_mode && $lrm_mode eq 'maintenance') {
+ } elsif ($lrm_mode && ($lrm_mode eq 'maintenance' || $lrm_mode eq 'disarm')) {
$status_str = "$lrm_mode mode";
}
@@ -284,6 +320,15 @@ __PACKAGE__->register_method({
my $node = $data->{node} // '---'; # to be safe against manual tinkering
$data->{state} = PVE::HA::Tools::get_verbose_service_state($ss, $sc);
+
+ # show disarm resource mode instead of generic verbose state
+ if (my $disarm = $status->{disarm}) {
+ my $mode = $disarm->{mode};
+ if (!$ss && $mode eq 'ignore') {
+ $data->{state} = 'ignore';
+ }
+ }
+
$data->{status} = "$sid ($node, $data->{state})"; # backward compat. and CLI
# also return common resource attributes
@@ -348,4 +393,58 @@ __PACKAGE__->register_method({
},
});
+__PACKAGE__->register_method({
+ name => 'disarm-ha',
+ path => 'disarm-ha',
+ method => 'POST',
+ description => "Request disarming the HA stack, releasing all watchdogs cluster-wide.",
+ permissions => {
+ check => ['perm', '/', ['Sys.Console']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ 'resource-mode' => {
+ description => "Controls how HA managed resources are handled while disarmed."
+ . " The current state of resources is not affected."
+ . " 'freeze': new commands and state changes are not applied."
+ . " 'ignore': resources are removed from HA tracking and can be"
+ . " managed as if they were not HA managed.",
+ type => 'string',
+ enum => ['freeze', 'ignore'],
+ },
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ PVE::HA::Config::queue_crm_commands("disarm-ha $param->{'resource-mode'}");
+
+ return undef;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'arm-ha',
+ path => 'arm-ha',
+ method => 'POST',
+ description => "Request re-arming the HA stack after it was disarmed.",
+ permissions => {
+ check => ['perm', '/', ['Sys.Console']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {},
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ PVE::HA::Config::queue_crm_commands("arm-ha");
+
+ return undef;
+ },
+});
+
1;
diff --git a/src/PVE/CLI/ha_manager.pm b/src/PVE/CLI/ha_manager.pm
index be6978c..f257c01 100644
--- a/src/PVE/CLI/ha_manager.pm
+++ b/src/PVE/CLI/ha_manager.pm
@@ -298,6 +298,8 @@ our $cmddef = {
enable => [__PACKAGE__, 'node-maintenance-set', ['node'], { disable => 0 }],
disable => [__PACKAGE__, 'node-maintenance-set', ['node'], { disable => 1 }],
},
+ 'disarm-ha' => ['PVE::API2::HA::Status', 'disarm-ha', ['resource-mode']],
+ 'arm-ha' => ['PVE::API2::HA::Status', 'arm-ha', []],
},
};
--
2.47.3
prev parent reply other threads:[~2026-03-09 22:02 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-03-09 21:57 [PATCH ha-manager 0/3] fix #2751: implement disarm/arm HA for safer cluster maintenance Thomas Lamprecht
2026-03-09 21:57 ` [PATCH ha-manager 1/3] api: status: add fencing status entry with armed/standby state Thomas Lamprecht
2026-03-09 21:57 ` [PATCH ha-manager 2/3] fix #2751: implement disarm-ha and arm-ha for safe cluster maintenance Thomas Lamprecht
2026-03-09 21:57 ` Thomas Lamprecht [this message]
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=20260309220128.973793-4-t.lamprecht@proxmox.com \
--to=t.lamprecht@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