Intro

I am using VyOS virtual routers to provide firewall and routing services for my network. For configuration mangagement, I am using SaltStack. Awesomely, VyOS supports running a Salt minion, so it seemed like the perfect opportunity to manage the configuration of the VyOS devices with SaltStack.

In this post, I will show you how to get your VyOS devices configured via SaltStack with the native Salt minion.

Software Versions

The following software versions were used in this post.

  • SaltStack - 3006.6
  • VyOS - 1.5-rolling-202402050024

Salt Minion

In a previous post I showed how to bootstrap a VyOS device via cloud-init and Terraform to automagically join VyOS VMs to a Salt master.

Here is a snippet of VyOS commands from that post.

vyos
set system host-name '${hostname}'
set system domain-name '${domain}'
set service salt-minion id '${hostname}'
set system static-host-mapping host-name salt-master.${domain} inet '${salt_master_ipv4_addr}'
set service salt-minion master 'salt-master.${domain}'

This is all that is required on the VyOS device to join the Salt master. Depending on how your Master is setup, you might also need to accept the key on the master.

Config Methods

There are numerous ways to configure VyOS devices via SaltStack. There is Nornir, Napalam, and Netmiko. These methods either use the API or SSH to configure the devices.

Another way to configure VyOS devices is to use command scripting. This allows you to run set commands in a script to configure the device.

With command scripting you can use the native Salt minion to configure the device. This is the method I am using.

Config Files

I have a main config.jinja file that mostly imports other configuration block with the jinja include statement.

When using command scripting, you need to add a few items to the header, so that the script imports the necessary functions and sets the environment to run correctly.

Important
Comand scripts need to be run with the vyattacfg group, otherwise the configuration will be locked and a reboot of the device is required to alter it again.

The following config block, shows the header I use for the script.

header_block.jinja
{% from "makro/makro.sls" import start_comment with context %}
{% from "makro/makro.sls" import end_comment with context %}
{% from "makro/makro.sls" import echo with context %}
#! /bin/vbash

{{ start_comment("HEADER") }}
{{ echo("HEADER") }}
# Execute this script with the following command:

# Discard configuration changes
# VYOS_COMMIT="false" sg vyattacfg -c /home/minion/config.sh

# Commit configuration changes
# VYOS_COMMIT="true" sg vyattacfg -c /home/minion/config.sh

# Sets up aliases and functions for running Vyatta commands from scripts.
source /opt/vyatta/etc/functions/script-template

# This ensures that the script is run with the vyattacfg group.
if [ "$(id -g -n)" != 'vyattacfg' ] ; then
    exec sg vyattacfg -c "/bin/vbash $(readlink -f $0) $@"
fi

{{ end_comment("HEADER") }}

I won't show you all the configuration blocks, but here is a snippet of the VRRP block.

vrrp_block.jinja
{% import_json "data/netdata.json" as netdata %}
{% from "makro/makro.sls" import start_comment with context %}
{% from "makro/makro.sls" import end_comment with context %}
{% from "makro/makro.sls" import echo with context %}
{{ start_comment("VRRP")}}
{{ echo("VRRP")}}
{% set priority = pillar["priority"] %}
delete high-availability vrrp

set high-availability vrrp global-parameters version 3
{% for net, data in netdata["networks"].items() %}
{%   if data["vrrp_enabled"] %}
set high-availability vrrp group VLAN{{ data["id"] }}v4 vrid {{ data["id"] }}
set high-availability vrrp group VLAN{{ data["id"] }}v4 priority {{ data["vrrp"][priority]["priority"] }}
set high-availability vrrp group VLAN{{ data["id"] }}v4 rfc3768-compatibility
set high-availability vrrp group VLAN{{ data["id"] }}v4 interface {{ pillar["vyos"]["interface_map"][net] }}
set high-availability vrrp group VLAN{{ data["id"] }}v4 address {{ data["vrrp"]["gateway"]["ipv4"] }}/{{ data["ipv4"]["prefix_length"] }}
set high-availability vrrp group VLAN{{ data["id"] }}v4 excluded-address {{ data["vrrp"][priority]["ipv6"] }}/{{ data["ipv6"]["prefix_length"] }}

