Skip to the content.

GSoC 2026: Event-Driven Automation for Uyuni via MQTT and Node-RED

Google Summer of Code 2026 · openSUSE Project

Contributor: Geetansh Goyal (@geetxnshgoyal) Mentors: Ondřej Holeček, Abid Mehmood Organisation: openSUSE · Uyuni


Deliverables

What Link Status
RFC, the design and topic scheme uyuni-rfc#119 Open, under review
Implementation, publisher plus containers plus Node-RED nodes uyuni#12385 Open, under review

Those two pull requests are the complete record of the work. The code PR has 14 commits, +5,185 lines across 69 files, touching the Java core, two container images and a Node-RED package.


Current state, and what is merged upstream

Nothing is merged yet. Both pull requests are open and under active review at the time of submission. I am stating that plainly because it is the honest position: the code is complete and working, but it has not landed in uyuni-project/uyuni master.

   
Code PR #12385 Open, 14 commits, +5,185 / 69 files. All 16 review comments addressed.
RFC PR #119 Open, reviewed, design agreed with mentors.
Merged to master None yet

The work is functional and has been verified running on a real Uyuni server, as described below. Review is ongoing because the feature touches the reactor and adds a new dependency, so it needs sign-off beyond my mentors.

Anyone wanting to continue this can check out the branch geetxnshgoyal:mqtt-event-publisher and pick up from the open items listed further down.


The problem I set out to solve

Uyuni knows a lot about the machines it manages. It knows the moment a system registers, when a Salt job comes back, when states get applied, when an image is deployed. The catch is that none of it ever left the server.

If you wanted to automate on any of that, you had one option: poll the XML-RPC API on a timer and diff the results. That is wasteful at scale, it adds however much latency your polling interval happens to be, and it quietly misses anything that starts and finishes between two polls.

I wanted Uyuni to be able to say “this just happened” out loud, in a format other tools could understand without knowing anything about how Uyuni works inside.


What I built

An MQTT event publisher inside Uyuni’s Java core

The new com.suse.manager.reactor.mqtt package publishes nine event types asynchronously over Eclipse Paho.

Five come from the Salt reactor, hooked in as a MessageAction next to the handlers that were already there:

systems/registered · jobs/returned · states/applied · images/deployed · batches/started

The other four are published straight from the domain code:

orgs/created · users/created · clm/build-started · clm/build-completed

A few decisions worth calling out:

Topics carry the server’s FQDN, so they look like uyuni/<fqdn>/<category>/<event>. Several Uyuni servers can then share one broker, and a subscriber can still narrow down to one of them with uyuni/+/....

Every message uses the same envelope: an eventId, a UTC timestamp, the topic, and a data payload. Whatever the event is, a consumer parses one shape.

Adding an event type means adding a class. The MqttEvent interface owns both the topic suffix and the payload, so nobody has to extend a switch statement to add the tenth event.

Nothing publishes until the transaction commits. An event announcing a change that then gets rolled back is worse than no event at all.

It is off unless you turn it on. uyuni.mqtt.enabled defaults to false, so a server that does not want MQTT opens no connection and starts no threads.

The queue is bounded. If the broker goes away, the publisher drops the oldest queued message rather than growing until something falls over.

How it fits together

flowchart LR
    subgraph uyuni["Uyuni Server"]
        salt["Salt Reactor"] --> mq["MessageQueue"]
        mq --> action["MqttEventAction"]
        domain["OrgFactory<br/>UserFactory<br/>ContentManager"] --> helper["MqttEventHelper"]
        action --> helper
        helper --> pub["MqttPublisherService<br/><i>async, bounded queue</i>"]
    end

    pub -->|"MQTT / QoS 1"| broker["Mosquitto<br/><i>ACL: publisher write only</i>"]

    broker --> nr["Node-RED<br/><i>uyuni-events</i>"]
    broker --> other["Any MQTT client"]

    nr -->|"XML-RPC"| uyuni

    style pub fill:#2d7d46,color:#fff
    style broker fill:#1f6feb,color:#fff
    style nr fill:#8250df,color:#fff

The dashed path back from Node-RED matters: uyuni-action and uyuni-query call Uyuni’s XML-RPC API, so a flow can both react to an event and act on the result.

Topic hierarchy

flowchart TD
    root["uyuni/"] --> fqdn["&lt;server-fqdn&gt;/"]
    fqdn --> systems["systems/"] --> reg["registered"]
    fqdn --> jobs["jobs/"] --> ret["returned"]
    fqdn --> states["states/"] --> app["applied"]
    fqdn --> images["images/"] --> dep["deployed"]
    fqdn --> batches["batches/"] --> bs["started"]
    fqdn --> orgs["orgs/"] --> oc["created"]
    fqdn --> users["users/"] --> uc["created"]
    fqdn --> clm["clm/"] --> cb["build-started<br/>build-completed"]

    style fqdn fill:#1f6feb,color:#fff

Putting the FQDN in the second segment is what lets several Uyuni servers share one broker. A subscriber wanting every server uses uyuni/+/jobs/returned; one wanting a single server names it directly.

A Node-RED node package

node-red-contrib-uyuni gives administrators four nodes, so building automation is a matter of dragging boxes around rather than writing a consumer from scratch.

Node What it does
uyuni-events Subscribes to Uyuni events and unwraps the envelope onto msg
uyuni-config Shared broker connection and credentials
uyuni-action Acts back on Uyuni: apply highstate, reboot, install packages
uyuni-query Reads from Uyuni: systems, errata, packages, channels

