]> git.proxmox.com Git - pve-ha-manager.git/blobdiff - src/PVE/HA/LRM.pm
env: datacenter config: include crs (cluster-resource-scheduling) setting
[pve-ha-manager.git] / src / PVE / HA / LRM.pm
index 5b986a454cc064b56d380e82bb97cc9015a0a509..5d2fa2c21656e787275e5d780e1e1047ada9ec76 100644 (file)
@@ -16,9 +16,14 @@ use PVE::HA::Resources;
 my $valid_states = {
     wait_for_agent_lock => "waiting for agent lock",
     active => "got agent_lock",
+    maintenance => "going into maintenance",
     lost_agent_lock => "lost agent_lock",
 };
 
+# we sleep ~10s per 'active' round, so if no services is available for >= 10 min we'd go in wait
+# state giving up the watchdog and the LRM lock voluntary, ensuring the WD can do no harm
+my $max_active_idle_rounds = 60;
+
 sub new {
     my ($this, $haenv) = @_;
 
@@ -35,6 +40,7 @@ sub new {
        # mode can be: active, reboot, shutdown, restart
        mode => 'active',
        cluster_state_update => 0,
+       active_idle_rounds => 0,
     }, $class;
 
     $self->set_local_status({ state =>         'wait_for_agent_lock' });
@@ -53,26 +59,35 @@ sub shutdown_request {
 
     my ($shutdown, $reboot) = $haenv->is_node_shutdown();
 
-    my $dc_ha_cfg = $haenv->get_ha_settings();
-    my $shutdown_policy = $dc_ha_cfg->{shutdown_policy} // 'conditional';
+    my $dc_cfg = $haenv->get_datacenter_settings();
+    my $shutdown_policy = $dc_cfg->{ha}->{shutdown_policy} // 'conditional';
 
     if ($shutdown) { # don't log this on service restart, only on node shutdown
        $haenv->log('info', "got shutdown request with shutdown policy '$shutdown_policy'");
     }
 
     my $freeze_all;
+    my $maintenance;
     if ($shutdown_policy eq 'conditional') {
        $freeze_all = $reboot;
     } elsif ($shutdown_policy eq 'freeze') {
        $freeze_all = 1;
     } elsif ($shutdown_policy eq 'failover') {
        $freeze_all = 0;
+    } elsif ($shutdown_policy eq 'migrate') {
+       $maintenance = 1;
     } else {
        $haenv->log('err', "unknown shutdown policy '$shutdown_policy', fall back to conditional");
        $freeze_all = $reboot;
     }
 
-    if ($shutdown) {
+    if ($maintenance) {
+       # we get marked as unaivalable by the manager, then all services will
+       # be migrated away, we'll still have the same "can we exit" clause than
+       # a normal shutdown -> no running service on this node
+       # FIXME: after X minutes, add shutdown command for remaining services,
+       # e.g., if they have no alternative node???
+    } elsif ($shutdown) {
        # *always* queue stop jobs for all services if the node shuts down,
        # independent if it's a reboot or a poweroff, else we may corrupt
        # services or hinder node shutdown
@@ -89,7 +104,10 @@ sub shutdown_request {
 
     if ($shutdown) {
        my $shutdown_type = $reboot ? 'reboot' : 'shutdown';
-       if ($freeze_all) {
+       if ($maintenance) {
+           $haenv->log('info', "$shutdown_type LRM, doing maintenance, removing this node from active list");
+           $self->{mode} = 'maintenance';
+       } elsif ($freeze_all) {
            $haenv->log('info', "$shutdown_type LRM, stop and freeze all services");
            $self->{mode} = 'restart';
        } else {
@@ -101,11 +119,11 @@ sub shutdown_request {
        $self->{mode} = 'restart';
     }
 
-    $self->{shutdown_request} = 1;
+    $self->{shutdown_request} = $haenv->get_time();
 
     eval { $self->update_lrm_status() or die "not quorate?\n"; };
     if (my $err = $@) {
-       $self->log('err', "unable to update lrm status file - $err");
+       $haenv->log('err', "unable to update lrm status file - $err");
     }
 }
 
@@ -168,6 +186,8 @@ sub update_service_status {
        return undef;
     } else {
        $self->{service_status} = $ms->{service_status} || {};
+       my $nodename = $haenv->nodename();
+       $self->{node_status} = $ms->{node_status}->{$nodename} || 'unknown';
        return 1;
     }
 }
@@ -203,17 +223,45 @@ sub get_protected_ha_agent_lock {
     return 0;
 }
 
-sub active_service_count {
+# only cares if any service has the local node as their node, independent of which req.state it is
+sub has_configured_service_on_local_node {
+    my ($self) = @_;
+
+    my $haenv = $self->{haenv};
+    my $nodename = $haenv->nodename();
+
+    my $ss = $self->{service_status};
+    foreach my $sid (keys %$ss) {
+       my $sd = $ss->{$sid};
+       next if !$sd->{node} || $sd->{node} ne $nodename;
+
+       return 1;
+    }
+    return 0;
+}
+
+sub is_fence_requested {
     my ($self) = @_;
 
     my $haenv = $self->{haenv};
 
+    my $nodename = $haenv->nodename();
+    my $ss = $self->{service_status};
+
+    my $fenced_services = PVE::HA::Tools::count_fenced_services($ss, $nodename);
+
+    return $fenced_services || $self->{node_status} eq 'fence';
+}
+
+sub active_service_count {
+    my ($self) = @_;
+
+    my $haenv = $self->{haenv};
     my $nodename = $haenv->nodename();
 
     my $ss = $self->{service_status};
 
     my $count = 0;
-
     foreach my $sid (keys %$ss) {
        my $sd = $ss->{$sid};
        next if !$sd->{node};
@@ -221,6 +269,7 @@ sub active_service_count {
        my $req_state = $sd->{state};
        next if !defined($req_state);
        next if $req_state eq 'stopped';
+       # NOTE: 'ignored' ones are already dropped by the manager from service_status
        next if $req_state eq 'freeze';
        # erroneous services are not managed by HA, don't count them as active
        next if $req_state eq 'error';
@@ -249,6 +298,17 @@ sub do_one_iteration {
     return $res;
 }
 
+# NOTE: this is disabling the self-fence mechanism, so it must NOT be called with active services
+# It's normally *only* OK on graceful shutdown (with no services, or all services frozen)
+my sub give_up_watchdog_protection {
+    my ($self) = @_;
+
+    if ($self->{ha_agent_wd}) {
+       $self->{haenv}->watchdog_close($self->{ha_agent_wd});
+       delete $self->{ha_agent_wd}; # only delete after close!
+    }
+}
+
 sub work {
     my ($self) = @_;
 
@@ -269,7 +329,7 @@ sub work {
 
     $self->update_service_status();
 
-    my $fence_request = PVE::HA::Tools::count_fenced_services($self->{service_status}, $haenv->nodename());
+    my $fence_request = $self->is_fence_requested();
 
     # do state changes first
 
@@ -300,6 +360,31 @@ sub work {
            $self->set_local_status({ state => 'lost_agent_lock'});
        } elsif (!$self->get_protected_ha_agent_lock()) {
            $self->set_local_status({ state => 'lost_agent_lock'});
+       } elsif ($self->{mode} eq 'maintenance') {
+           $self->set_local_status({ state => 'maintenance'});
+       } else {
+           if (!$self->has_configured_service_on_local_node() && !$self->run_workers()) {
+               # no active service configured for this node and all (old) workers are done
+               $self->{active_idle_rounds}++;
+               if ($self->{active_idle_rounds} > $max_active_idle_rounds) {
+                   $haenv->log('info', "node had no service configured for $max_active_idle_rounds rounds, going idle.\n");
+                   # safety: no active service & no running worker for quite some time -> OK
+                   $haenv->release_ha_agent_lock();
+                   give_up_watchdog_protection($self);
+                   $self->set_local_status({ state => 'wait_for_agent_lock'});
+                   $self->{active_idle_rounds} = 0;
+               }
+           } elsif ($self->{active_idle_rounds}) {
+               $self->{active_idle_rounds} = 0;
+           }
+       }
+    } elsif ($state eq 'maintenance') {
+
+       if ($fence_request) {
+           $haenv->log('err', "node need to be fenced during maintenance mode - releasing agent_lock\n");
+           $self->set_local_status({ state => 'lost_agent_lock'});
+       } elsif (!$self->get_protected_ha_agent_lock()) {
+           $self->set_local_status({ state => 'lost_agent_lock'});
        }
     }
 
@@ -335,31 +420,24 @@ sub work {
            if ($self->{shutdown_request}) {
 
                if ($self->{mode} eq 'restart') {
-
+                   # catch exited workers to update service state
+                   my $workers = $self->run_workers();
                    my $service_count = $self->active_service_count();
 
-                   if ($service_count == 0) {
-
-                       if ($self->run_workers() == 0) {
-                           if ($self->{ha_agent_wd}) {
-                               $haenv->watchdog_close($self->{ha_agent_wd});
-                               delete $self->{ha_agent_wd};
-                           }
-
-                           $shutdown = 1;
+                   if ($service_count == 0 && $workers == 0) {
+                       # safety: no active services or workers -> OK
+                       give_up_watchdog_protection($self);
+                       $shutdown = 1;
 
-                           # restart with no or freezed services, release the lock
-                           $haenv->release_ha_agent_lock();
-                       }
+                       # restart with no or freezed services, release the lock
+                       $haenv->release_ha_agent_lock();
                    }
                } else {
 
                    if ($self->run_workers() == 0) {
                        if ($self->{shutdown_errors} == 0) {
-                           if ($self->{ha_agent_wd}) {
-                               $haenv->watchdog_close($self->{ha_agent_wd});
-                               delete $self->{ha_agent_wd};
-                           }
+                           # safety: no active services and LRM shutdown -> OK
+                           give_up_watchdog_protection($self);
 
                            # shutdown with all services stopped thus release the lock
                            $haenv->release_ha_agent_lock();
@@ -393,10 +471,8 @@ sub work {
 
     } elsif ($state eq 'lost_agent_lock') {
 
-       # Note: watchdog is active an will triger soon!
-
+       # NOTE: watchdog is active an will trigger soon!
        # so we hope to get the lock back soon!
-
        if ($self->{shutdown_request}) {
 
            my $service_count = $self->active_service_count();
@@ -418,13 +494,8 @@ sub work {
                    }
                }
            } else {
-
-               # all services are stopped, so we can close the watchdog
-
-               if ($self->{ha_agent_wd}) {
-                   $haenv->watchdog_close($self->{ha_agent_wd});
-                   delete $self->{ha_agent_wd};
-               }
+               # safety: all services are stopped, so we can close the watchdog
+               give_up_watchdog_protection($self);
 
                return 0;
            }
@@ -432,6 +503,36 @@ sub work {
 
        $haenv->sleep(5);
 
+    } elsif ($state eq 'maintenance') {
+
+       my $startime = $haenv->get_time();
+       return if !$self->update_service_status();
+
+       # wait until all active services moved away
+       my $service_count = $self->active_service_count();
+
+       my $exit_lrm = 0;
+
+       if ($self->{shutdown_request}) {
+           if ($service_count == 0 && $self->run_workers() == 0) {
+               # safety: going into maintenance and all active services got moved -> OK
+               give_up_watchdog_protection($self);
+
+               $exit_lrm = 1;
+
+               # restart with no or freezed services, release the lock
+               $haenv->release_ha_agent_lock();
+           }
+       }
+
+       $self->manage_resources() if !$exit_lrm;
+
+       $self->update_lrm_status();
+
+       return 0 if $exit_lrm;
+
+       $haenv->sleep_until($startime + 5);
+
     } else {
 
        die "got unexpected status '$state'\n";
@@ -450,55 +551,61 @@ sub run_workers {
 
     # number of workers to start, if 0 we exec the command directly witouth forking
     my $max_workers = $haenv->get_max_workers();
-
     my $sc = $haenv->read_service_config();
 
-    while (($haenv->get_time() - $starttime) < 5) {
-       my $count =  $self->check_active_workers();
-
-       foreach my $sid (sort keys %{$self->{workers}}) {
-           last if $count >= $max_workers && $max_workers > 0;
-
-           my $w = $self->{workers}->{$sid};
-           if (!$w->{pid}) {
-               # only fork if we may else call exec_resource_agent
-               # directly (e.g. for regression tests)
-               if ($max_workers > 0) {
-                   my $pid = fork();
-                   if (!defined($pid)) {
-                       $haenv->log('err', "fork worker failed");
-                       $count = 0; last; # abort, try later
-                   } elsif ($pid == 0) {
-                       $haenv->after_fork(); # cleanup
-
-                       # do work
-                       my $res = -1;
-                       eval {
-                           $res = $self->exec_resource_agent($sid, $sc->{$sid}, $w->{state}, $w->{params});
-                       };
-                       if (my $err = $@) {
-                           $haenv->log('err', $err);
-                           POSIX::_exit(-1);
-                       }
-                       POSIX::_exit($res);
-                   } else {
-                       $count++;
-                       $w->{pid} = $pid;
-                   }
-               } else {
+    my $worker = $self->{workers};
+    # we only got limited time but want to ensure that every queued worker is scheduled
+    # eventually, so sort by the count a worker was seen here in this loop
+    my $fair_sorter = sub {
+       $worker->{$b}->{start_tries} <=> $worker->{$a}->{start_tries} || $a cmp $b
+    };
+
+    while (($haenv->get_time() - $starttime) <= 8) {
+       my $count = $self->check_active_workers();
+
+       for my $sid (sort $fair_sorter grep { !$worker->{$_}->{pid} } keys %$worker) {
+           my $w = $worker->{$sid};
+           # higher try-count means higher priority especially compared to newly queued jobs, so
+           # count every try to avoid starvation
+           $w->{start_tries}++;
+           next if $count >= $max_workers && $max_workers > 0;
+
+           # only fork if we may, else call exec_resource_agent directly (e.g. for tests)
+           if ($max_workers > 0) {
+               my $pid = fork();
+               if (!defined($pid)) {
+                   $haenv->log('err', "forking worker failed - $!");
+                   $count = 0; last; # abort, try later
+               } elsif ($pid == 0) {
+                   $haenv->after_fork(); # cleanup
+
+                   # do work
                    my $res = -1;
                    eval {
                        $res = $self->exec_resource_agent($sid, $sc->{$sid}, $w->{state}, $w->{params});
-                       $res = $res << 8 if $res > 0;
                    };
                    if (my $err = $@) {
                        $haenv->log('err', $err);
+                       POSIX::_exit(-1);
                    }
-                   if (defined($w->{uid})) {
-                       $self->resource_command_finished($sid, $w->{uid}, $res);
-                   } else {
-                       $self->stop_command_finished($sid, $res);
-                   }
+                   POSIX::_exit($res);
+               } else {
+                   $count++;
+                   $w->{pid} = $pid;
+               }
+           } else {
+               my $res = -1;
+               eval {
+                   $res = $self->exec_resource_agent($sid, $sc->{$sid}, $w->{state}, $w->{params});
+                   $res = $res << 8 if $res > 0;
+               };
+               if (my $err = $@) {
+                   $haenv->log('err', $err);
+               }
+               if (defined($w->{uid})) {
+                   $self->resource_command_finished($sid, $w->{uid}, $res);
+               } else {
+                   $self->stop_command_finished($sid, $res);
                }
            }
        }
@@ -526,13 +633,20 @@ sub manage_resources {
 
     foreach my $sid (keys %$ss) {
        my $sd = $ss->{$sid};
-       next if !$sd->{node};
-       next if !$sd->{uid};
+       next if !$sd->{node} || !$sd->{uid};
        next if $sd->{node} ne $nodename;
-       my $req_state = $sd->{state};
-       next if !defined($req_state);
-       next if $req_state eq 'freeze';
-       $self->queue_resource_command($sid, $sd->{uid}, $req_state, {'target' => $sd->{target}});
+       my $request_state = $sd->{state};
+       next if !defined($request_state);
+       # can only happen for restricted groups where the failed node itself needs to be the
+       # reocvery target. Always let the master first do so, it will then marked as 'stopped' and
+       # we can just continue normally. But we must NOT do anything with it while still in recovery
+       next if $request_state eq 'recovery';
+       next if $request_state eq 'freeze';
+
+       $self->queue_resource_command($sid, $sd->{uid}, $request_state, {
+           'target' => $sd->{target},
+           'timeout' => $sd->{timeout},
+       });
     }
 
     return $self->run_workers();
@@ -541,12 +655,11 @@ sub manage_resources {
 sub queue_resource_command {
     my ($self, $sid, $uid, $state, $params) = @_;
 
-    # do not queue the excatly same command twice as this may lead to
-    # an inconsistent HA state when the first command fails but the CRM
-    # does not process its failure right away and the LRM starts a second
-    # try, without the CRM knowing of it (race condition)
-    # The 'stopped' command is an exception as we do not process its result
-    # in the CRM and we want to execute it always (even with no active CRM)
+    # do not queue the exact same command twice as this may lead to an inconsistent HA state when
+    # the first command fails but the CRM does not process its failure right away and the LRM starts
+    # a second try, without the CRM knowing of it (race condition) The 'stopped' command is an
+    # exception as we do not process its result in the CRM and we want to execute it always (even
+    # with no active CRM)
     return if $state ne 'stopped' && $uid && defined($self->{results}->{$uid});
 
     if (my $w = $self->{workers}->{$sid}) {
@@ -559,6 +672,7 @@ sub queue_resource_command {
        sid => $sid,
        uid => $uid,
        state => $state,
+       start_tries => 0,
     };
 
     $self->{workers}->{$sid}->{params} = $params if $params;
@@ -571,18 +685,17 @@ sub check_active_workers {
     my $count = 0;
     foreach my $sid (keys %{$self->{workers}}) {
        my $w = $self->{workers}->{$sid};
-       if (my $pid = $w->{pid}) {
-           # check status
-           my $waitpid = waitpid($pid, WNOHANG);
-           if (defined($waitpid) && ($waitpid == $pid)) {
-               if (defined($w->{uid})) {
-                   $self->resource_command_finished($sid, $w->{uid}, $?);
-               } else {
-                   $self->stop_command_finished($sid, $?);
-               }
+       my $pid = $w->{pid} || next;
+
+       my $waitpid = waitpid($pid, WNOHANG); # check status
+       if (defined($waitpid) && ($waitpid == $pid)) {
+           if (defined($w->{uid})) {
+               $self->resource_command_finished($sid, $w->{uid}, $?);
            } else {
-               $count++;
+               $self->stop_command_finished($sid, $?);
            }
+       } else {
+           $count++; # still active
        }
     }
 
@@ -733,7 +846,6 @@ sub exec_resource_agent {
 
     # process error state early
     if ($cmd eq 'error') {
-
        $haenv->log('err', "service $sid is in an error state and needs manual " .
                    "intervention. Look up 'ERROR RECOVERY' in the documentation.");
 
@@ -771,9 +883,13 @@ sub exec_resource_agent {
 
        return SUCCESS if !$running;
 
-       $haenv->log("info", "stopping service $sid");
+       if (defined($params->{timeout})) {
+           $haenv->log("info", "stopping service $sid (timeout=$params->{timeout})");
+       } else {
+           $haenv->log("info", "stopping service $sid");
+       }
 
-       $plugin->shutdown($haenv, $id);
+       $plugin->shutdown($haenv, $id, $params->{timeout});
 
        $running = $plugin->check_running($haenv, $id);