openwrt-iac / uapi / docs / 2.5.1 / Non-uci state: what uapi covers and what it doesn't
uapi's design contract is "uci is the source of truth". Almost every resource translates HTTP REST verbs into uci_set / uci_commit followed by a daemon reload. That model covers most of OpenWrt's configuration surface, but not all of it. This document inventories the small set of state that lives outside uci, in two categories:
The bar for adding to category 1 is high. New non-uci resources should justify themselves against three questions: is the underlying daemon's uci surface genuinely missing this? would the right long-term fix be to add a uci option upstream rather than carry workaround state in uapi? does the resource fit a curated CRUD shape, or is it a one-shot action better handled by an operator tool?
| Resource | Source of truth | Lock | Reload | Notes |
|---|---|---|---|---|
packages/installed |
apk DB (apk add/del shell-out) |
with_lock on global |
none | postinst runs as root; package's responsibility to wire its own uci/init scripts |
packages/feeds |
/etc/apk/repositories.d/*.list + apk update |
with_lock on global |
none | url-validated, name-validated |
dhcp/leases |
/tmp/dhcp.leases (parse) |
none (read-only) | n/a | dnsmasq IPv4 leases |
dhcp/leases6 |
/tmp/hosts/odhcpd (parse), fallback /tmp/odhcpd.leases |
none (read-only) | n/a | odhcpd IPv6 leases |
system/password |
/bin/busybox passwd <user> (stdin pipe) |
with_lock on global |
none | write-only; audit logs token+user, never the password |
system/authorized_keys |
/etc/dropbear/authorized_keys (mode 0600) |
with_lock on global for writes; reads lock-free |
none | dropbear re-reads the file per connection |
Each entry below expands on its lock semantics, audit shape, the file/process it touches, and why the standard uci-transaction recipe doesn't apply.
Adding a non-uci resource means adding a row above. The bar is high: prefer driving the underlying daemon's uci surface if the option exists, or upstreaming the option to OpenWrt uci if it doesn't, before adding non-uci state to uapi.
packages/installedapk info --installed; writes shell out to apk add <name> and apk del <name> via fs.popen.transaction.with_lock on /var/lock/uapi.lock. apk has its own DB lock; the uapi flock serialises concurrent uapi requests so they don't race apk-vs-apk and surface as nondeterministic 5xxs. Contention returns 423 locked with Retry-After: 1.^[A-Za-z0-9_+][A-Za-z0-9_+.-]*$. Names starting with - or . are rejected to prevent apk-flag injection (--allow-untrusted, --repository=...); the shell-out also uses apk add -- <name> as a defense-in-depth separator.uapi-pkg-failure <request_id> action=install name="..." exit=<n> output="..."). The HTTP response carries only a generic "apk add failed (exit N); see syslog /var/lib/apk/...), not a uci config. The right place to drive it is apk itself.packages/feeds/etc/apk/repositories.d/*.list.packages/installed. After every write, apk update is invoked so the new feed is immediately available.apk update IS the reload.^[A-Za-z0-9_][A-Za-z0-9_.-]*$ (no leading dot, no leading dash). URL must be http:// or https://. file:// and other schemes are rejected./etc/apk/repositories.d/ directly. There is no uci wrapper for it upstream.dhcp/leases (read-only)/tmp/dhcp.leases (the dnsmasq lease dump).<expires_at> <mac> <ip> <hostname> <duid?>./etc/config/.dhcp/leases6 (read-only)/tmp/hosts/odhcpd (preferred) or /tmp/odhcpd.leases (older odhcpd versions).<duid> <iaid> <hostname> <expires_at> <interface> <IA_NA|IA_PD> <ip>[/<prefixlen>] [<ip2> ...]. Lines that don't match this shape are silently skipped. odhcpd's statefile format varies across versions; parser failure on a single line never breaks the read path for the rest.dhcp/leases; runtime state.system/password (write-only)/etc/shadow, via passwd(1).transaction.with_lock./bin/busybox passwd <user> and pipes <pw>\n<pw>\n through stdin (the same recipe LuCI uses for its "Router Password" page).user must match ^(root|[a-z][a-z0-9_-]*)$. password must be at least 8 characters.uapi-passwd-set <request_id> user="...". Failures emit uapi-passwd-failure <request_id> user="..." exit=<n>. The password VALUE is never logged.POST /api/v2/system/password returns 204. There is no GET (password hashes don't leak through the API at all).passwd -d root line, etc.) is the alternative for purely offline provisioning.system/authorized_keys/etc/dropbear/authorized_keys (mode 0600).transaction.with_lock for writes; reads are lock-free.<type> <base64-blob> [comment]. Allowed types: ssh-rsa, ssh-ed25519, ecdsa-sha2-nistp{256,384,521}, sk-ssh-ed25519@openssh.com, sk-ecdsa-sha2-nistp256@openssh.com. Options blocks (command="...", no-pty, etc.) before the key type are stripped from the canonical form.+/= remapped to letters so it fits a URL-safe ^[a-z0-9]{12}$ pattern). Same key always gets the same id, idempotent across re-adds.GET lists; POST adds one; PUT replaces wholesale (dedup); DELETE /<id> removes one.fs plugin.The following items are real configuration concerns that uapi deliberately does NOT cover. For each, the recommended out-of-band path is listed. An IaC orchestrator typically wires these once at image-bake time (cloud-init / /etc/uci-defaults/) rather than reaching for them on every reconcile.
unbound uci has no clean option for binding the recursive server to a specific address (e.g. loopback-only). The upstream init script emits interface-automatic: yes unconditionally when interface_auto=1; there is no uci path that produces an interface: 127.0.0.1 line. uapi's unbound/server resource curates every uci option the upstream init script actually consumes, but it deliberately does not invent an option that uci doesn't expose./etc/unbound/unbound_srv.conf (auto-included inside the server: clause), or set manual_conf=1 via PATCH /api/v2/unbound/server and write the full /etc/unbound/unbound.conf yourself. The unbound_srv.conf file ships as a stub from the upstream package precisely for this use./etc/inittab is not uci./etc/inittab at first boot.etherwake via packages/installed and run it from a script over SSH or via a separate orchestration tool. The MAC + interface map is operator memory.px5g_x509uhttpd/certs curates the px5g cert-generation PARAMETERS (days, bits, CN, etc.), but the actual one-shot regeneration is an action. uci doesn't model it.px5g_x509 (or acme.sh / luci-app-acme for real certs) out-of-band. For production, an ACME workflow keyed off a cron or systemd timer is the standard approach.sysctl tunables from an OPNsense-style migrationsysctl parameter) and drop it into /etc/sysctl.d/ at image-bake time.vnstat/config and vnstat/interfaces for configuring the collector; the database itself stays where vnstat puts it.If your real-world configuration needs something uapi can't currently express, the first question to ask is: does the underlying OpenWrt package expose this via uci? If yes, the curated resource is undercurated; file an issue. If no, the right fix is usually to upstream a uci option, not to add another non-uci resource to uapi.
If, after weighing those, a new non-uci resource really IS the right answer, it needs to ship together with:
<domain>:<resource>:rw / :ro).