Add read-only PostgreSQL promotion database preflight

This commit is contained in:
Lyncoln
2026-09-17 22:48:25 -04:00
parent 5fcd0bf931
commit 1e72dda238
4 changed files with 414 additions and 3 deletions
+185
View File
@@ -531,6 +531,191 @@ def notify_target(config, runtime_state, request_parameters):
}
def promote_database(config, runtime_state, request_parameters):
"""
Read-only PostgreSQL promotion preflight.
Validate that Pacemaker selected this local node for promotion, that the
local PostgreSQL instance is still an eligible standby, and that no
reachable configured peer is currently writable.
This action never promotes PostgreSQL. The OCF resource agent remains
authoritative for the actual pg_ctl promote operation and for local
filesystem state such as the rejoin-required marker.
"""
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 promote.database"
)
notification = _postgres_runtime_state(
runtime_state
).get("notification")
if not isinstance(notification, dict):
return {
"ocf_rc": 1,
"ready": False,
"mutation": False,
"reason": "notification_missing",
"node": node_name,
}
if notification.get("type") != "pre":
return {
"ocf_rc": 1,
"ready": False,
"mutation": False,
"reason": "notification_type_invalid",
"node": node_name,
}
if notification.get("operation") != "promote":
return {
"ocf_rc": 1,
"ready": False,
"mutation": 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,
"mutation": 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,
"mutation": False,
"reason": "notification_stale",
"age_seconds": age,
"node": node_name,
}
target_node = str(
notification.get("promote_uname", "")
).strip().lower()
previous_primary_node = str(
notification.get("demote_uname", "")
).strip().lower()
if not target_node:
return {
"ocf_rc": 1,
"ready": False,
"mutation": False,
"reason": "promotion_target_missing",
"node": node_name,
}
if target_node != node_name:
return {
"ocf_rc": 1,
"ready": False,
"mutation": False,
"reason": "local_node_not_target",
"node": node_name,
"target_node": target_node,
"previous_primary_node": previous_primary_node,
}
local = monitor(config, runtime_state)
local_ha = local.get("ha") or {}
checks = {
"notification": True,
"local_target": True,
"local_monitor_success": (
local_ha.get("ocf_rc") == OCF_SUCCESS
),
"local_standby": (
local.get("role") == "standby"
and local.get("in_recovery") is True
),
"promotion_eligible": (
local_ha.get("promotion_eligible") is True
),
}
if not all(checks.values()):
return {
"ocf_rc": 1,
"ready": False,
"mutation": False,
"reason": "local_not_promotion_eligible",
"node": node_name,
"target_node": target_node,
"previous_primary_node": previous_primary_node,
"local": local_ha,
"checks": checks,
}
try:
password = read_secret(config["PGPASSWORD_FILE"])
peer_state = probe_replication_peers(config, password)
except (KeyError, TypeError, ValueError, psycopg2.Error) as exc:
return {
"ocf_rc": 1,
"ready": False,
"mutation": False,
"reason": "peer_probe_failed",
"node": node_name,
"target_node": target_node,
"previous_primary_node": previous_primary_node,
"error": str(exc).splitlines()[0],
"checks": checks,
}
writable_primaries = [
str(name).strip().lower()
for name in peer_state.get("writable_primaries", [])
]
checks["no_reachable_writable_primary"] = (
len(writable_primaries) == 0
)
ready = all(checks.values())
return {
"ocf_rc": OCF_SUCCESS if ready else OCF_ERR_GENERIC,
"ready": ready,
"mutation": False,
"reason": (
"promotion_preflight_ready"
if ready
else "reachable_writable_primary"
),
"node": node_name,
"target_node": target_node,
"previous_primary_node": previous_primary_node,
"writable_primaries": writable_primaries,
"writable_primary_count": len(writable_primaries),
"peer_state": peer_state,
"checks": checks,
"age_seconds": age,
}
def demote_prepare(config, runtime_state, request_parameters):
"""
Read-only demotion preparation.