From 6e00e80c5e989ee18ec61e5224e3a4675b0e8a9f Mon Sep 17 00:00:00 2001 From: Lyncoln Date: Thu, 17 Sep 2026 16:14:15 -0400 Subject: [PATCH] Dispatch parameterized daemon actions --- daemon/registry.py | 29 +++++++- daemon/server.py | 23 ++++++ tests/test-parameterized-actions.py | 110 ++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+), 4 deletions(-) create mode 100755 tests/test-parameterized-actions.py diff --git a/daemon/registry.py b/daemon/registry.py index 1141d9b..84f4a56 100644 --- a/daemon/registry.py +++ b/daemon/registry.py @@ -81,7 +81,13 @@ class Plugin: + ", ".join(missing) ) - def execute(self, action, config, runtime_state): + def execute( + self, + action, + config, + runtime_state, + request_parameters=None, + ): if action not in self.actions: raise RegistryError( f"plugin {self.name} does not support action {action}" @@ -98,9 +104,18 @@ class Plugin: # Backward-compatible generic state injection. Existing one-argument # plugins continue to receive only config; state-aware plugins can # declare a second positional parameter named runtime_state. - parameters = list(inspect.signature(handler).parameters.values()) + handler_parameters = list( + inspect.signature(handler).parameters.values() + ) - if len(parameters) >= 2: + if len(handler_parameters) >= 3: + return handler( + config, + runtime_state, + request_parameters or {}, + ) + + if len(handler_parameters) >= 2: return handler(config, runtime_state) return handler(config) @@ -178,7 +193,12 @@ class Registry: in self.resources.items() } - def execute(self, resource_id, action): + def execute( + self, + resource_id, + action, + request_parameters=None, + ): resource = self.resources.get(resource_id) if resource is None: @@ -190,6 +210,7 @@ class Registry: action, resource.config, resource.runtime_state, + request_parameters, ) return { diff --git a/daemon/server.py b/daemon/server.py index 0ed9c76..6cd9f13 100644 --- a/daemon/server.py +++ b/daemon/server.py @@ -209,6 +209,20 @@ def handle_request( "resources": registry.list_resources(), }) + request_parameters = req.get( + "parameters", + {}, + ) + + if not isinstance(request_parameters, dict): + return protocol_response({ + "ok": False, + "error_code": "invalid_request", + "error": "parameters must be a JSON object", + "resource": req.get("resource"), + "action": action, + }) + resource = req.get("resource") if not resource: @@ -259,6 +273,15 @@ def handle_request( "action": action, }) + if request_parameters: + return protocol_response( + registry.execute( + resource, + action, + request_parameters, + ) + ) + return protocol_response( registry.execute( resource, diff --git a/tests/test-parameterized-actions.py b/tests/test-parameterized-actions.py new file mode 100755 index 0000000..cd21bf3 --- /dev/null +++ b/tests/test-parameterized-actions.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 + +import sys + +sys.path.insert(0, "daemon") + +import server + + +class FakeRegistry: + def __init__(self): + self.resources = {"postgresql-ha": object()} + self.executions = [] + + def execute( + self, + resource, + action, + request_parameters=None, + ): + self.executions.append( + (resource, action, request_parameters) + ) + + return { + "ok": True, + "resource": resource, + "plugin": "fake", + "action": action, + "result": { + "parameters": request_parameters or {}, + }, + } + + +def require(condition, message): + if not condition: + raise SystemExit("FAIL: " + message) + + +print("===== PARAMETERIZED ACTIONS =====") + +registry = FakeRegistry() +server.registry = registry +server.pacemaker_resource_id = "postgresql-ha" +server.pacemaker_peer_uid = 65534 +server.pacemaker_peer_gid = 65534 +server.pacemaker_node_name = "david-prodesk" + +peer = { + "pid": 1, + "uid": 65534, + "gid": 65534, +} + +parameters = { + "type": "pre", + "operation": "demote", + "promote_uname": "david-eve", + "demote_uname": "david-prodesk", +} + +response = server.handle_request( + { + "protocol_version": 1, + "resource": "postgresql-ha", + "action": "notify", + "parameters": parameters, + }, + peer, +) + +require(response["ok"] is True, "parameterized action was rejected") +require( + response["result"]["parameters"] == parameters, + "action parameters were not preserved", +) +require( + registry.executions + == [("postgresql-ha", "notify", parameters)], + "parameterized action was not dispatched exactly once", +) +print("parameterized_action dispatched") + +response = server.handle_request( + { + "protocol_version": 1, + "resource": "postgresql-ha", + "action": "notify", + "parameters": [], + }, + peer, +) + +require( + response["ok"] is False, + "non-object parameters were accepted", +) +require( + response["error_code"] == "invalid_request", + "invalid parameters had the wrong classification", +) +require( + len(registry.executions) == 1, + "invalid parameters reached the registry", +) +print("invalid_parameters rejected") + +print() +print("PARAMETERIZED_ACTION_TEST_PASS=1")