~/posts/netbox-config-templates-for-vyos
NetBox Config Templates for VyOS
In an earlier post I used Peering Manager to generate the BGP sessions for VyOS. That covers the peering part, but a router has a lot more configuration: hostname, interfaces, addresses, loopbacks, name servers, NTP, SNMP. If NetBox already is your source of truth for all of that, it is a waste to maintain it a second time by hand on the router.
Since version 3.5 NetBox has config templates: Jinja2 templates that are rendered against a device and its data. This post shows a basic template for VyOS and how to get the rendered result onto the router.
How config templates work
A config template is a Jinja2 template stored in NetBox (or synced from a Git repository through a data source). You assign it to a platform, a device role or a single device; the most specific assignment wins. When you render it, the template gets:
device: the device object, with all its relations (interfaces, IP addresses, site, platform and so on)- the device’s config context data as top-level variables
That last part is what makes it flexible. Things that are not modelled in NetBox, such as NTP or syslog servers, go into a config context per site or region and become available as plain variables in the template.
A config context for the site
First, a config context assigned to the site (or a region, to share it between sites):
{
"ntp_servers": ["192.0.2.123", "198.51.100.123"],
"name_servers": ["192.0.2.53", "198.51.100.53"],
"snmp_community": "change-me",
"syslog_host": "192.0.2.14"
}
The template
The template below renders the system section and all physical interfaces with their IP addresses. It skips interfaces that are disabled in NetBox and uses the interface description from NetBox as the description on the router.
set system host-name '{{ device.name }}'
{% for ns in name_servers %}
set system name-server '{{ ns }}'
{% endfor %}
{% for server in ntp_servers %}
set service ntp server '{{ server }}'
{% endfor %}
set system syslog host {{ syslog_host }} facility all level 'notice'
set service snmp community {{ snmp_community }} authorization 'ro'
{% for iface in device.interfaces.all() %}
{% if iface.enabled and iface.name.startswith('eth') %}
set interfaces ethernet {{ iface.name }} description '{{ iface.description or iface.name }}'
{% if iface.mtu %}
set interfaces ethernet {{ iface.name }} mtu '{{ iface.mtu }}'
{% endif %}
{% for ip in iface.ip_addresses.all() %}
set interfaces ethernet {{ iface.name }} address '{{ ip.address }}'
{% endfor %}
{% endif %}
{% endfor %}
{% for iface in device.interfaces.filter(name__startswith='dum') %}
{% for ip in iface.ip_addresses.all() %}
set interfaces dummy {{ iface.name }} address '{{ ip.address }}'
{% endfor %}
{% endfor %}
A few notes:
- The output is a list of
setcommands, the same format you type in VyOS configuration mode. That keeps the template readable and the output easy to review. - The syntax is for VyOS 1.4. Some paths change between releases (syslog, for example), so check the output against the version you run.
- Loopback addresses are modelled in NetBox as
dum0interfaces of type virtual, which maps directly onto VyOSdummyinterfaces. - You can call Django queryset methods such as
filter()in the template. That is convenient, but keep it limited: complex logic is easier to test in a NetBox custom script or plugin than in Jinja.
Rendering
In the NetBox UI, open the device and go to the Render Config tab. That is the quickest way to develop the template: edit, refresh, repeat.
For automation, use the API. A POST to the render-config endpoint returns the rendered configuration:
curl -s -X POST \
-H "Authorization: Token $NETBOX_TOKEN" \
-H "Accept: text/plain" \
https://netbox.example.net/api/dcim/devices/42/render-config/
With Accept: text/plain you get the configuration as plain text; without it, you get JSON with the result in the content field.
Getting it onto the router
How you apply it depends on how much you trust the template. My advice: start with diffs only. Ansible’s vyos.vyos.vyos_config module can load set commands and shows what it would change in check mode:
- name: Render and apply base config from NetBox
hosts: vyos
gather_facts: false
tasks:
- name: Render config template
ansible.builtin.uri:
url: "{{ netbox_url }}/api/dcim/devices/{{ netbox_id }}/render-config/"
method: POST
headers:
Authorization: "Token {{ netbox_token }}"
Accept: "text/plain"
return_content: true
delegate_to: localhost
register: rendered
- name: Apply to VyOS
vyos.vyos.vyos_config:
lines: "{{ rendered.content.splitlines() | select | list }}"
save: true
Run it with --check --diff first. Only when the diff is empty, or contains exactly what you expect, run it for real.
Keep in mind that this only adds and changes configuration. Something you remove from NetBox does not disappear from the router. You can handle that later with delete statements in the template or by replacing whole sections, but for a first version, additive is the safer choice.
Combined with Peering Manager
NetBox renders the base configuration, Peering Manager renders the BGP sessions and route-maps. Both produce set commands, so you can apply them one after the other in the same playbook. With that, the router configuration is completely generated from the two sources of truth.