From 10df464b00b01a075146dbedd9d2eced81cb5d4b Mon Sep 17 00:00:00 2001 From: Lyncoln Date: Thu, 17 Sep 2026 17:58:11 -0400 Subject: [PATCH] Validate demotion successor against peer map --- plugins/postgresql/plugin.py | 33 +++++++++++++++++++++++++++++++++ tests/test-demote-prepare.py | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/plugins/postgresql/plugin.py b/plugins/postgresql/plugin.py index 2ad372b..8fc44ad 100644 --- a/plugins/postgresql/plugin.py +++ b/plugins/postgresql/plugin.py @@ -625,6 +625,36 @@ def demote_prepare(config, runtime_state, request_parameters): "node": node_name, } + try: + peers = parse_replication_peers( + config.get("PG_REPLICATION_PEERS", "") + ) + except (TypeError, ValueError): + return { + "ocf_rc": 1, + "ready": False, + "reason": "peer_map_invalid", + "node": node_name, + "successor_node": successor_node, + } + + successor_peer = next( + ( + peer for peer in peers + if peer["name"].strip().lower() == successor_node + ), + None, + ) + + if successor_peer is None: + return { + "ocf_rc": 1, + "ready": False, + "reason": "successor_not_configured", + "node": node_name, + "successor_node": successor_node, + } + return { "ocf_rc": 0, "ready": False, @@ -634,8 +664,11 @@ def demote_prepare(config, runtime_state, request_parameters): "node": node_name, "target_node": target_node, "successor_node": successor_node, + "successor_host": successor_peer["host"], + "successor_port": successor_peer["port"], "age_seconds": age, "checks": { + "peer_map": True, "notification": True, "local_target": True, "successor_selected": True, diff --git a/tests/test-demote-prepare.py b/tests/test-demote-prepare.py index e034a38..fbc9475 100755 --- a/tests/test-demote-prepare.py +++ b/tests/test-demote-prepare.py @@ -37,7 +37,13 @@ runtime_state = { } result = plugin.demote_prepare( - {"PACEMAKER_NODE_NAME": "david-eve"}, + { + "PACEMAKER_NODE_NAME": "david-eve", + "PG_REPLICATION_PEERS": ( + "david-prodesk=192.168.1.4:5432," + "david-eve=192.168.1.3:5432" + ), + }, runtime_state, {}, ) @@ -47,6 +53,9 @@ assert result["ready"] is False assert result["identity_validated"] is True assert result["reason"] == "database_checks_pending" assert result["checks"]["database_checks"] is False +assert result["checks"]["peer_map"] is True +assert result["successor_host"] == "192.168.1.4" +assert result["successor_port"] == 5432 assert result["mutation"] is False assert result["successor_node"] == "david-prodesk" print("valid_prepare accepted") @@ -90,3 +99,28 @@ print("missing_successor rejected") print() print("DEMOTE_PREPARE_TEST_PASS=1") + +result = plugin.demote_prepare( + { + "PACEMAKER_NODE_NAME": "david-eve", + "PG_REPLICATION_PEERS": ( + "david-macbookpro=192.168.1.5:5432" + ), + }, + { + "postgresql": { + "notification": { + "type": "pre", + "operation": "demote", + "demote_uname": "david-eve", + "promote_uname": "david-prodesk", + "received_at": time.time(), + }, + }, + }, + {}, +) + +assert result["ocf_rc"] == 1 +assert result["reason"] == "successor_not_configured" +print("unconfigured_successor rejected")