set high-availability vrrp group VLAN{{ data["id"] }}v6 vrid 1{{ data["id"] }}
set high-availability vrrp group VLAN{{ data["id"] }}v6 priority {{ data["vrrp"][priority]["priority"] }}
set high-availability vrrp group VLAN{{ data["id"] }}v6 rfc3768-compatibility
set high-availability vrrp group VLAN{{ data["id"] }}v6 interface {{ pillar["vyos"]["interface_map"][net] }}
set high-availability vrrp group VLAN{{ data["id"] }}v6 address {{ data["vrrp"]["gateway"]["ipv6"] }}/{{ data["ipv6"]["prefix_length"] }}
set high-availability vrrp group VLAN{{ data["id"] }}v6 excluded-address {{ data["vrrp"][priority]["ipv4"] }}/{{ data["ipv4"]["prefix_length"] }}

{%   endif %}
{% endfor %}
{{ end_comment("VRRP")}}

I have a Salt state that generates the netdata.json data file. This file contains all the configuration parameters for my network and saves me manually having to define alot of pillar data. I'll write about this more in another post.

Finally, we have the config.jinja file that imports all the configuration blocks. At the end of the file, I use an environment variable $VYOS_COMMIT to determine if the configuration should be committed or discarded. This allows me to test configuration changes before I commit them.

config.jinja
{% include "vyos_config/header_block.jinja" %}

{% include "vyos_config/configure_block.jinja" %}

{% include "vyos_config/firewall_global_option_block.jinja" %}

{% include "vyos_config/firewall_group_block.jinja" %}

{% include "vyos_config/firewall_filter_block.jinja" %}

{% include "vyos_config/dns_block.jinja" %}

{% include "vyos_config/user_block.jinja" %}

{% include "vyos_config/ntp_block.jinja" %}

{% include "vyos_config/lldp_block.jinja" %}

{% include "vyos_config/ssh_block.jinja" %}

{% include "vyos_config/nat_block.jinja" %}

{% include "vyos_config/dhcp_block.jinja" %}

{% include "vyos_config/salt_block.jinja" %}

{% include "vyos_config/syslog_block.jinja" %}

{% include "vyos_config/interface_block.jinja" %}

{% include "vyos_config/vrrp_block.jinja" %}

{% include "vyos_config/isis_block.jinja" %}

{% include "vyos_config/system_block.jinja" %}

{% include "vyos_config/compare_block.jinja" %}

case $VYOS_COMMIT in
  0)
    {% include "vyos_config/discard_block.jinja" %}
    ;;

  1)
    {% include "vyos_config/commit_block.jinja" %}
    ;;

  *)
    {% include "vyos_config/discard_block.jinja" %}
    ;;
esac

Salt State

The Salt state that manages the configuration is shown below. The state first manages the config.sh file, then runs the script to apply the configuration. The script is run with the vyattacfg group, so that the configuration is not locked.

I use the pillar["vyos"]["commit_config"] pillar variable as the value for the VYOS_COMMIT environment variable. This allows me to override the value when I want to commit the configuration. The default vaule is 0 which discards the configuration.

init.sls
vyos_config:
  file.managed:
    - name: /home/minion/config.sh
    - source: salt://vyos_config/config.jinja 
    - template: jinja
    - user: minion
    - group: vyattacfg
    - mode: 0755

apply_config:
  cmd.run:
    - name: sg vyattacfg -c /home/minion/config.sh
    - env:
      - VYOS_COMMIT: {{ pillar["vyos"]["commit_config"] }}
    - watch:
      - file: vyos_config

Apply Config

To test configuration changes, I can run the following command. Which will run against all VyOS devices in the vyos node group. It loads the configuration, runs a comparison then discard it. This allows me to confirm all commands are working and shows me what is changing prior to committing.

cmd
sudo salt -N vyos state.apply

When I am ready to apply the configuration, I can run the following command. Which will apply the configuration, commit and save it.

cmd
sudo salt -N vyos state.apply pillar='{"vyos": {"commit_config": 1}}'

Outro

This my friends, brings us to the end of this post. I hope you found it useful. In this post, I showed you how to configure VyOS devices with SaltStack using the native Salt minion.

Until next time. ✌️ Stay weird ✌️

# salt
# vyos