Chapter 4. Configuration Management: Introduction
The previous chapters covered the basics: you learned how to install and configure the tools, and began working with the Salt CLI. Now that you have an understanding of those fundamentals, you can start diving into the configuration management and advanced templating capabilities of Salt.
Configuration management is among the most important tasks in the automation process, Salt’s built-in features simplify this process dramatically. It is essential that you have thoroughly reviewed the material covered in previous chpaters before proceeding with this one, as the concepts we’ve discussed previously play an important role in the configuration management methodologies discussed here. For the moment we will mainly use the CLI to apply simple configuration changes: it is important to understand and get comfortable with advanced Salt templating.
Loading Static Configuration
Let’s suppose we are at a point where our large-scale network does not have consistent configuration. Loading a static configuration change can come in very handy for such cases (see Example 4-1).
Example 4-1. Load static configuration changes
$
sudo salt -G'vendor:arista'
\
net.load_config\
text
=
'ntp server 172.17.17.1'
device2: ---------- already_configured: False comment: diff: @@ -42,6 +42,7 @@ ntp server 10.10.10.1 ntp server 10.10.10.2 ntp server 10.10.10.3 +ntp server 172.17.17.1 ntp serve all ! result: True
By executing net.load_config
, you can load a simple configuration change. Targeting using the grain matcher, each device that is an Arista switch replies back with a configuration difference (the equivalent of show | compare
in Junos terms). It also informs us that the device was not already configured and the changes succeeded. The result
field is True
, as you can see in Example 4-2.
Example 4-2. Loading static configuration changes: dry run
$
sudo salt -G os:eos\
net.load_config\
text
=
'ntp server 172.17.17.1'
\
test
=
True device2: ---------- already_configured: False comment: Configuration discarded. diff: @@ -42,6 +42,7 @@ ntp server 10.10.10.1 ntp server 10.10.10.2 ntp server 10.10.10.3 +ntp server 172.17.17.1 ntp serve all ! result: True
Executing the same command now, but appending the test=True
option, Salt will load the configuration changes, determine the configuration difference, discard the changes and return the same output as before—the difference is that the comment
informs us that the changes made into the candidate configuration have been revoked.
Note
When executing in test mode (dry run), we do not apply any changes in the running configuration of the device. There are no risks: the changes are always loaded into the candidate configuration and transferred into the running configuration only when an explicit commit is invoked. During a dry run (test=True
) we do not commit.
If you need more changes, you can store them in a file and reference it using the absolute path (see Example 4-3).
Example 4-3. Loading static configuration from the file
$
sudo salt -G'vendor:arista'
net.load_config /path/to/file.cfg# output omitted
Loading Dynamic Changes
Loading static changes cannot be enough; very often you will need to configuredifferent properties depending on the device and its characteristics. The methodologies presented here are very important and will be referenced often in this book. At this point, you should focus mainly on learning the substance rather than the CLI usage.
For dynamic changes we will use a template engine, such as Jinja (discussed in “The Three Rules of Jinja”); see Example 4-4.
Example 4-4. Loading dynamic changes using a very basic template
$
sudo salt -G os:eos\
net.load_template\
template_source
=
'hostname {{ host }}'
\
host
=
'arista.lab'
device2: ---------- already_configured: False comment: diff: @@ -35,7 +35,7 @@ logging console emergencies logging host 192.168.0.1 ! -hostname edge01.bjm01 +hostname arista.lab ! result: True
Observe the name of the function is net.load_template
. Inside the
template_source
argument we have the Jinja template defined in-line; host
is the variable used inside the template.
One of the most important features in Salt is that you are able to use the grains
, pillar
, and the configuration options (opts
) inside the template (see Example 4-5).
Example 4-5. Using grains inside the inline template
$
sudo salt -G os:eos\
net.load_template\
template_source
=
'hostname {{ grains.model }}'
device2: ---------- already_configured: False comment: diff: @@ -35,7 +35,7 @@ logging console emergencies logging host 192.168.0.1 ! -hostname edge01.bjm01 +hostname DCS-7280SR-48C6-M-R ! result: True
Referencing grains data is very easy, we only need to use the reserved variable grains
followed by the grain name. Example 4-5 uses the model
grain, which provides the physical chassis model of the network device.
Grains prove extremely useful inside templates: they allow you to define one single template and use it across your entire network, regardless of the vendor. Example 4-6 shows a sample template.
Example 4-6. Cross-vendor Jinja template
{%
-set
router_vendor
=
grains.vendor
-%}
{%
-set
hostname
=
pillar.proxy.fqdn.replace
(
'as1234.net'
,
''
)
-%}
{%
-if
router_vendor
|
lower
==
'juniper'
%}
system {
host-name
{{
hostname
}}
lab;
}
{%
-elif
router_vendor
|
lower
in
[
'cisco'
,
'arista'
]
%}
hostname
{{
hostname
}}
lab
{%
-endif
%}
Using the vendor
, os
, and version
grains, we can determine the platform characteristics and generate the configuration accordingly, from one single template. Note that we also introduced the usage of another Salt property: pillar
. Using this we can access data from the pillar. In this example we configure the hostname of the device based on the fqdn
field from the proxy pillar, by removing the as1234.net
part.
Saving the contents from Example 4-6 to /etc/salt/templates/hostname.jinja, you can then execute the configuration load against all your devices and the template is smart enough to know what configuration to generate (see Example 4-7).
Example 4-7. Execute cross-vendor template
$
sudo salt device1\
net.load_template\
/etc/salt/templates/hostname.jinja device1: ---------- already_configured: False comment: diff:[
edit system]
- host-name edge01.flw01;
+ host-name r1.bbone.lab;
result: True
Having /etc/salt/templates
configured as one of the paths under file_roots
, we are able to render the template and load the generated configuration on the device, by executing: salt '*' net.load_template salt://templates/hostname.jinja
. Not only is this syntax easier to remember, but it is also a very good practice as we don’t rely on a specific environment setup.
Tip
We can even use remote templates: besides salt://
, we can equally use ftp://
, http://
, https://
, s3://
, or swift://
. The templates will be retrieved from the corresponding location, then rendered.
Another very useful feature is the debug mode. When working with more
complex templates, we can see the result of the template rendering by using the
debug
flag, as shown in Example 4-8.
Example 4-8. Using the debug mode
$
sudo salt device1 net.load_template\
salt://templates/hostname.jinjadebug
=
True device1: ---------- already_configured: False comment: diff:[
edit system]
- host-name edge01.flw01;
+ host-name r1.bbone.lab;
loaded_config: system{
host-name r1.bbone.lab;
}
result: True
Under the loaded_config
we can see the exact result of the template rendering, which is not necessarily identical to the configuration diff.
Tip
For debugging purposes, in Salt we can even use logging inside our templates. For example, the following line will log the message in the proxy log file (typically under /var/log/salt/proxy, unless configured elsewhere):
{
%-do
salt.log.debug(
'Get salted'
)
-%}
One of the most important features in Salt templating is reusability. We’ve seen the grain
and the pillar
variables, now let’s introduce the salt
variable. This allows you to call any execution function. While on the CLI we would execute salt device2 net.arp
, inside the template we can have {%- set arp_table = salt.net.arp() -%}
to load the output of the net.arp
execution function into the arp_table
Jinja variable, then manipulate it as needed.
Example 4-9. Cross-vendor template reusing Salt functions
{%
-set
route_output
=
salt.route.show
(
'0.0.0.0/0'
,
'static'
)
-%}
{%
-set
default_routes
=
route_output
[
'out'
]
-%}
{%
-if
not
default_routes
-%}
{# if no default route found in the table #}
{%
-if
grains.vendor
|
lower
==
'juniper'
-%}
routing-options {
static {
route 0.0.0.0/0 next-hop
{{
.def_nh
}}
;
}
}
{%
-elif
grains.os
|
lower
==
'iosxr'
-%}
{%
-set
def_nh
=
pillar.def_nh
%}
router static address-family ipv4 unicast 0.0.0.0/0
{{
def_nh
}}
{%
-endif
%}
{%
-endif
-%}
The Jinja template from Example 4-9 retrieves the default static routes from the RIB using the route.show
execution function. If the result is empty (no static routes found), it will generate the configuration for a static route to 0.0.0.0/0
using as next hop the value of the def_nh
field from the Pillar. And we can achieve this with just a couple of lines, covering two different types of platforms: Junos and IOS-XR.
Tip
Using the Salt advanced templating capabilities, we can write beautiful and more readable templates, by moving the complexity into the execution modules. There’s a tutorial showing how in the SaltStack documentation.
Get Network Automation at Scale now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.