The Waldur Site Agent uses a pluggable backend system that allows external developers to create custom backend plugins without modifying the core codebase.
---
config:
layout: elk
---
graph TB
subgraph "Core Package"
WA[waldur-site-agent<br/>Core Logic & Processing]
BB[BaseBackend<br/>Abstract Interface]
BC[BaseClient<br/>Client Interface]
UB[AbstractUsernameManagementBackend<br/>Abstract Interface]
CU[Common Utils<br/>Entry Point Discovery]
end
subgraph "Plugin Ecosystem"
PLUGINS[Backend Plugins<br/>SLURM, MOAB, MUP, etc.]
UMANAGE[Username Management<br/>Plugins]
end
subgraph "Entry Point System"
EP_BACKENDS[waldur_site_agent.backends]
EP_USERNAME[waldur_site_agent.username_management_backends]
EP_SCHEMAS[waldur_site_agent.component_schemas<br/>waldur_site_agent.backend_settings_schemas]
end
%% Core dependencies
WA --> BB
WA --> BC
WA --> UB
WA --> CU
%% Plugin registration and discovery
CU --> EP_BACKENDS
CU --> EP_USERNAME
CU --> EP_SCHEMAS
EP_BACKENDS -.-> PLUGINS
EP_USERNAME -.-> UMANAGE
EP_SCHEMAS -.-> PLUGINS
%% Plugin inheritance
PLUGINS -.-> BB
PLUGINS -.-> BC
UMANAGE -.-> UB
%% Styling - Dark mode compatible colors
classDef corePackage fill:#1E3A8A,stroke:#3B82F6,stroke-width:2px,color:#FFFFFF
classDef plugin fill:#581C87,stroke:#8B5CF6,stroke-width:2px,color:#FFFFFF
classDef entrypoint fill:#065F46,stroke:#10B981,stroke-width:2px,color:#FFFFFF
class WA,BB,BC,UB,CU corePackage
class PLUGINS,UMANAGE plugin
class EP_BACKENDS,EP_USERNAME,EP_SCHEMAS entrypoint
---
config:
layout: elk
---
graph TB
subgraph "Agent Modes"
ORDER[agent-order-process<br/>Order Processing]
REPORT[agent-report<br/>Usage Reporting]
SYNC[agent-membership-sync<br/>Membership Sync]
EVENT[agent-event-process<br/>Event Processing]
end
subgraph "Plugin Layer"
PLUGINS[Backend Plugins<br/>SLURM, MOAB, MUP, etc.]
end
subgraph "External Systems"
WALDUR[Waldur Mastermind<br/>REST API]
BACKENDS[Cluster Backends<br/>CLI/API Systems]
STOMP[STOMP Broker<br/>Event Processing]
end
%% Agent mode usage of plugins
ORDER --> PLUGINS
REPORT --> PLUGINS
SYNC --> PLUGINS
EVENT --> PLUGINS
%% External connections
ORDER <--> WALDUR
REPORT <--> WALDUR
SYNC <--> WALDUR
EVENT <--> WALDUR
EVENT <--> STOMP
PLUGINS <--> BACKENDS
%% Styling - Dark mode compatible colors
classDef agent fill:#B45309,stroke:#F59E0B,stroke-width:2px,color:#FFFFFF
classDef plugin fill:#581C87,stroke:#8B5CF6,stroke-width:2px,color:#FFFFFF
classDef external fill:#C2410C,stroke:#F97316,stroke-width:2px,color:#FFFFFF
class ORDER,REPORT,SYNC,EVENT agent
class PLUGINS plugin
class WALDUR,BACKENDS,STOMP external
The event_process mode uses WebSocket STOMP connections to receive real-time events from Waldur Mastermind
via RabbitMQ. The main loop combines event-driven processing with periodic reconciliation to ensure data
consistency even when STOMP messages are missed.
---
config:
layout: elk
---
graph TB
subgraph "Startup"
INIT[Run Initial<br/>Offering Processing]
REG[Register Agent Identity<br/>& Unified Event Queue]
STOMP_CONN[Connect one WebSocket STOMP<br/>per offering]
end
subgraph "Main Loop (1-min tick)"
TICK[Wake Up]
WD[Watchdog: reconnect dropped consumers,<br/>touch liveness heartbeat while healthy]
HC_CHECK{Health check<br/>every 30 min?}
HC[Send Health Checks<br/>for offerings with order processing]
RC_CHECK{Reconciliation<br/>interval elapsed?<br/>default: 60 min}
RC[Reconcile orders, API keys,<br/>offering users, project hierarchy<br/>+ usernames if enabled]
SLEEP[Sleep 60s]
end
subgraph "STOMP Event Handlers"
ORDER_H[Order & API Key<br/>Handlers]
MEMBER_H[Membership Handlers<br/>roles, resources, accounts,<br/>forced resource sync]
OU_H[OfferingUser Handler<br/>sync usernames]
IMPORT_H[Resource Import<br/>Handler]
LIMITS_H[Periodic Limits<br/>Handler]
end
subgraph "External Systems"
WALDUR[Waldur Mastermind<br/>REST API]
RMQ[RabbitMQ<br/>WebSocket STOMP]
BACKEND[Backend System<br/>SLURM / Waldur B / etc.]
end
%% Startup flow
INIT --> REG --> STOMP_CONN
%% Main loop
STOMP_CONN --> TICK
TICK --> WD --> HC_CHECK
HC_CHECK -->|Yes| HC --> RC_CHECK
HC_CHECK -->|No| RC_CHECK
RC_CHECK -->|Yes| RC --> SLEEP
RC_CHECK -->|No| SLEEP
SLEEP --> TICK
%% STOMP event handlers
RMQ -->|events| ORDER_H
RMQ -->|events| MEMBER_H
RMQ -->|events| OU_H
RMQ -->|events| IMPORT_H
RMQ -->|events| LIMITS_H
%% External connections
ORDER_H --> WALDUR
MEMBER_H --> WALDUR
OU_H --> WALDUR
HC --> WALDUR
RC --> WALDUR
RC --> BACKEND
ORDER_H --> BACKEND
MEMBER_H --> BACKEND
STOMP_CONN --> RMQ
WD --> RMQ
%% Styling - Dark mode compatible colors
classDef startup fill:#1E3A8A,stroke:#3B82F6,stroke-width:2px,color:#FFFFFF
classDef loop fill:#B45309,stroke:#F59E0B,stroke-width:2px,color:#FFFFFF
classDef handler fill:#581C87,stroke:#8B5CF6,stroke-width:2px,color:#FFFFFF
classDef external fill:#C2410C,stroke:#F97316,stroke-width:2px,color:#FFFFFF
classDef decision fill:#065F46,stroke:#10B981,stroke-width:2px,color:#FFFFFF
class INIT,REG,STOMP_CONN startup
class TICK,WD,HC,RC,SLEEP loop
class ORDER_H,MEMBER_H,OU_H,IMPORT_H,LIMITS_H handler
class WALDUR,RMQ,BACKEND external
class HC_CHECK,RC_CHECK decision
Each STOMP-enabled offering registers one consumer queue (consumer_<uuid>) for its agent
identity and opens one WebSocket STOMP connection to it. Every event type the offering subscribes
to arrives on that queue; the payload’s object_type picks the handler.
Event-driven processing can miss updates — a dropped connection loses messages that were in flight.
The main loop therefore runs a reconciliation pass on its first tick and then every
WALDUR_SITE_AGENT_RECONCILIATION_PERIOD_MINUTES (default 60). It covers every offering in the
configuration, STOMP-enabled or not:
| Step | Runs for offerings with | What it does |
| —- | ———————– | ———— |
| Order reconciliation | order_processing_backend | Re-processes orders stuck in executing or pending-provider for 30+ minutes |
| API key reconciliation | order_processing_backend and a backend that supports resource API keys | Re-issues API key commands whose reply never reached Waldur |
| Offering user reconciliation | membership_sync_backend | Retries username generation for offering users stuck in a pre-OK state; then runs the username backend’s own reconcile and deletion sweep (that part also runs without a membership backend) |
| Project hierarchy sync | membership_sync_backend | Checks and corrects the backend account hierarchy |
| Username reconciliation | username_reconciliation_enabled: true | Pulls backend-assigned usernames back into Waldur |
Every step is idempotent: on an offering whose state is already consistent it changes nothing. Health checks are sent on their own fixed 30-minute timer.
A resource’s paused / downscaled change reaches an event-mode agent only as one message,
published when the flag changes. Every
WALDUR_SITE_AGENT_RESOURCE_STATUS_RECONCILIATION_MINUTES (default 60; 0 disables it) the
main loop re-applies the current status of every resource of each STOMP-enabled offering with a
membership sync backend — pause, downscale, or restore — so a lost or failed message is caught
within one interval. The first pass runs one interval after startup (initial processing has just
applied every status). Each resource is re-read right before its status is applied, handled on
its own, and a failure is only logged; this pass never marks a resource ERRED. Offerings with
stomp_membership_sync_enabled: false are skipped — the polling agent owns their status.
Handlers do not run on stomp.py’s receiver thread. The listener only queues each message; one
worker thread per queue runs the handlers in arrival order, so the receiver keeps reading frames
and heartbeats however long a handler takes, and per-queue ordering is kept. Handling is
still serial per queue: every object type of an offering shares one unified queue, so a slow
handler (an order, say) holds up the messages behind it, including other object types. A
handler running longer than WALDUR_SITE_AGENT_STOMP_HANDLER_STUCK_AFTER_MINUTES (default 30)
counts as the queue being down for the liveness watchdog below.
The queue is subscribed with ack="client-individual" and prefetch-count: 1. The worker acks
a message after its handler returns — also when the handler raised, after logging the error, so
a failing message is not retried. A message the broker delivered but the agent has not acked is
requeued when the connection drops and redelivered after the reconnect. Messages queued from a
dropped connection that the worker has not started are skipped and left to that redelivery, and
no ack is sent with an id from an old connection. A message whose handler was running when the
connection dropped can therefore be handled twice; handlers are idempotent (they re-read the
order or resource state from Waldur).
No STOMP connect is unbounded outside a listener’s own reconnect loop: at startup each offering gets three attempts (each limited to 30 s for the WebSocket handshake and the broker’s CONNECTED frame), and a queue that registered but could not connect is kept, so the main loop always starts and later only reconnects it.
Each listener reconnects on its own after a disconnect, but gives up after ten attempts
(roughly ten minutes of exponential backoff). On every tick (60 s) the main loop’s watchdog
makes one bounded attempt for any consumer still disconnected, and retries each missing
consumer of a STOMP-enabled offering separately — the offering’s own queue and, for
federation, the target subscription — with backoff up to 15 minutes. A consumer only counts
as recovered after two consecutive ticks connected, so a queue the broker closes right after
connecting does not look healthy. When the broker reports the queue is gone (NOT_FOUND),
the listener registers it again before reconnecting.
While everything is connected the loop touches the liveness heartbeat as usual. Once the
offering’s own consumer, or a transiently failing setup, has stayed down longer than
WALDUR_SITE_AGENT_STOMP_UNHEALTHY_AFTER_MINUTES (default 15, above the listener’s own retry
window), the loop stops touching the heartbeat and an orchestrator restarts the agent. The
probe fails only once the heartbeat is older than its own maximum age
(waldur_site_healthz --max-age, default 300 s), so the time from the outage to a failed
probe is roughly the threshold plus five minutes, plus the probe’s period × failure
threshold before the restart. The watchdog keeps retrying meanwhile; the heartbeat resumes
the tick the connection comes back.
What a restart cannot fix only logs at ERROR and never withholds the heartbeat: a federation target’s consumer (another Waldur’s broker), and a setup refused with a 4xx other than 408/429 (for example a queue held by another user, 409), which is retried every 15 minutes.
Which object types an offering’s queue receives depends on its configuration. “Membership events
on” below means membership_sync_backend is set and stomp_membership_sync_enabled is not
false.
| Object type | Subscribed when |
| ———– | ————— |
| order, resource_api_key_rotation | order_processing_backend is set |
| user_role, resource, service_account, course_account, offering_user, offering_resources_sync | membership events on |
| offering_user only | no membership backend, stomp_membership_sync_enabled not false, and the username backend has reconcile hooks (LDAP, for example) |
| service_provider_project_group | stomp_membership_sync_enabled not false and the username backend writes project groups |
| importable_resources | resource_import_enabled: true |
| resource_periodic_limits | backend_settings.periodic_limits.enabled: true |
Leaving membership_sync_backend out of an event_process offering therefore turns membership sync
off for it; it does not fall back to polling. To poll membership while orders come over STOMP, set
stomp_membership_sync_enabled: false and run a membership_sync agent.
BaseBackenduv workspace dependenciesplugins/{backend_name}/
├── pyproject.toml # Entry point registration
├── waldur_site_agent_{name}/ # Plugin implementation
│ ├── backend.py # Backend class inheriting BaseBackend
│ ├── client.py # Client for external system communication
│ └── parser.py # Data parsing utilities (optional)
└── tests/ # Plugin-specific tests
The full list of plugin packages is the plugin table in the README. The sections below describe a selection.
waldur-site-agent-slurm)sacctmgr / sacct / scancel commands, or the slurmrestd REST API
(execution_mode: rest)SlurmClient with command-line executionwaldur-site-agent-moab)mam-* commandsMoabClient with MOAB Accounting Manager integrationwaldur-site-agent-mup)MUPClient with HTTP authentication and comprehensive API coveragewaldur-site-agent-waldur)backend_id mappingWaldurClient with waldur_api_client (httpx-based)order_process) and event-driven (event_process) modeswaldur-site-agent-basic-username-management)waldur-site-agent-rancher)keycloak-client shared packageRancherClient (httpx-based) talking to one cluster per
offering (cluster_id is offering-level config)waldur-site-agent-rancher-kc-crd)ManagedRancherProject
CRs that the rancher-keycloak-operator
reconcilesbackend_id = Rancher cluster ID. The plugin reads cluster_id
per-Resource at CR-build timeCreating → OK on operator phase=Ready,
→ Erred on phase=Error) so the homeport UI reflects the
downstream reconcile statekubernetes Python wrapper for apply / get /
list / delete of CRs in one namespace0.3.1+ (recommended)For comprehensive plugin development instructions, including:
BaseBackend and BaseClient method referencesunit_factor) explainedA ready-to-use plugin template is available at docs/plugin-template/.
waldur_site_agent.common.utils builds the backend registries from entry points when it is
imported:
BACKENDS: dict[str, tuple[type[BaseBackend], str, str]] = {
entry_point.name: (
entry_point.load(), # backend class
entry_point.dist.name if entry_point.dist else entry_point.name, # distribution
version(entry_point.dist.name) if entry_point.dist else "unknown", # its version
)
for entry_point in entry_points(group="waldur_site_agent.backends")
}
USERNAME_BACKENDS is built the same way from waldur_site_agent.username_management_backends.
The settings and component schemas are discovered from their own groups when a configuration file
is loaded.
This means:
common.utils imports every installed backend plugin. A plugin
module that imports common.utils at module level therefore creates an import cycle — import it
inside the function that needs it.Plugins integrate through offering configuration:
offerings:
- name: "Example Offering"
backend_type: "slurm" # Selects the settings/component schemas
order_processing_backend: "slurm" # Order processing via SLURM
reporting_backend: "custom-api" # A third-party reporting backend
membership_sync_backend: "slurm" # Membership sync via SLURM
username_management_backend: "custom" # A third-party username backend
This allows: