VyOSIII · VyOS ArchitectureArchitecture
The commit engine — from candidate to running
What you'll learn
- Describe the commit-engine pipeline from `commit` to applied system state
- Separate the two validation layers — node-definition syntax and script `verify()`
- Read the commit output and the journal to diagnose a failed commit
- Recognise the commit-engine failure modes and their remediations
Prerequisites
Verified against VyOS 1.5.x LTS (circinus) · VyOS 1.4.x (sagitta) — legacy · FRRouting 10.x (VyOS 1.5) · Linux kernel 6.6 LTS (VyOS 1.5 base) · strongSwan 5.9.x (IPsec) · WireGuard 1.0.x (kernel module + userspace tooling) · 2026-08-19
The commit command is the most-used command on a VyOS
router. Every operator runs it dozens of times a day. The
operator who does not understand what the commit engine does
will not recognise the failure modes that surface as
half-applied configurations or as a commit that reports success
while the system does nothing new.
This lesson is the operator’s foundation in the commit engine:
what commit actually executes, where the two validation layers
live, and the failure modes that appear on either side of them.
What commit actually runs
VyOS does not translate the configuration tree into one long
script. It owns the tree in subtrees. Each subtree — interfaces ethernet, protocols static, firewall, service ssh — has a
Python configuration script that owns it, and the node
definitions in the tree record which script owns which node and
at which priority it must run.
When you type commit, the engine walks the difference between
the candidate and the running configuration, works out which
owning scripts are affected, sorts them by the priority declared
in the node definitions, and runs each one. Every one of those
scripts has the same four functions:
get_config()— read this script’s subtree out of the candidate configuration.verify()— decide whether that subtree is coherent. Raise aConfigErrorif not.generate()— render the subsystem’s own configuration: an FRR block, an nftables ruleset, awpa_supplicantfile, a systemd unit.apply()— hand that rendered state to the subsystem: netlink andipcommands for interfaces,frr-reloadfor routing,nftfor firewalling,systemctlfor services.
flowchart TB
A["Candidate configuration<br/>(working tree)"] --> B["Node-definition validation<br/>(already ran at set time)"]
B --> C["Diff vs running<br/>which subtrees changed"]
C --> D["Owning scripts, in priority order"]
D --> E["verify()"]
E -->|ConfigError| X["Commit failed<br/>node named in the output"]
E -->|coherent| F["generate()"]
F --> G["apply()<br/>ip / frr-reload / nft / systemctl"]
G --> H["Running configuration"]
The important structural fact is on the right-hand edge of that
diagram: verify() and apply() are per script, not per
commit. The engine does not verify the whole candidate and then
apply the whole candidate. It verifies and applies one subtree,
then moves to the next. A script that fails late in the priority
order therefore fails after earlier subtrees have already been
applied.
Two validation layers, and they run at different times
Operators routinely say “the commit rejected it”. Half the time
the commit never happened, because the value was refused several
seconds earlier at set time. The two layers are worth keeping
apart, because they fail differently and they tell you different
things.
Layer 1 — the node definition, at set time
Every leaf in the tree declares what it accepts: a type, and
often a validator with a range or a regular expression. That
check runs the moment you press Enter on the set, before the
value is ever written into the candidate.
vyos@r1# set interfaces ethernet eth0 address 192.0.2.300/24Invalid value
Value validation failed
Set failedIllustrative output
Nothing entered the candidate. compare shows no change, and
commit has nothing to do. The same layer refuses an MTU outside
the range the node advertises, a malformed AS number, and an
interface name that does not match the expected pattern. The
accepted range is not a secret: press ? or Tab after the node
and the CLI prints the constraint the definition declares.
Layer 2 — the script’s verify(), at commit time
Layer 1 cannot catch anything that depends on the rest of the
configuration, because it only sees one value. Coherence is the
job of verify(), and it runs during the commit:
- An address that is syntactically fine but already assigned to another interface.
- A firewall rule, policy or service that names an interface which does not exist.
- Deleting an interface that is still a member of a bridge or a bond.
- A protocol block that is missing a mandatory companion node.
vyos@r1# commit[ interfaces ethernet eth9 ]
Interface eth9 does not exist
[[interfaces ethernet eth9]] failed
Commit failedIllustrative output
Read that output structurally rather than as a sentence. The first bracketed line is the configuration path the engine was working on; the line under it is the message the script raised; the double-bracketed line repeats the path as the failure. If you know the path, you know which script to read and which subtree to fix.
Generate and apply: one subsystem at a time
generate() and apply() are where the tree stops being a tree
and becomes state on a Linux box. The translation is not uniform,
and knowing which subsystem owns a given piece of configuration
is what makes verification possible.
| Configuration subtree | Rendered into | Applied by |
|---|---|---|
interfaces ... | netlink / ip operations | the kernel, immediately |
protocols ..., vrf ... protocols ... | an FRR configuration block | frr-reload, into the running FRR daemons |
firewall ..., nat ... | an nftables ruleset | nft, into the kernel |
service ... | a daemon config file plus a unit state | systemctl |
The routing row is the one that surprises people. set protocols static route 10.0.0.0/24 next-hop 192.0.2.1 does not become
ip route add. It becomes a line of FRR configuration, FRR’s
zebra decides whether the route is usable, and only then does
zebra install it into the kernel FIB. That is a two-stage path,
and each stage has its own failure.
The diff decides which scripts run, not which lines change
The engine’s diff is computed over the configuration tree, and
its granularity is the owning script. If you change one static
route in a configuration that holds a thousand of them, the
engine runs one script — the one that owns protocols static —
and not the interface scripts, the firewall script, or anything
else. That is what makes a commit on a large configuration cheap.
What that script then does is a second, independent reconciliation.
It regenerates the whole FRR block it owns and hands it to
frr-reload, which compares the rendered configuration against
what FRR is actually running and issues only the vtysh commands
needed to close the gap. So the one-route change stays a one-route
change all the way down — but by two different diffs, computed by
two different pieces of software, for two different reasons.
The practical consequence: a routing change that does not take
effect can have failed at either diff. Either VyOS did not think
the subtree changed, or frr-reload did not accept the line it
generated. Those have different evidence, which is the next
section.
Failure modes
Commit fails at verify()
The named subtree is rejected. Scripts that already ran have already applied.
Causes:
- The subtree references something that does not exist.
- A mandatory companion node is missing.
- The change conflicts with configuration owned by another subtree.
Diagnostic:
- Read the bracketed path in the commit output; it names the script.
compareshows what is still pending in the candidate.journalctl -u vyos-configd -n 100shows the traceback when the message alone is not enough.
Fix:
- Correct the offending subtree, then commit again.
- If the failure was late in the priority order, check the subsystems that ran before it rather than assuming nothing was applied.
Commit succeeds but the kernel does not show the route
show ip route 10.0.0.0/24 shows the static route, but ip route show 10.0.0.0/24 returns nothing.
Causes:
- Zebra has the route but has not selected it — most commonly because the next-hop does not resolve.
- Another protocol has a route to the same prefix with a better administrative distance, so the static route lost selection.
Diagnostic:
show ip route 10.0.0.0/24— an unselected route is printed without the*FIB marker.show ip route 192.0.2.1— does the next-hop itself resolve?show interfaces— is the egress interface actually up?
Fix:
- Make the next-hop resolvable, or bind the route to an interface.
- Do not delete the static route to “clean up” before the next-hop is fixed; you will lose the evidence.
Commit succeeds but FRR does not show the configuration
sudo vtysh -c "show running-config" does not contain the block
you configured, even though show configuration commands does.
Causes:
- The configuration landed somewhere other than where you are looking — a VRF, or a different address family.
frr-reloadrejected the generated line, and the rejection is in the journal rather than on your terminal.
Diagnostic:
journalctl -u vyos-configd -n 100andjournalctl -u frr -n 100around the time of the commit.- Compare the VyOS tree with the FRR running configuration directly rather than from memory.
Fix:
- Correct the VyOS configuration and commit again.
- Restarting FRR is a last resort, not a first step: it tears down every routing adjacency on the box, and because VyOS regenerates FRR’s configuration from its own tree, a restart cannot introduce configuration that a commit did not already produce.
Operational commands
# The commit engine's own log.
journalctl -u vyos-configd -n 100
# Every commit the router has made, newest first, with user and method.
# Run from operational mode:
# show system commit
# The archived revisions themselves:
ls -l /config/archive/
show system commit is the command most operators are missing.
It lists the archived revisions with a timestamp, the user, and
how the change arrived (CLI, or an API/config-load). When someone
asks “what changed on this router last Tuesday”, that list is the
answer, and compare between two revision numbers is the diff.
Inside configuration mode:
compare— candidate against running.compare saved— candidate against what is on disk.discard— throw the candidate changes away.
The three states
The configuration exists in three states:
- Candidate (working) — being edited, not applied.
- Running (active) — applied to the system.
- Saved — on disk, in
/config/config.boot.
stateDiagram-v2
[*] --> Candidate: configure
Candidate --> Running: commit
Candidate --> Candidate: discard
Running --> Saved: save
Running --> Candidate: configure
Saved --> Running: reboot
The commit engine moves candidate to running. save moves
running to saved. A reboot loads saved into running. The common
and expensive mistake is committing without saving: the change
works, the operator leaves, and the next reboot silently reverts
it. compare saved is the one command that catches this, and it
costs nothing to run before you log out.
Validation
The sequence for “the commit engine is wrong”:
- The commit output — the bracketed path names the failing subtree.
- The engine log —
journalctl -u vyos-configd -n 100carries the traceback the terminal truncated. - Candidate against running —
compare, inside configuration mode. - Candidate against disk —
compare saved. - The subsystem itself —
ip route show,sudo vtysh -c "show running-config",sudo nft list ruleset. The tree is an intention; these are the state. - The history —
show system commitfor when and by whom.
If the tree contains the configuration and the subsystem does
not, the failure is in generate() or apply() and the journal
has it. If the subsystem reflects the change and /config/config.boot
does not, a reboot will revert it.
Cross-course references
- The Linux course’s
XX-Linux-NetConfigcovers the kernel state the interface scripts drive. - The lesson on commit-confirm and remote change discipline in Part VI covers the safe-change mechanics.
- The lesson on configuration tree structure in this part covers the tree itself.
Quiz
Knowledge check · 4 questions
Q1. You commit a static route. The commit reports no error. `show ip route` lists the prefix but `ip route show` does not. What is happening, and what do you check?
The operator runs: `set protocols static route 10.0.0.0/24 next-hop 192.0.2.1` `commit` The commit prints nothing. `show ip route 10.0.0.0/24` prints a static entry for the prefix, but without the `*` that marks a FIB-installed route. `ip route show 10.0.0.0/24` returns no entry at all.
Q2. You change one static route in a configuration that holds a thousand of them. What does the commit engine do?
Q3. A commit that fails can still have applied part of the change, because each owning script verifies and applies its own subtree before the next one runs.
Q4. You configure a BGP neighbour with the correct AS numbers on both sides and commit. The session stays in Active. What is the most likely cause?
R1 carries: `set protocols bgp system-as 65000` `set protocols bgp neighbor 192.0.2.1 remote-as 65001` The commit prints nothing, so it succeeded. `show bgp summary` shows the session in Active rather than Established, and the state does not change over several minutes.
Passing score: 75%. Answers are checked in this browser.
Production discipline
The commit engine is the bridge between the configuration tree and a running Linux router. It diffs, verifies, generates and applies — one owned subtree at a time, in a declared order. The operator who holds that model can read a failure message as a location rather than as a complaint, and can tell a commit that failed from a commit that succeeded into a subsystem that then disagreed.
Plan the commit discipline once. Validate after every commit with
the operational command for the thing you changed. Use
commit-confirm for remote changes. Run compare saved before
you log out. Then either the commit succeeded cleanly, or you
find the failure while you are still the person who caused it.