There are 59 unit tests at 94.65% statement coverage, using node-red-node-test-helper with the XML-RPC layer stubbed, so the suite runs without a Uyuni server anywhere nearby.

Two container images

containers/mqtt-broker-image is a Mosquitto broker set up the way Uyuni wants it: anonymous access off, and an ACL giving uyuni-publisher write-only and uyuni-subscriber read-only access to uyuni/#.

containers/node-red-image is Node-RED with the Uyuni nodes already installed. It refuses to start unless you supply a bcrypt admin password hash or explicitly disable the editor. That is deliberate: the Node-RED admin API can deploy flows, and a flow can run arbitrary code, so an unprotected editor is remote code execution on the host.

Documentation

A deployment guide for the whole stack, an architecture README for the Java side, and a reference for every uyuni.mqtt.* property.


What I verified

I did not want to hand in something that only worked on my laptop, so I deployed it onto a real Uyuni server.

Latency came out at 0.112 seconds, from a Salt job finishing to the event arriving at a subscriber.

Real events from a live Uyuni 2026.08 instance. Creating a user produced:

uyuni/server.local/users/created
{"eventId":"cd7a0eda-c4ba-4f5e-b0ad-3b62f9592cd1",
 "data":{"userId":4,"orgId":1,"username":"gdgd"},
 "timestamp":"2026-08-18T10:33:14.846Z"}

Node-RED picked those events up through the uyuni-events node and drove an action off the back of them.

Deploying properly also turned up three bugs that code review had not:

  1. The FQDN lookup was returning the container’s internal hostname, which would have made every containerised Uyuni publish under the same topic prefix. The whole reason for putting the FQDN in the topic would have been lost.
  2. The branch needed a database schema newer than the released container image. The mismatch failed a route validator, which failed the entire web application, which put Tomcat into a restart loop.
  3. Moving the connection log lines from warn to info made them completely invisible, because the default root logger is set to warn.

Not one of those was visible in a diff.


What is still left

Writing this down plainly, because whoever picks it up next deserves to know.

Review is still in progress. Both PRs are open. Every one of the 16 review comments so far has been addressed.

The broker container will not build from SLE_BCI yet, because bci-base has no mosquitto package. Getting the OBS repository configured is being handled on the openSUSE side.

The Node-RED image build inside OBS is an open question. It builds fine locally, but satisfying the npm dependency fetch in an offline build environment has not been worked out.

No Cucumber acceptance tests. The Java unit tests and the Node-RED suite cover the pieces, but there is nothing end to end in Uyuni’s acceptance suite.

Where the Node-RED package should be published is undecided: npm, an openSUSE package, or both.

TLS to the broker. Username and password auth is done. Client certificates were deliberately left for later and are noted in the RFC.


What I took away from it

The lesson that stuck was that being correct often comes down to ordering rather than logic. Abid pointed out in review that my events were going out before the database commit that produced them. My first fix, deferring publication until the transaction completed, looked right and was not enough, because Uyuni gives every handler its own transaction. The real fix was where the action got registered: it had to come after the handlers that actually write the row.

The other lesson was that passing tests and working software are not the same thing. Everything was green before those three deployment bugs turned up in the first couple of hours on a real server.


AI assistance disclosure

Yes, I used AI coding assistance during this project. In my GSoC final evaluation I recorded it under three categories: text generation, debugging and error detection, and issue triage or pull request review. This section describes what that meant in practice.

Debugging and error detection. This is where it earned its place. Deploying onto a real server produced failures with no obvious cause: Tomcat restarting in a loop every 82 seconds, a container silently discarding files I had copied into it, podman failing to resolve a hostname that curl resolved fine on the same machine. Working through those with an assistant was faster than working through them alone. In each case the diagnosis came from evidence we gathered together, not from a guess it produced.

Text generation. Documentation, commit messages, the pull request description, and this report. I wrote the deployment guide and the Java architecture README with assistance and then edited them, because a document that describes behaviour I have not verified is worse than no document.

Issue triage and pull request review. Drafting replies to the sixteen review comments on the pull request, and working through what each one was actually asking for before answering it.

Alongside those three, the assistant helped me read and modify code: finding where things live in a codebase far larger than anything I had touched before, and refactoring within it. I did not hand it a specification and paste back the result. Every change went through review by my mentors, and I understand the pull request well enough to defend it, which review has repeatedly required.

Where it was wrong, and what that taught me. When Abid’s review pointed out that events were being published before the database commit, the fix I arrived at with AI assistance, deferring publication until the transaction completed, looked right and was not enough. Uyuni gives each handler its own transaction, so deferring to “the” transaction deferred to the wrong one. Finding that out required reading ActionExecutor myself and understanding how the dispatch actually works. The assistant had been confidently plausible.

That happened often enough to become the lesson: treat the output as a hypothesis to check, not an answer to paste. It was reliably useful for “where is this handled” and “why is this failing”, and reliably risky on anything involving transactions, ordering, or how the software behaves once deployed.

What was not AI. The design in the RFC came out of discussion with my mentors. The decision to deploy on real infrastructure rather than trust the test suite was mine, and it is what surfaced the three bugs listed above.


Thanks

To Ondřej and Abid, for reviews that went after real problems instead of stopping at style, and for being patient with someone working in a codebase this size for the first time. And to openSUSE generally, for making a first-year student feel like a contributor rather than a visitor.