Skip to main content
RunBook Academy

VyOSV · Configuration ModelConfigure mode

Hierarchical tree — the schema the operator navigates

Foundation⏱ ~14 minconfigureshow configuration commandsedit / up / topcompare

What you'll learn

  • Tell a node, a tag node and a leaf node apart, and name each part of a configuration path
  • Use scope (relative paths from inside an `edit`) to write concise configurations
  • Discover a node and its constraints from the CLI, and read the node definition on disk when that is not enough
  • Recognise the failure modes that come from scope drift and from schema differences between releases

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.

Hierarchical tree — the schema the operator navigates

Every VyOS configuration is a tree. The set command writes to a path through it, delete removes from it, show reads it back. Knowing the shape of that tree — which parts of a path are fixed names, which parts you choose, and which part is the value — is the difference between an operator who can follow a runbook and one who can work out a command they have never seen.

Three kinds of node

Every element of a VyOS path is one of three things, and the whole CLI follows from the distinction:

  • Node — a branch with a fixed name. interfaces, protocols, firewall, protocols bgp. You cannot invent one; it exists or it does not.
  • Tag node — a branch whose name you supply. ethernet eth0, neighbor 192.0.2.2, rule 10, name WAN-IN. The node is neighbor; the tag is 192.0.2.2. Tag nodes are why the tree is not a fixed-size document.
  • Leaf node — the end of a path, holding a value. description, remote-as, mtu. Some leaves hold exactly one value, some hold several, and some hold none at all — their presence is the value, which is how disable, passive and no-summary work.

Read a path by labelling each element:

set protocols bgp neighbor 192.0.2.2 address-family ipv4-unicast maximum-prefix '5000'
     ^node    ^node ^tagnode ^tag    ^node          ^tag         ^leaf          ^value

Once you can do that, an unfamiliar command stops being a magic string. You can see which piece is yours to choose, which piece has to exist already, and where the value goes.

The tree shape

flowchart TB
  Root(("/")) --> system
  Root --> interfaces
  Root --> protocols
  Root --> firewall
  Root --> service
  Root --> vrf
  system --> login
  login --> user["user (tag node)"]
  user --> vyos["vyos"]
  interfaces --> ethernet["ethernet (tag node)"]
  ethernet --> eth0["eth0"]
  eth0 --> addr["address (multi leaf)"]
  eth0 --> desc["description (leaf)"]
  protocols --> bgp
  protocols --> ospf
  bgp --> sysas["system-as (leaf)"]
  bgp --> nbr["neighbor (tag node)"]
  nbr --> peer["192.0.2.2"]
  peer --> ras["remote-as (leaf)"]

Note where the local AS number sits. On VyOS 1.5 it is a leaf under protocols bgpset protocols bgp system-as 64512 — and neighbours are tag nodes directly under protocols bgp. In VyOS 1.3 the AS number was itself a tag node, so every BGP path went through it (set protocols bgp 64512 neighbor ...). VyOS 1.4 flattened that, and a 1.3-era command pasted into a 1.5 router is rejected as an unknown path rather than quietly misapplied.

edit <path> moves the working point down into a node; up moves one level back; top returns to the root. The prompt banner in square brackets always tells you where you are:

[edit]
vyos@r1# edit protocols bgp
[edit protocols bgp]
vyos@r1# set neighbor 192.0.2.2 remote-as '65001'
[edit protocols bgp]
vyos@r1# set neighbor 192.0.2.2 address-family ipv4-unicast maximum-prefix '5000'
[edit protocols bgp]
vyos@r1# up
[edit protocols]
vyos@r1# top
[edit]

Inside a node, set paths are relative to that node. So this, from [edit protocols bgp]:

set neighbor 192.0.2.2 remote-as '65001'

writes exactly the same path as this, from [edit]:

set protocols bgp neighbor 192.0.2.2 remote-as '65001'

The relative form is shorter when you are adding many leaves under one scope, and it is where most scope accidents happen. Read the bracketed banner before every set you did not type from the top.

Discovering a node without leaving the CLI

Before reading anything on disk, ask the CLI. It carries the same help text the schema declares, and it is always right for the release you are standing on:

  • set protocols bgp ? lists the children of that node with one line of help each.
  • Tab completion after a partial word completes it, or lists the candidates when it is ambiguous.
  • ? after a leaf shows what kind of value it expects — a range, an address, an enumeration.

This matters more than it sounds. Documentation and runbooks age; the tree on the router in front of you does not. When a command from a blog post is rejected, ? at each level of the path shows you where the tree actually diverges from the one the author had.

[edit]
vyos@r1# set protocols bgp ?

Where the schema really lives

Two places, and confusing them is the usual reason an operator goes looking in the wrong directory.

In the source, node definitions are XML: the vyos-1x repository holds an interface-definitions/ tree where each subsystem declares its nodes, tag nodes, leaf nodes, help text, value constraints and the script that owns the subtree. This is what a contributor edits.

On the running router, those XML files have been compiled at package-build time into the vyatta-cfg template tree under /opt/vyatta/share/vyatta-cfg/templates/. The directory layout mirrors the configuration tree exactly — one directory per node, node.tag/ where a tag node is, and a node.def file carrying that node’s declaration.

# The path mirrors the configuration path, one directory per node.
NODE=/opt/vyatta/share/vyatta-cfg/templates/protocols/bgp

ls "$NODE"
cat "$NODE/node.def"

Multi-valued leaves, and the comma trick that is not one

A leaf marked multi: accumulates. Each set adds a value rather than replacing the previous one:

