From 64cdc024b0f91b879ea875a82af5098648f97e8e Mon Sep 17 00:00:00 2001 From: Lyncoln Date: Thu, 17 Sep 2026 16:03:03 -0400 Subject: [PATCH] Bind daemon to local Pacemaker node identity --- daemon/server.py | 43 +++++++++++++++++++++++ tests/test-node-identity.py | 70 +++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100755 tests/test-node-identity.py diff --git a/daemon/server.py b/daemon/server.py index 5bb3a14..0ed9c76 100644 --- a/daemon/server.py +++ b/daemon/server.py @@ -20,6 +20,7 @@ registry = None pacemaker_resource_id = None pacemaker_peer_uid = None pacemaker_peer_gid = None +pacemaker_node_name = None PROTOCOL_VERSION = 1 @@ -68,6 +69,38 @@ def load_pacemaker_resource_binding( return resource_id +def load_pacemaker_node_name(environ=None): + """ + Load the Pacemaker node represented by this daemon instance. + + The daemon is local to one PostgreSQL instance. This identity is not + used for routing; it is used to validate promotion and demotion context. + """ + if environ is None: + environ = os.environ + + raw_node_name = environ.get("PACEMAKER_NODE_NAME") + + if raw_node_name is None: + raise RegistryError( + "PACEMAKER_NODE_NAME is not configured" + ) + + node_name = raw_node_name.strip().lower() + + if not node_name: + raise RegistryError( + "PACEMAKER_NODE_NAME is empty" + ) + + if any(char.isspace() for char in node_name): + raise RegistryError( + "PACEMAKER_NODE_NAME must not contain whitespace" + ) + + return node_name + + def load_pacemaker_peer_authorization( environ=None, ): @@ -163,6 +196,7 @@ def handle_request( "pacemaker_resource": ( pacemaker_resource_id ), + "pacemaker_node": pacemaker_node_name, }) if action == "registry.list": @@ -171,6 +205,7 @@ def handle_request( "pacemaker_resource": ( pacemaker_resource_id ), + "pacemaker_node": pacemaker_node_name, "resources": registry.list_resources(), }) @@ -263,6 +298,7 @@ def serve(): global pacemaker_resource_id global pacemaker_peer_uid global pacemaker_peer_gid + global pacemaker_node_name registry = Registry() pacemaker_resource_id = ( @@ -272,6 +308,7 @@ def serve(): pacemaker_peer_uid, pacemaker_peer_gid, ) = load_pacemaker_peer_authorization() + pacemaker_node_name = load_pacemaker_node_name() socket_directory = os.path.dirname( SOCKET_PATH @@ -321,6 +358,12 @@ def serve(): flush=True, ) + print( + "Pacemaker node identity: " + + pacemaker_node_name, + flush=True, + ) + try: while True: conn, _ = server_socket.accept() diff --git a/tests/test-node-identity.py b/tests/test-node-identity.py new file mode 100755 index 0000000..49e4f52 --- /dev/null +++ b/tests/test-node-identity.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 + +import sys + +sys.path.insert(0, "daemon") + +import server +from registry import RegistryError + + +def require(condition, message): + if not condition: + raise SystemExit("FAIL: " + message) + + +def require_error(environment, message): + try: + server.load_pacemaker_node_name(environment) + except RegistryError: + return + + raise SystemExit("FAIL: " + message) + + +print("===== PACEMAKER NODE IDENTITY =====") + +require_error( + {}, + "missing node identity was accepted", +) +print("missing_identity rejected") + +require_error( + {"PACEMAKER_NODE_NAME": " "}, + "empty node identity was accepted", +) +print("empty_identity rejected") + +require_error( + {"PACEMAKER_NODE_NAME": "david prodesk"}, + "whitespace node identity was accepted", +) +print("whitespace_identity rejected") + +node_name = server.load_pacemaker_node_name({ + "PACEMAKER_NODE_NAME": " DAVID-PRODESK ", +}) + +require( + node_name == "david-prodesk", + "node identity was not normalized", +) +print("valid_identity normalized") + +server.pacemaker_node_name = node_name + +ping = server.protocol_response({ + "ok": True, + "daemon": "managed-containerd", + "pacemaker_node": server.pacemaker_node_name, +}) + +require( + ping["pacemaker_node"] == "david-prodesk", + "node identity was not reportable", +) + +print("reported_identity accepted") +print() +print("NODE_IDENTITY_TEST_PASS=1")