Add read-only PostgreSQL demotion preparation
This commit is contained in:
@@ -5,7 +5,8 @@
|
|||||||
"actions": [
|
"actions": [
|
||||||
"monitor",
|
"monitor",
|
||||||
"notify",
|
"notify",
|
||||||
"notify.target"
|
"notify.target",
|
||||||
|
"demote.prepare"
|
||||||
],
|
],
|
||||||
"required_config": [
|
"required_config": [
|
||||||
"PGHOST",
|
"PGHOST",
|
||||||
|
|||||||
@@ -529,3 +529,114 @@ def notify_target(config, runtime_state, request_parameters):
|
|||||||
"node": node_name,
|
"node": node_name,
|
||||||
"age_seconds": age,
|
"age_seconds": age,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def demote_prepare(config, runtime_state, request_parameters):
|
||||||
|
"""
|
||||||
|
Read-only demotion preparation.
|
||||||
|
|
||||||
|
Validates the latest Pacemaker demotion notification against this local
|
||||||
|
daemon and reports the successor. PostgreSQL mutation remains outside this
|
||||||
|
action until candidate and WAL checks are migrated.
|
||||||
|
"""
|
||||||
|
del request_parameters
|
||||||
|
|
||||||
|
node_name = str(
|
||||||
|
config.get("PACEMAKER_NODE_NAME", "")
|
||||||
|
).strip().lower()
|
||||||
|
|
||||||
|
if not node_name:
|
||||||
|
raise ValueError(
|
||||||
|
"PACEMAKER_NODE_NAME is required for demote.prepare"
|
||||||
|
)
|
||||||
|
|
||||||
|
notification = _postgres_runtime_state(
|
||||||
|
runtime_state
|
||||||
|
).get("notification")
|
||||||
|
|
||||||
|
if not isinstance(notification, dict):
|
||||||
|
return {
|
||||||
|
"ocf_rc": 1,
|
||||||
|
"ready": False,
|
||||||
|
"reason": "notification_missing",
|
||||||
|
"node": node_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if notification.get("type") != "pre":
|
||||||
|
return {
|
||||||
|
"ocf_rc": 1,
|
||||||
|
"ready": False,
|
||||||
|
"reason": "notification_type_invalid",
|
||||||
|
"node": node_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if notification.get("operation") != "demote":
|
||||||
|
return {
|
||||||
|
"ocf_rc": 1,
|
||||||
|
"ready": False,
|
||||||
|
"reason": "notification_operation_invalid",
|
||||||
|
"node": node_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
received_at = notification.get("received_at")
|
||||||
|
|
||||||
|
if not isinstance(received_at, (int, float)):
|
||||||
|
return {
|
||||||
|
"ocf_rc": 1,
|
||||||
|
"ready": False,
|
||||||
|
"reason": "notification_timestamp_invalid",
|
||||||
|
"node": node_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
age = time.time() - received_at
|
||||||
|
|
||||||
|
if age < 0 or age > NOTIFY_TARGET_MAX_AGE_SECONDS:
|
||||||
|
return {
|
||||||
|
"ocf_rc": 1,
|
||||||
|
"ready": False,
|
||||||
|
"reason": "notification_stale",
|
||||||
|
"age_seconds": age,
|
||||||
|
"node": node_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
target_node = str(
|
||||||
|
notification.get("demote_uname", "")
|
||||||
|
).strip().lower()
|
||||||
|
|
||||||
|
successor_node = str(
|
||||||
|
notification.get("promote_uname", "")
|
||||||
|
).strip().lower()
|
||||||
|
|
||||||
|
if target_node != node_name:
|
||||||
|
return {
|
||||||
|
"ocf_rc": 1,
|
||||||
|
"ready": False,
|
||||||
|
"reason": "local_node_not_target",
|
||||||
|
"node": node_name,
|
||||||
|
"target_node": target_node,
|
||||||
|
"successor_node": successor_node,
|
||||||
|
}
|
||||||
|
|
||||||
|
if not successor_node:
|
||||||
|
return {
|
||||||
|
"ocf_rc": 1,
|
||||||
|
"ready": False,
|
||||||
|
"reason": "successor_missing",
|
||||||
|
"node": node_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ocf_rc": 0,
|
||||||
|
"ready": True,
|
||||||
|
"mutation": False,
|
||||||
|
"node": node_name,
|
||||||
|
"target_node": target_node,
|
||||||
|
"successor_node": successor_node,
|
||||||
|
"age_seconds": age,
|
||||||
|
"checks": {
|
||||||
|
"notification": True,
|
||||||
|
"local_target": True,
|
||||||
|
"successor_selected": True,
|
||||||
|
"postgresql_mutated": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
Executable
+89
@@ -0,0 +1,89 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import types
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
fake_psycopg2 = types.ModuleType("psycopg2")
|
||||||
|
fake_psycopg2.Error = Exception
|
||||||
|
fake_psycopg2.OperationalError = OSError
|
||||||
|
|
||||||
|
fake_extras = types.ModuleType("psycopg2.extras")
|
||||||
|
fake_extras.RealDictCursor = object
|
||||||
|
fake_psycopg2.extras = fake_extras
|
||||||
|
|
||||||
|
sys.modules["psycopg2"] = fake_psycopg2
|
||||||
|
sys.modules["psycopg2.extras"] = fake_extras
|
||||||
|
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
"postgresql_plugin",
|
||||||
|
Path("plugins/postgresql/plugin.py"),
|
||||||
|
)
|
||||||
|
plugin = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(plugin)
|
||||||
|
|
||||||
|
runtime_state = {
|
||||||
|
"postgresql": {
|
||||||
|
"notification": {
|
||||||
|
"type": "pre",
|
||||||
|
"operation": "demote",
|
||||||
|
"demote_uname": "david-eve",
|
||||||
|
"promote_uname": "david-prodesk",
|
||||||
|
"received_at": time.time(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result = plugin.demote_prepare(
|
||||||
|
{"PACEMAKER_NODE_NAME": "david-eve"},
|
||||||
|
runtime_state,
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["ocf_rc"] == 0
|
||||||
|
assert result["ready"] is True
|
||||||
|
assert result["mutation"] is False
|
||||||
|
assert result["successor_node"] == "david-prodesk"
|
||||||
|
print("valid_prepare accepted")
|
||||||
|
|
||||||
|
result = plugin.demote_prepare(
|
||||||
|
{"PACEMAKER_NODE_NAME": "david-macbookpro"},
|
||||||
|
runtime_state,
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["ocf_rc"] == 1
|
||||||
|
assert result["reason"] == "local_node_not_target"
|
||||||
|
print("wrong_target rejected")
|
||||||
|
|
||||||
|
runtime_state["postgresql"]["notification"]["received_at"] = (
|
||||||
|
time.time() - 121
|
||||||
|
)
|
||||||
|
|
||||||
|
result = plugin.demote_prepare(
|
||||||
|
{"PACEMAKER_NODE_NAME": "david-eve"},
|
||||||
|
runtime_state,
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["ocf_rc"] == 1
|
||||||
|
assert result["reason"] == "notification_stale"
|
||||||
|
print("stale_notification rejected")
|
||||||
|
|
||||||
|
runtime_state["postgresql"]["notification"]["received_at"] = time.time()
|
||||||
|
runtime_state["postgresql"]["notification"]["promote_uname"] = ""
|
||||||
|
|
||||||
|
result = plugin.demote_prepare(
|
||||||
|
{"PACEMAKER_NODE_NAME": "david-eve"},
|
||||||
|
runtime_state,
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["ocf_rc"] == 1
|
||||||
|
assert result["reason"] == "successor_missing"
|
||||||
|
print("missing_successor rejected")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("DEMOTE_PREPARE_TEST_PASS=1")
|
||||||
Reference in New Issue
Block a user