mirror of
https://github.com/outbackdingo/patroni.git
synced 2026-08-25 14:53:37 +00:00
Exclude leader from failover candidates in ctl (#2983)
Exclude actual leader (not the passed leader argument) from the candidates list in the `patronictl failover` prompt. Abort `patronictl failover` execution if candidate specified is the same as the current cluster leader
This commit is contained in:
+19
-17
@@ -1185,7 +1185,7 @@ def reinit(cluster_name: str, group: Optional[int], member_names: List[str], for
|
|||||||
|
|
||||||
|
|
||||||
def _do_failover_or_switchover(action: str, cluster_name: str, group: Optional[int],
|
def _do_failover_or_switchover(action: str, cluster_name: str, group: Optional[int],
|
||||||
leader: Optional[str], candidate: Optional[str],
|
switchover_leader: Optional[str], candidate: Optional[str],
|
||||||
force: bool, scheduled: Optional[str] = None) -> None:
|
force: bool, scheduled: Optional[str] = None) -> None:
|
||||||
"""Perform a failover or a switchover operation in the cluster.
|
"""Perform a failover or a switchover operation in the cluster.
|
||||||
|
|
||||||
@@ -1199,7 +1199,7 @@ def _do_failover_or_switchover(action: str, cluster_name: str, group: Optional[i
|
|||||||
:param cluster_name: name of the Patroni cluster.
|
:param cluster_name: name of the Patroni cluster.
|
||||||
:param group: filter Citus group within we should perform a failover or switchover. If ``None``, user will be
|
:param group: filter Citus group within we should perform a failover or switchover. If ``None``, user will be
|
||||||
prompted for filling it -- unless *force* is ``True``, in which case an exception is raised.
|
prompted for filling it -- unless *force* is ``True``, in which case an exception is raised.
|
||||||
:param leader: name of the current leader member.
|
:param switchover_leader: name of the leader member passed as switchover option.
|
||||||
:param candidate: name of a standby member to be promoted. Nodes that are tagged with ``nofailover`` cannot be used.
|
:param candidate: name of a standby member to be promoted. Nodes that are tagged with ``nofailover`` cannot be used.
|
||||||
:param force: perform the failover or switchover without asking for confirmations.
|
:param force: perform the failover or switchover without asking for confirmations.
|
||||||
:param scheduled: timestamp when the switchover should be scheduled to occur. If ``now`` perform immediately.
|
:param scheduled: timestamp when the switchover should be scheduled to occur. If ``now`` perform immediately.
|
||||||
@@ -1208,10 +1208,11 @@ def _do_failover_or_switchover(action: str, cluster_name: str, group: Optional[i
|
|||||||
:class:`PatroniCtlException`: if:
|
:class:`PatroniCtlException`: if:
|
||||||
* Patroni is running on a Citus cluster, but no *group* was specified; or
|
* Patroni is running on a Citus cluster, but no *group* was specified; or
|
||||||
* a switchover was requested by the cluster has no leader; or
|
* a switchover was requested by the cluster has no leader; or
|
||||||
* *leader* does not match the current leader of the cluster; or
|
* *switchover_leader* does not match the current leader of the cluster; or
|
||||||
* cluster has no candidates available for the operation; or
|
* cluster has no candidates available for the operation; or
|
||||||
* no *candidate* is given for a failover operation; or
|
* no *candidate* is given for a failover operation; or
|
||||||
* *leader* and *candidate* are the same; or
|
* current leader and *candidate* are the same; or
|
||||||
|
* *candidate* is tagged as nofailover; or
|
||||||
* *candidate* is not a member of the cluster; or
|
* *candidate* is not a member of the cluster; or
|
||||||
* trying to schedule a switchover in a cluster that is in maintenance mode; or
|
* trying to schedule a switchover in a cluster that is in maintenance mode; or
|
||||||
* user aborts the operation.
|
* user aborts the operation.
|
||||||
@@ -1231,23 +1232,24 @@ def _do_failover_or_switchover(action: str, cluster_name: str, group: Optional[i
|
|||||||
|
|
||||||
config = global_config.from_cluster(cluster)
|
config = global_config.from_cluster(cluster)
|
||||||
|
|
||||||
|
cluster_leader = cluster.leader and cluster.leader.name
|
||||||
# leader has to be be defined for switchover only
|
# leader has to be be defined for switchover only
|
||||||
if action == 'switchover':
|
if action == 'switchover':
|
||||||
if cluster.leader is None or not cluster.leader.name:
|
if not cluster_leader:
|
||||||
raise PatroniCtlException('This cluster has no leader')
|
raise PatroniCtlException('This cluster has no leader')
|
||||||
|
|
||||||
if leader is None:
|
if switchover_leader is None:
|
||||||
if force:
|
if force:
|
||||||
leader = cluster.leader.name
|
switchover_leader = cluster_leader
|
||||||
else:
|
else:
|
||||||
prompt = 'Standby Leader' if config.is_standby_cluster else 'Primary'
|
prompt = 'Standby Leader' if config.is_standby_cluster else 'Primary'
|
||||||
leader = click.prompt(prompt, type=str, default=(cluster.leader and cluster.leader.name))
|
switchover_leader = click.prompt(prompt, type=str, default=cluster_leader)
|
||||||
|
|
||||||
if cluster.leader.name != leader:
|
if cluster_leader != switchover_leader:
|
||||||
raise PatroniCtlException(f'Member {leader} is not the leader of cluster {cluster_name}')
|
raise PatroniCtlException(f'Member {switchover_leader} is not the leader of cluster {cluster_name}')
|
||||||
|
|
||||||
# excluding members with nofailover tag
|
# excluding members with nofailover tag
|
||||||
candidate_names = [str(m.name) for m in cluster.members if m.name != leader and not m.nofailover]
|
candidate_names = [str(m.name) for m in cluster.members if m.name != cluster_leader and not m.nofailover]
|
||||||
# We sort the names for consistent output to the client
|
# We sort the names for consistent output to the client
|
||||||
candidate_names.sort()
|
candidate_names.sort()
|
||||||
|
|
||||||
@@ -1260,10 +1262,10 @@ def _do_failover_or_switchover(action: str, cluster_name: str, group: Optional[i
|
|||||||
if action == 'failover' and not candidate:
|
if action == 'failover' and not candidate:
|
||||||
raise PatroniCtlException('Failover could be performed only to a specific candidate')
|
raise PatroniCtlException('Failover could be performed only to a specific candidate')
|
||||||
|
|
||||||
if candidate == leader:
|
|
||||||
raise PatroniCtlException(action.title() + ' target and source are the same.')
|
|
||||||
|
|
||||||
if candidate and candidate not in candidate_names:
|
if candidate and candidate not in candidate_names:
|
||||||
|
if candidate == cluster_leader:
|
||||||
|
raise PatroniCtlException(
|
||||||
|
f'Member {candidate} is already the leader of cluster {cluster_name}')
|
||||||
raise PatroniCtlException(
|
raise PatroniCtlException(
|
||||||
f'Member {candidate} does not exist in cluster {cluster_name} or is tagged as nofailover')
|
f'Member {candidate} does not exist in cluster {cluster_name} or is tagged as nofailover')
|
||||||
|
|
||||||
@@ -1292,7 +1294,7 @@ def _do_failover_or_switchover(action: str, cluster_name: str, group: Optional[i
|
|||||||
|
|
||||||
failover_value = {'candidate': candidate}
|
failover_value = {'candidate': candidate}
|
||||||
if action == 'switchover':
|
if action == 'switchover':
|
||||||
failover_value['leader'] = leader
|
failover_value['leader'] = switchover_leader
|
||||||
if scheduled_at_str:
|
if scheduled_at_str:
|
||||||
failover_value['scheduled_at'] = scheduled_at_str
|
failover_value['scheduled_at'] = scheduled_at_str
|
||||||
|
|
||||||
@@ -1300,7 +1302,7 @@ def _do_failover_or_switchover(action: str, cluster_name: str, group: Optional[i
|
|||||||
|
|
||||||
# By now we have established that the leader exists and the candidate exists
|
# By now we have established that the leader exists and the candidate exists
|
||||||
if not force:
|
if not force:
|
||||||
demote_msg = f', demoting current leader {cluster.leader.name}' if cluster.leader else ''
|
demote_msg = f', demoting current leader {cluster_leader}' if cluster_leader else ''
|
||||||
if scheduled_at_str:
|
if scheduled_at_str:
|
||||||
# only switchover can be scheduled
|
# only switchover can be scheduled
|
||||||
if not click.confirm(f'Are you sure you want to schedule switchover of cluster '
|
if not click.confirm(f'Are you sure you want to schedule switchover of cluster '
|
||||||
@@ -1334,7 +1336,7 @@ def _do_failover_or_switchover(action: str, cluster_name: str, group: Optional[i
|
|||||||
logging.exception(r)
|
logging.exception(r)
|
||||||
logging.warning('Failing over to DCS')
|
logging.warning('Failing over to DCS')
|
||||||
click.echo('{0} Could not {1} using Patroni api, falling back to DCS'.format(timestamp(), action))
|
click.echo('{0} Could not {1} using Patroni api, falling back to DCS'.format(timestamp(), action))
|
||||||
dcs.manual_failover(leader, candidate, scheduled_at=scheduled_at)
|
dcs.manual_failover(switchover_leader, candidate, scheduled_at=scheduled_at)
|
||||||
|
|
||||||
output_members(cluster, cluster_name, group=group)
|
output_members(cluster, cluster_name, group=group)
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -157,7 +157,8 @@ class TestCtl(unittest.TestCase):
|
|||||||
# Target and source are equal
|
# Target and source are equal
|
||||||
result = self.runner.invoke(ctl, ['switchover', 'dummy', '--group', '0'], input='leader\nleader\n\ny')
|
result = self.runner.invoke(ctl, ['switchover', 'dummy', '--group', '0'], input='leader\nleader\n\ny')
|
||||||
self.assertEqual(result.exit_code, 1)
|
self.assertEqual(result.exit_code, 1)
|
||||||
self.assertIn('Switchover target and source are the same', result.output)
|
self.assertIn("Candidate ['other']", result.output)
|
||||||
|
self.assertIn('Member leader is already the leader of cluster dummy', result.output)
|
||||||
|
|
||||||
# Candidate is not a member of the cluster
|
# Candidate is not a member of the cluster
|
||||||
result = self.runner.invoke(ctl, ['switchover', 'dummy', '--group', '0'], input='leader\nReality\n\ny')
|
result = self.runner.invoke(ctl, ['switchover', 'dummy', '--group', '0'], input='leader\nReality\n\ny')
|
||||||
@@ -220,6 +221,11 @@ class TestCtl(unittest.TestCase):
|
|||||||
result = self.runner.invoke(ctl, ['failover', 'dummy'], input='0\n')
|
result = self.runner.invoke(ctl, ['failover', 'dummy'], input='0\n')
|
||||||
self.assertIn('Failover could be performed only to a specific candidate', result.output)
|
self.assertIn('Failover could be performed only to a specific candidate', result.output)
|
||||||
|
|
||||||
|
# Candidate is the same as the leader
|
||||||
|
result = self.runner.invoke(ctl, ['failover', 'dummy', '--group', '0'], input='leader\n')
|
||||||
|
self.assertIn("Candidate ['other']", result.output)
|
||||||
|
self.assertIn('Member leader is already the leader of cluster dummy', result.output)
|
||||||
|
|
||||||
# Temp test to check a fallback to switchover if leader is specified
|
# Temp test to check a fallback to switchover if leader is specified
|
||||||
with patch('patroni.ctl._do_failover_or_switchover') as failover_func_mock:
|
with patch('patroni.ctl._do_failover_or_switchover') as failover_func_mock:
|
||||||
result = self.runner.invoke(ctl, ['failover', '--leader', 'leader', 'dummy'], input='0\n')
|
result = self.runner.invoke(ctl, ['failover', '--leader', 'leader', 'dummy'], input='0\n')
|
||||||
|
|||||||
Reference in New Issue
Block a user