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.
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).
$
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.
$
sudo salt -G os:eosnet.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.
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).
$
sudo salt -G'vendor:arista'
net.load_config /path/to/file.cfg# output omitted
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.
$
sudo salt -G os:eosnet.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).
$
sudo salt -G os:eosnet.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.
{%
-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).
$
sudo salt device1net.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.
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.
$
sudo salt device1 net.load_templatesalt://templates/hostname.jinja
debug
=
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.
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.
{%
-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.
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.
18.119.28.237