published: 20th of February 2024
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.
The following software versions were used in this post.
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.
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.
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.
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.
The following config block, shows the header I use for the script.
{% 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.
{% 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.
{% 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
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.
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
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.
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.
sudo salt -N vyos state.apply pillar='{"vyos": {"commit_config": 1}}'
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 ✌️
https://docs.vyos.io/en/latest/automation/vyos-salt.html
https://docs.vyos.io/en/latest/automation/command-scripting.html