Add read-only PostgreSQL promotion database preflight
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "postgresql",
|
||||
"version": 3,
|
||||
"version": 4,
|
||||
"module": "plugin.py",
|
||||
"actions": [
|
||||
"monitor",
|
||||
"notify",
|
||||
"notify.target",
|
||||
"promote.database",
|
||||
"demote.prepare",
|
||||
"demote.database"
|
||||
],
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user