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.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.

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.