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





      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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal