Skip to main content
RunBook Academy

VyOSIII · VyOS ArchitectureArchitecture

The commit engine — from candidate to running

Intermediate⏱ ~14 minconfigurecommitcompareshow system commitshow logvyos

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

Not yet marked complete on this device.

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:

  1. get_config() — read this script’s subtree out of the candidate configuration.
  2. verify() — decide whether that subtree is coherent. Raise a ConfigError if not.
  3. generate() — render the subsystem’s own configuration: an FRR block, an nftables ruleset, a wpa_supplicant file, a systemd unit.
  4. apply() — hand that rendered state to the subsystem: netlink and ip commands for interfaces, frr-reload for routing, nft for firewalling, systemctl for 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.

Configuration changerejected at set time
vyos@r1# set interfaces ethernet eth0 address 192.0.2.300/24
Invalid value
Value validation failed
Set failed

Illustrative 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.
Configuration changerejected at commit time
vyos@r1# commit
[ interfaces ethernet eth9 ]
Interface eth9 does not exist

[[interfaces ethernet eth9]] failed
Commit failed

Illustrative 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 subtreeRendered intoApplied by
interfaces ...netlink / ip operationsthe kernel, immediately
protocols ..., vrf ... protocols ...an FRR configuration blockfrr-reload, into the running FRR daemons
firewall ..., nat ...an nftables rulesetnft, into the kernel
service ...a daemon config file plus a unit statesystemctl

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.
  • compare shows what is still pending in the candidate.
  • journalctl -u vyos-configd -n 100 shows 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-reload rejected the generated line, and the rejection is in the journal rather than on your terminal.

Diagnostic:

  • journalctl -u vyos-configd -n 100 and journalctl -u frr -n 100 around 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”:

  1. The commit output — the bracketed path names the failing subtree.
  2. The engine logjournalctl -u vyos-configd -n 100 carries the traceback the terminal truncated.
  3. Candidate against runningcompare, inside configuration mode.
  4. Candidate against diskcompare saved.
  5. The subsystem itselfip route show, sudo vtysh -c "show running-config", sudo nft list ruleset. The tree is an intention; these are the state.
  6. The historyshow system commit for 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-NetConfig covers 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

  1. 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.

  2. Q2. You change one static route in a configuration that holds a thousand of them. What does the commit engine do?

  3. 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.

  4. 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.