Moving community.openwrt from shell to ucode
I manage a small fleet of OpenWrt routers with Ansible: cloud routers, a VPS
and the boxes at home. The tool for that is
community.openwrt,
the successor of the old gekmihesg.openwrt role. Its trick is that the
router needs no Python: the modules used to be shell scripts that the
controller copied over SSH and ran with the router’s own BusyBox.
That works, but it is slow, and speed is what pushed me to rewrite it. In
the shell uci module every option meant a few more forks of uci and
jsonfilter, and my mesh needs a lot of options on every router — one task,
one round of forks each. On top of that, JSON is assembled from shell
strings, quoting is a minefield, and the modules are hard to test.
Why ucode
OpenWrt already has a better runtime on every device:
ucode, the small JavaScript-like language
firewall4 is written in. It has native JSON and ships bindings
for fs, uci and ubus, so a module can read and write configuration
directly instead of shelling out for every value.
What happened
- I started with a ucode UCI module plus a shared helper library (#264). It was closed in favour of smaller, focused pull requests.
- The conversion of the
ucimodule itself landed as #280. The main feature isoperations: a whole batch of changes — delete stale sections, upsert new ones — applied in order in a single call, instead of one task per option. It also addsredact_keys, to keep secrets out of the diff output. - Release 1.9.0 is the first release of the collection based on ucode instead
of shell scripts: 17 modules, from
apkandcopytoserviceanduci.
Next: WireGuard and AmneziaWG
My homelab mesh runs on WireGuard and AmneziaWG tunnels, and until now the
wiring lived in a big Ansible role. The new wg_interface and wg_peer
modules (#281,
in review) take over the device-side logic: key generation, shared PSKs,
peers, and cleanup of interfaces that are no longer wanted. I already run them
from my fork, with Molecule scenarios on real router firmware in VMs as
integration tests.
A few ucode gotchas
- A module gets its arguments as a JSON file path in
ARGV[0]; keep theWANT_JSONmarker in the file so Ansible passes them that way. - Strings and arrays have no
.length; use the globallength(x). - Objects compare by reference, so change detection needs its own recursive equality check.
sprintf("%J", obj)is the quickest way to print JSON.