The configuration tree#
A Wheelhouse router keeps its entire configuration in one tree. Every setting is a
path through that tree ending in a value, and every change is a set or a delete
against such a path. There is no second place where configuration lives: no per-page
table, no XML document, no database rows that a form writes on save. The web UI is a
set of editors over that one tree, and each screen states the subtree it owns beside
its title, so you always know which part of the tree you are looking at.
Paths, nodes, values#
Read a path as words separated by spaces, the way the router's own CLI spells it:
set interfaces ethernet eth1 address 203.0.113.2/24
set interfaces ethernet eth1 description 'primary uplink'
set protocols static route 0.0.0.0/0 next-hop 203.0.113.1Three kinds of thing appear in a path.
| Kind | Example | What it is |
|---|---|---|
| A node | interfaces, protocols static | A branch with a fixed name. Its children are fixed too. |
| A tag node | ethernet eth1, route 0.0.0.0/0, name adguard | A branch whose children are named by you. ethernet is the tag node; eth1 is one instance of it. |
| A value | 203.0.113.2/24, 'primary uplink' | The leaf. It is the last word of a set command. |
A node can also exist with no value at all — set service ssh switches SSH on and has
nothing after it. That shape matters later, because a
desired-state file has to be able to express it.
Reading a subtree#
GET /api/config returns the tree, or the subtree named by path, as JSON.
R=https://<router>:8443
T=wh_... # an API token with the viewer role
curl -sk -H "Authorization: Bearer $T" "$R/api/config?path=%5B%22service%22,%22dns%22%5D"{"forwarding": {"cache-size": "10000", "listen-address": {"192.0.2.1": {}}}}Two details in that request are worth knowing, because both have cost somebody time.
path is a JSON array, not a slash-joined string. The agent accepts both, but a
slash-joined string cannot name a node whose key contains a slash — and plenty do:
route 0.0.0.0/0, subnet 192.0.2.0/24. The web UI always sends the array form
(agent/main.go parsePathParam).
A tag-node read comes back wrapped in its own name. The router answers a read
ending at an ordinary node with the bare subtree, but a read ending at a tag node with
the subtree inside a single key named after the node — container name answers
{"name": {"adguard": …}}, not {"adguard": …}. The agent unwraps that in one place,
agent/opmode.go configTag, and the comment there records
what reading the wrapped form as an instance map cost: every instance reported as
absent.
An unset subtree is not an error worth showing. The router answers a read of an
empty path with HTTP 400 and the words Configuration under specified path is empty.
That is the normal state of a router with no high-availability configuration, so
handlers test for it rather than reporting the router as unreachable —
agent/gaps.go handleVRRP.
Reading a subtree as commands#
Any subtree can be rendered back as the set lines that would recreate it. That is
what the Config tree page's All commands view shows, and what every CLI panel on
every editor is built from:
curl -sk -H "Authorization: Bearer $T" "$R/api/config/commands?path=%5B%22nat%22%5D"set nat destination rule 10 description 'web'
set nat destination rule 10 destination port 443
set nat destination rule 10 inbound-interface name eth1
set nat destination rule 10 translation address 192.0.2.10A third form exists for backups: GET /api/config/raw returns the configuration in
the router's native curly-brace format. It is the only configuration read that is not
redacted, so it requires the admin role — agent/main.go,
routes().
What the tree is not#
- It is not the agent's data. The router owns it. The agent persists only what the
router cannot: accounts, API token hashes, sessions, settings, the licence key and
the audit log, in
/config/wheelhouse/—agent/store.go. Every other thing the UI shows is read from the router on demand. - It is not everything the router is doing. The tree is intent. What the kernel has actually installed is a separate question, and where the two disagree the UI says so — see State, intent and drift.
- It does not cover the whole of Linux. Some things the kernel can do have no node in the tree. Where that matters, the page in question says so with a Platform gap callout rather than inventing a node.
Why every screen names its subtree#
Because it makes a screen falsifiable. A page that says it owns nat can be checked:
read nat as commands, compare it with what the page shows, and any difference is a
bug in the page rather than a mystery. The rule is in
docs/ui.md — every page names its config path next to the
title — and it is the same rule that makes the Config tree page useful as a
cross-check on every other page.
The Config tree page renders the whole tree, filters it by path or value, and
renders any node you select as the commands that would recreate it
(ui/src/pages/ConfigTree.tsx). The command
palette indexes config objects out of the running configuration as well as pages, so
a rule number or a peer name is a search away
(ui/src/components/CommandPalette.tsx).
See also#
- Every click shows its commands — the same commands, rendered before anything is staged.
- Staging: the working set — where a
setgoes when you click Add. - Desired state — the same tree, written down as a file.
- Agent endpoints —
/api/config,/api/config/commandsand/api/config/raw. - What the agent asks the router — why these reads are cached.
Checked against#
agent/vyos.go ·
agent/opmode.go ·
agent/main.go ·
agent/gaps.go ·
agent/staging.go ·
agent/store.go ·
ui/src/pages/ConfigTree.tsx ·
ui/src/lib/format.ts ·
docs/ui.md