[edit]
vyos@r1# set system name-server '192.0.2.53'
[edit]
vyos@r1# set system name-server '198.51.100.53'
[edit]
vyos@r1# show system name-server
 name-server 192.0.2.53
 name-server 198.51.100.53

To replace rather than add, delete the value first. This is the single most common surprise for operators arriving from a config-file world, where writing the line twice would overwrite.

A comma-separated string is a different mechanism and it is not interchangeable. It works only where the leaf’s own value type accepts a list — port matches are the usual case:

set firewall ipv4 name WAN-IN rule 10 destination port '80,443,8080'

Here the whole string is one value that the port validator knows how to parse. Try the same on an address leaf and the validator rejects it, because an IPv4-address type does not accept a comma. The rule of thumb: multi-valued leaves take repeated set commands; only leaves documented as accepting a list take a comma.

Two validation layers

The tree validates in two places, and they fire at different times.

At set time, the node’s own constraint runs — the type and whatever syntax:expression: declares. This is where a malformed address or an out-of-range number is refused, before the value enters the candidate at all. compare shows nothing, because nothing changed.

At commit time, the script that owns the subtree runs its verify(), which is the only layer that can see the rest of the configuration. That is where cross-references are checked:

  • A jump-target naming a rule set that does not exist: set firewall ipv4 forward filter rule 10 action jump with jump-target WAN-IN requires firewall ipv4 name WAN-IN to exist, and the commit fails if it does not.
  • An address on an interface that is a member of a bridge — the bridge owns the addressing, and the commit says so.
  • A route-map, prefix-list or interface named by one subtree and absent from another.

The split explains a common confusion. “The commit rejected my IP address” is almost always wrong: a malformed address never reached the commit. What the commit rejects is a relationship.

From tree to running system

On commit, the owning script for each changed subtree generates its subsystem’s configuration and applies it. The path differs by subsystem, and knowing which one owns your change is what makes verification possible:

flowchart LR
  A["commit"] --> B["diff the tree<br/>which subtrees changed"]
  B --> C["protocols / vrf"]
  B --> D["firewall / nat"]
  B --> E["interfaces"]
  C --> F["FRR config block"] --> G["frr-reload"]
  D --> H["nftables ruleset"] --> I["nft"]
  E --> J["netlink / ip"]

Note the firewall row: VyOS 1.4 and later render nftables, not iptables. sudo nft list ruleset is where a firewall change ends up, and a runbook that tells you to check iptables -L was written for 1.3.

How the result is validated

From configuration mode:

show configuration commands
show configuration commands | match 'interfaces ethernet'
compare
compare saved

show configuration commands prints the tree back as the set commands that would rebuild it, which is the form to paste into a change record. compare shows candidate against running; compare saved shows candidate against what is on disk, and is the one that catches the commit-without-save mistake.

From the shell, when you need the schema rather than the values:

NODE=/opt/vyatta/share/vyatta-cfg/templates/interfaces/ethernet
find "$NODE" -maxdepth 2 -name node.def | head

How it fails

  • Scope drift. A set issued from inside an edit writes a relative path. If the resulting path happens to be valid, nothing complains and the change lands in the wrong branch. compare before commit is what catches it.
  • A path from the wrong release. A 1.3-era command is rejected as an unknown path, not silently accepted. That is the good outcome; the bad one is a command whose shape survived the release but whose meaning changed, which is why a migration is a re-read rather than a search and replace.
  • Constraint rejected at set, blamed on commit. The error arrived seconds earlier and scrolled away. If compare shows no change, the set never took.
  • Cross-reference rejected at commit. A jump-target, route-map or interface named before it exists. Create the referenced object first, or create both in the same candidate — the check runs against the candidate, so both being present at commit is enough.
  • Loading an old configuration into a newer image. VyOS migration scripts convert a saved configuration on upgrade, but a configuration hand-edited to a path that no longer exists fails to load. Read the release notes before pasting a saved config forward.

Rollback

The tree itself has no undo; recovery is at the candidate and revision level:

  • discard — throw away every uncommitted change and start again.
  • delete <path> then commit — remove one specific change.
  • rollback N — return to an archived revision, listed by show system commit.
  • load /config/archive/<file> then commit and save — restore a specific archived snapshot.

Production discipline

Cross-course references

The Linux course’s II-Linux-Install covers the filesystem the template tree lives on. The Ansible course’s XLII-Ansible-BeyondLinux shows how to drive the same tree from automation, which is worth reading precisely because it has to produce the same paths this lesson teaches you to read.

Quiz

Knowledge check · 4 questions

  1. Q1. Where does the configuration-tree schema live on a running VyOS 1.5 router?

  2. Q2. Two `set` calls on a multi-valued leaf append to the list rather than overwrite.

  3. Q3. An operator runs `set interfaces ethernet eth0 address '192.0.2.1/24'` from inside `[edit protocols bgp]`. The CLI refuses it. What happened, and what is the habit that prevents it?

    The operator had run `edit protocols bgp` to add several neighbour leaves, then typed a full absolute path for an unrelated interface change without returning to the top of the tree. The CLI rejects the command.

  4. Q4. A `set` command is refused with a validation error and the operator is not sure whether the value or the path is wrong. How do they establish which, and where does the constraint come from?

    The operator types `set protocols bgp system-as 4294967296` and the CLI answers with a validation failure rather than accepting the value. A colleague insists the router `does not support 4-byte AS numbers`, which does not sound right.

Passing score: 75%. Answers are checked in this browser.