What You’ll Learn

What You’ll Need

Assumptions:

The Cisco Catalyst Center platform provides a feature-rich template editor interface, similar to some code editors that you may be familiar with. It provides features like real-time code hints, syntax checking and highlighting, variable customization and a template simulator, which allows you to test your templates without having to deploy them to a device. In software releases prior to v2.3.5, this interface was called Template Editor. In release v2.3.5 and later, the interface was renamed Template Hub, and it includes a significantly redesigned user interface.

Note: Although Cisco DNA Center is now called Cisco Catalyst Center, many of the screenshots shown in this tutorial still display the former name. The user interface shown is otherwise the same.

Regardless of which version you have installed, to find the templating editor interface, navigate to the Catalyst Center main menu and choose the Tools submenu:

template_hub_menu_location.png

The below sections cover all possible versions. Please scroll to the section that applies to your environment.


Template Hub

The Template Hub interface has a more complex navigation menu on the left, allowing you to filter the displayed templates based on various attributes.

template_hub_interface_navigation.png


Code Editor Options

While editing a template in the code editor (Template) tab, there are several options available along the upper menu bar. They include:

template_hub_code_editor_options.png


Save and Commit

To save and commit a template, you can use the corresponding buttons located at the bottom-right corner of the Templates and Variables tabs in the editor interface. When editing a template, your changes are not automatically saved, so you will be prompted to save or abandon them if you attempt to navigate away from the editor window.

Saving your template simply means that your changes are “staged.” Committing your template takes your staged changes and adds them to the version control database as a new template version. If this sounds a bit like the version control system Git, that’s because it is. Think of the Catalyst Center template database as a scaled-down version of Git.


Variables Editor Options

The Variables tab includes many options to customize the variables in your template. They include:

template_hub_variables_tab.png


Simulation

The Simulation tab allows you to test your template’s output with any input values that you specify, and it can even use any device from the Catalyst Center inventory to provide values for system bind variables. The template simulator can simulate the output of a template, but it cannot validate that the template output will actually work on the target device. That’s a very important bit of information to remember, so I’ll repeat it: The template simulator cannot validate whether your template will deploy successfully; it can only simulate what the output would look like. You will need to test and validate your templates carefully before deploying them into a production network.

  1. Click the Create Simulation button.
  2. Enter a name in the Simulation Name field.
  3. Choose an example target device from the inventory (only relevant for templates with system bind variables).
  4. Fill out variable values.
  5. Click the Run button.

template_hub_simulation_tab.png

After you click the Run button, a Results pane will appear on the right. This pane can be expanded to full size by clicking the blue double-arrow button in the upper-right corner of the Results pane.

template_hub_simulation_results.png


Template Editor

The Template Editor interface has a simple “directory tree” navigation menu on the left, which displays all the template projects and allows you to expand them to view the templates that they contain.

template_editor_interface_navigation.png


Code Editor Options

While editing a template in the code editor (Template) tab, there are several options available along the upper menu bar. They include:

template_editor_code_editor_options.png


Form Editor Options

The Form Editor interface includes many options to customize the variables in your template. They include:

template_editor_variable_settings.png


Simulation

The Simulation interface allows you to test your template’s output with any input values that you specify, and it can even use any device from the Catalyst Center inventory to provide values for system bind variables. The template simulator can simulate the output of a template, but it cannot validate that the template output will actually work on the target device. That’s a very important bit of information to remember, so I’ll repeat it: The template simulator cannot validate whether your template will deploy successfully; it can only simulate what the output would look like. You will need to test and validate your templates carefully before deploying them into a production network.

  1. Click the Create Simulation button.

    1. (Only for system bind variables): If your template contains system bind variables, you will see the following text appear directly above the Simulation Name field. You must click the here hyperlink and select a target device from the inventory.

    template_editor_simulator_with_bind_variable.png

  2. Optionally, you can enter a name in the Simulation Name field.

  3. Fill out variable values.

  4. Click the Run button.

After you click the Run button, a Results pane will appear on the right. This pane can be expanded by hovering over the left edge until the mouse becomes a double-arrow slider, and then dragging the edge toward the left.

template_editor_simulation_interface.png


Code Editor

When you click a template’s name in Template Hub or Template Editor, it will open into the built-in Code Editor. Templates are really just device configuration snippets—or even entire configuration files!—so they could be as simple as this:

hostname test-switch.cisco.com
interface Mgmt0
 ip address 192.168.1.1 255.255.255.0
 no shutdown

Note: This template doesn’t contain any Jinja code, has no variables, and will simply send these static configuration commands to a device.

Or templates can be as complex as this:

no service pad
no platform punt-keepalive disable-kernel-core
no ntp allow mode control
service timestamps debug datetime msec
service timestamps log datetime localtime show-timezone
diagnostic bootup level minimal
transceiver type all
 monitoring
system mtu {{ system_mtu }}
!
{%- set gmt_offset_list = gmt_offset | split(":") %}
{% if gmt_offset_list | length == 1 %}
clock timezone {{ timezone | upper }} {{ gmt_offset_list[0] }} 0
{% else %}
clock timezone {{ timezone | upper }} {{ gmt_offset_list[0] }} {{ gmt_offset_list[1] | int }}
{% endif -%}
!
{%- if enable_dst == 'yes' %}
{% if timezone | lower in ["est", "cst", "mst", "pst"] -%}
clock summer-time {{ timezone[:1] | upper }}D{{ timezone[2:] | upper }} recurring
{% elif timezone | lower in ["akst", "hst"] %}
clock summer-time {{ timezone[:-2] | upper }}DT recurring
{% endif -%}
{% endif -%}
!
{% if vtp_mode != "" %}
vtp mode {{ vtp_mode }}
{% endif %}
{% if vtp_domain != "" %}
vtp domain {{ vtp_domain }}
{% endif %}
!
{% if rsa_key_modulus %}
crypto key generate rsa general-keys modulus {{ rsa_key_modulus }}
{% endif %}
!
{%- set exec_timeout = exec_timeout | split(":") %}
line console 0
 logging synchronous
{% if exec_timeout | length == 2 %}
 exec-timeout {{ exec_timeout[0] | int }} {{ exec_timeout[1] | int }}
{% endif %}
line vty 0 31
 logging synchronous
{% if exec_timeout | length == 2 %}
 exec-timeout {{ exec_timeout[0] | int }} {{ exec_timeout[1] | int }}
{% endif %}

Note: This template makes use of some basic and advanced Jinja features, like filters, set statements, and conditionals. It also leverages features from the Python programming language, like string slicing and list indexes.

The template Code Editor provides you with a basic interface to write or edit your templates, and it adds helpful functionality like syntax highlighting, code hints and auto-fill suggestions. While editing your templates, be sure to save them frequently to prevent your work from being lost! You can commit your template less frequently—normally, only when it is complete and you have tested it in the simulator.

What Is Jinja?

Jinja is a templating language that is used to “mark up” plaintext, giving the Jinja templating engine special instructions about how it should render the output. Basically, it’s a Unicode markup language–similar to the Markdown language used to write this tutorial. Jinja templates can be as simple as a text file with no instructions or special language at all; the result would be that the output looks exactly like the input (the text file). However, Jinja was written in the Python programming language, so it inherits many of Python’s capabilities. You can use if/else statements, for loops, and many other programming constructs to control the output. To summarize, it’s a mixture of plaintext, special characters, and some programming logic.

The syntax documentation for the Jinja templating language can be found here: https://jinja.palletsprojects.com/en/latest/templates/.

When Jinja was developed, it was primarily used to dynamically generate HTML web pages, so its documentation was written with that purpose in mind. It can be difficult to understand the documentation and adapt it to use in the Catalyst Center templating interface, which is why we’ve developed this tutorial. It’s also important to note that not every capability or feature listed in the documentation has been implemented in Catalyst Center, so there are some things that simply won’t work or aren’t available.


Delimiters

Jinja code is set apart from any plaintext in a template (which is simply reprinted to the output) by using one of three types of delimiters to surround it:


Variables

The true power of Jinja templates is that they can leverage variables to store and retrieve values that are dynamic–meaning that they can change. It’s pretty easy to imagine a Cisco configuration file as a template. Many of the commands that you’d use would remain unchanged from one device to the next, but some commands will contain a value that is subject to change, like an IP address or hostname. In place of those dynamic values, we could substitute a named variable and then ask the template’s user to provide us with a value for it.

Template:

interface {{ management_interface }}
 ip address {{ management_ip }} {{ management_subnet_mask }}
 no shutdown

We can define new variables by giving them a unique name (one that hasn’t already been used in the template) and placing them inside a Jinja expression. This causes two things to happen:

  1. When running the template, the user is asked for a value for each unique variable.
  2. Jinja will print the output of the template, substituting in the user’s values in place of each variable.

Example values:

Output:

interface GigabitEthernet0/0
 ip address 192.168.1.1 255.255.255.0
 no shutdown

Sometimes, we might want to set a variable that is equal to a value that never changes. We can do that by using the special statement keyword set:

{% set management_interface = "GigabitEthernet0/0" %}
interface {{ management_interface }}
 ip address {{ management_ip }} {{ management_subnet_mask }}
 no shutdown

Now we’re no longer asking the user for the management interface name because we’ve hardcoded it into the template.

Another way to use the set keyword is to create one variable and set it equal to the value of another:

{% set mgmt_int = management_interface %}
interface {{ mgmt_int }}
 ip address {{ management_ip }} {{ management_subnet_mask }}
 no shutdown

If you think about it, this is redundant effort—we’re creating two variables to store one value—but it still works. In this example, the user is prompted for a value for management_interface. However, because we’ve copied that value into a new variable named mgmt_int, we can use that shorter name elsewhere in our template.

Special Syntax

In addition to the three commonly used Jinja delimiters (statements, expressions, and comments), there are also some special syntax tools that you can use in your templates to handle very specific scenarios.

Escaping Reserved Characters

Occasionally, you may need to use a special reserved character, or set of characters, inside a Jinja template. This happens most often when you need to print out a random string of characters, like a hash value, which coincidentally includes a set of special reserved characters. For example:

This is my example hash: 7bf2fcc8072cd12094fa74fa822c3570f6686ca{%04704f8bc5b95da98560a6606

The Jinja template above may cause an error because, in the middle of that hash value, we have the reserved characters {% next to each other. Jinja will compile this template, and when it reaches those characters, it will think you’re attempting to start a statement—which could cause it to fail and exit, or at least cause some unintended output.

There are two methods that you can use to “escape” any special characters that might pop up in your templates:

  1. Inline escaping: Surround the special characters in a Jinja expression and quotation marks ({{"{%"}}), rendering them as plaintext.

    •  This is my example hash 7bf2fcc8072cd12094fa74fa822c3570f6686ca{{"{%"}}04704f8bc5b95da98560a6606
      
  2. Raw blocks: Surround one or more lines in special raw block tags ({% raw %}, {% endraw %}), which render anything inside them as plaintext. (This also works well when you might need to include actual example Jinja code in your output.)

    •  {% raw %}
       This is my example hash: 7bf2fcc8072cd12094fa74fa822c3570f6686ca{{"{%"}}04704f8bc5b95da98560a6606
      
       And here's some Jinja code:
       {% set my_variable = "my value" %}
       {% endraw %}
      

Controlling Whitespace

Because Jinja originally was used to render HTML webpages dynamically, controlling whitespace in the output was very important. There’s a whole section in the Jinja documentation devoted to this topic, and there are even special configurations for the language that can affect how the output is rendered. For our purposes in Catalyst Center, we’re really just interested in controlling any blank lines that are printed to the output.

In Jinja templates, any comments that you add will not print to the output; however, they will cause a blank line of whitespace to be printed. For example:

Template:

{# My first comment #}
{% set my_variable = "my value" %}
{{ my_variable }}

Output:

 
my value

Notice the blank line preceding the output of the expression {{ my_variable }}? That’s coming from the comment that we put on the first line. Now, you may have noticed in the earlier “Code Editor” topic of this tutorial that some of the example Jinja statements had a dash character (-) immediately after the opening statement tag ({%). By putting a dash character right after the opening tag of a Jinja statement, you can instruct the Jinja templating engine to remove a single preceding line of whitespace.

Template:

{# My first comment #}
{%- set my_variable = "my value" %}
{{ my_variable }}

Output:

my value

You can also use the same syntax with the closing statement tag (-%}) to eliminate a single trailing line of whitespace.

Another good use for whitespace controls is inside a “for loop.” (Don’t worry, we’ll talk about for loops in detail during Part 2 of this tutorial.) When a for loop runs through each iteration, it prints a “newline” character at the end, causing the next iteration to print its output to a new line below the previous one. For example:

Template:

{% for i in range(0,10) %}
{{ i }}
{% endfor %}

Output:

0
1
2
3
4
5
6
7
8
9

If we wanted each of those numbers to print out next to each other, on the same line, we could do that with one simple change:

Template:

{% for i in range(0,10) %}
{{ i }}
{%- endfor %}

Output:

0123456789

One final strategy that we can use is a multiline comment. As we mentioned earlier, these whitespace control characters will only remove a single line of whitespace, either before or after a Jinja statement. That means just one comment line can be affected. But suppose that you have multiple lines of comments, right before a Jinja statement?

Template:

{# Comment Line 1 #}
{# Comment Line 2 #}
{# Comment Line 3 #}
{%- for i in range(0,10) %}
{{ i }}
{%- endfor %}

Output:

 
 
0123456789

What we could do instead is combine all three of those comment lines into a single multiline comment by surrounding the whole block with an opening and closing comment tag:

Template:

{# 
Comment Line 1
Comment Line 2
Comment Line 3
#}
{%- for i in range(0,10) %}
{{ i }}
{%- endfor %}

Output:

0123456789

There are some other interesting things that you can do with whitespace controls in Jinja. However, not all the documented methods will actually work in the Catalyst Center templating interface. As always, the best way to learn about templating in Catalyst Center is to experiment with them using the template simulator function.

Catalyst Center Special Keywords

In addition to the out-of-the-box syntax of the Jinja templating language, the Catalyst Center templating and compliance engines can make use of some additional special keywords to account for special circumstances. These are detailed in Cisco product documentation here and here, but we’ll also explain them below. Each of these special keyword tags can be used multiple times in a Catalyst Center template.

Enable Mode

When template-generated device configurations are deployed to a device, all the Cisco IOS and IOS XE commands are run in global configuration mode (commonly referred to as the config terminal prompt: Router(config)#). This is the default behavior of the Catalyst Center templating engine. However, sometimes you need to run commands outside of this context in the privileged EXEC (aka enable) mode (Router#). If your template contains configuration commands that must be run in this context, you can simply surround those command lines, above and below, with the following tags:

Template:

#MODE_ENABLE
enable_mode_cli_command
enable_mode_cli_command
#MODE_END_ENABLE

Interactive Commands

Some Cisco IOS and IOS XE commands will respond to the user with an interactive prompt, normally in the form of a confirmation dialog requiring you to respond with y or n. This is most common with commands that result in destructive and irreversible changes, like formatting Flash. (Please don’t do that in your templates!)

Catalyst Center will also be prompted for these interactive responses because it sends commands to a device over a Secure Shell (SSH) session. To account for this situation, you can use the #INTERACTIVE and #ENDS_INTERACTIVE tags, along with some additional syntax for recognizing and responding to prompts. Here’s an example:

Template:

#INTERACTIVE
no crypto pki trustpoint DNAC-CA<IQ>yes/no<R>yes
#ENDS_INTERACTIVE

Inside the opening and closing INTERACTIVE tags, you will also need to add the following tags:


Multiline Commands

Some Cisco IOS and IOS-XE commands require multiple lines, or the command may be so long that you want to wrap it around to new lines for better readability. To do this, use the <MLTCMD> and </MLTCMD> tags, placed at the beginning and end of your multiline command. For example:

Template:

<MLTCMD>command_line_1
command_line_2
command_line_3</MLTCMD>

Note that the tags are included inline with the multiline command rather than above and below it. Also, the <MLTCMD> and </MLTCMD> tags cannot be placed on the same line of text; they must be on separate lines.


Ignore Compliance Checks

The Catalyst Center configuration compliance engine performs regular checks of every managed device’s current running configuration to ensure that it matches the resulting templated configuration that Catalyst Center most recently applied to the device. These configurations are tracked in a database, and when a configuration mismatch is found, an alert is created. Some Cisco IOS and IOS XE commands can trigger false alarms in the compliance engine because the actual configuration line changes once it is applied to the device (such as a plaintext password being hashed).

Another common problem arises when a template is used to remove a configuration line from a device, using the no version of the command, such as no interface Loopback0. When such a command is sent, the device simply deletes that item from the running configuration; there is no lingering no interface Loopback0 line in the device configuration.

These types of situations may trigger a compliance alert that is a false positive. As a workaround, you can surround commands that may cause compliance alerts with the ! @start_ignore_compliance and ! @end_ignore_compliance tags (see documentation). For example:

Template:

! @start_ignore_compliance
no interface Loopback0
! @end_ignore_compliance

Lastly, to avoid other possible sources of false-positive compliance alerts, make sure that you follow these recommendations (along with any others mentioned in the documentation linked above):

Data Types

Jinja can understand all the basic data types that are native to Python. These data types are called “literals”:

Working with Strings

You might be thinking, “What more could I learn about a string? It’s just text”—and you’d be right. Strings really are just collections of text characters; they’re pretty simple. However, scripting and automation revolve around interpreting plaintext and possibly taking some automatic action. If you think about it, we humans communicate in plaintext language, and as a result, we’ve developed our computer programs to interact with text-based language. So, yes, strings are pretty important.

Let’s start by expanding your understanding of what string actually is. A string is very similar to a list (aka array) data type, which we mentioned in the previous topic. Both a string and a list are a “collection” of one or more indexed items. In the case of a string, each index contains a single text character. (When we say “text,” we really mean any Unicode character.) Just like lists, each indexed character has an index number starting at 0 and going upward. Here’s a simple example that illustrates this:

Template:

{% set text = "Hello World!" %}
Here we're selecting a single character:
{{ text[0:1] }}
{{ text[6:7] }}
Here we're specifying a start and end index number for a range of characters: {{ text[0:5] }}
Here we're only giving an end index number: {{ text[:5] }}
Here we're only giving a start index number: {{ text[6:] }}

Note: The type of syntax shown here, where a set of square brackets ([ ]) containing some argument follows a variable, is called subscript.

Output:

Here we're selecting a single character:
H
W
Here we're specifying a start and end index number for a range of characters: Hello
Here we're only giving an end index number: Hello
Here we're only giving a start index number: World!

In the example above, we created a string type variable named text, and we’ve filled it with 12 characters. Next, we’ve instructed Jinja to print out the contents of the variable text. However, we’ve added a set of square brackets ([ ]) immediately after the variable name, and we’ve entered an index number range inside those brackets. (We use a : character to separate those range numbers.) That range represents the index number to start at, followed by the index number to end before.

This is important to remember: In Python, Jinja, and most programming languages, the ending number of a “range” command is not inclusive, meaning that the range will continue up to, but not include, the ending number.

On lines 2 and 3 in the template above, we’ve told Jinja to print out only the character that exists at the beginning index number that we provided. In this example, index 0 contains H, and index 6 contains W.

Because we’re using ranges of index numbers, we can also make Jinja print out more than one character by specifying a beginning and ending range that are more than one number apart. Also, we don’t have to specify a starting or ending number. If we leave the starting number blank ([:5]), we’ll get every character in the string starting from index 0 and continuing up to (but not including) the ending index number that we specified. The same thing happens if we omit the ending index number ([6:]); we get all the characters from the starting index number until the end of the string is reached. What we’ve just demonstrated is called string slicing.


Formatting String Output: Filters and Methods

String type variables have many “methods” (built-in features) available for you to use, and Jinja also has many built-in filters that can perform some of the same actions. Their purpose is to either provide you with information about the variable’s contents or to copy the contents of a variable and modify them in some way. (Important note: Filters and methods do not directly modify a variable’s contents.) For instance:

Template:

{% set text = "Hello World!" %}
This is an example of a "method": {{ text.length() }}
This is an example of a "Filter": {{ text | length }}

Output:

This is an example of a "method": 12
This is an example of a "Filter": 12

As you can see from the example above, a “method” follows a period (.) immediately after the name of the variable. That’s because the method belongs to the variable; it’s a built-in feature of a string data type. On the other hand, a Jinja “filter” is placed after a pipe symbol (|); that’s because we’re passing the contents of the variable into the filter that is provided by Jinja. There are way too many methods and Jinja filters available for us to explain each of them in this tutorial, so instead, we’ll show you how to use some of the most useful ones.

Also, this can be become rather confusing to users if we dive too deep into it. For instance, although we know that Jinja was written using the Python programming language, the .length() method that you see above is actually coming from the Java programming language, because Java was used to create the templating interface in Catalyst Center. Where these features come from is less important than what they can do.

Let’s look at some of the most useful filters available to us for working with strings:

Template:

{% set text_2 = "hello world!" %}
{% set text_3 = " HELLO WORLD!" %}
Capitalize: {{ text_2 | capitalize }}
Title: {{ text_2 | title }}
Upper: {{ text_2 | upper }}
Lower: {{ text_3 | lower }}
Trim: {{ text_3 | trim }}
Length: {{ text_3 | length }}
Replace: {{ text_2 | replace("hello", "goodbye") }}

Output:

Capitalize: Hello world!
Title: Hello World!
Upper: HELLO WORLD!
Lower:  hello world!
Trim: HELLO WORLD!
Length: 13
Replace: goodbye world!

Next, let’s look at some of the most useful methods that are available to us for working with strings.

Note: If you’re interested in which methods are available for a string data type in Java, you can view them here: https://docs.oracle.com/javase/8/docs/api/java/lang/String.html#method.summary.

Template:

{% set text_2 = "hello world!" %}
{% set text_3 = " HELLO WORLD!" %}
{% set text_4 = "GigabitEthernet1/0/12" %}
contains: {{ text_2.contains("!") }}
indexOf: {{ text_2.indexOf("o", 0) }}
replaceAll (with Regex): {{ text_4.replaceAll("(\\/[1-2][0-9])$", "\\/20") }}
replaceAll (without Regex): {{ text_4.replaceAll("1", "2") }}
matches (with Regex): {{ text_4.matches("^Giga.+[1-2]\\/0\\/[0-9]{2}$") }}

Output:

contains: true
indexOf: 4
replaceAll (with Regex): GigabitEthernet1/0/20
replaceAll (without Regex): GigabitEthernet2/0/22
matches (with Regex): true

As we mentioned earlier, there are so many filters and methods available to use that we cannot cover them all in this tutorial. The best way to learn about what a filter or a method does is to experiment with it—and that’s where the template simulator becomes incredibly valuable. We encourage you to read the Jinja documentation and explore the Java object class documentation for each variable type, and then play around with them to see what happens.

Finally, an important reminder: The functionality of the Catalyst Center templating engine is subject to change with any new software release, so it is possible that some things explained in this tutorial may no longer work after an upgrade.

Working with Lists

Jinja variables of the type string, integer, float, and Boolean are really easy to work with; you simply set the value once and retrieve it later. However, the more complex literal data types of list, tuple, and dictionary are a little bit more challenging to work with. At this point, it’s helpful to have a basic understanding of a programming language like Python, but it’s not necessary.

Lists, tuples, and dictionaries are all basically containers that store one or more items inside them. In the case of lists and tuples, to retrieve those items, you either have to know exactly where an item is stored, or you have to go searching for it.

Let’s start with an example of how to create a list and then retrieve something from it. You can follow along by building the template in your own Catalyst Center as we go through this topic.

Note: This topic applies to tuples as well, because tuples are just a special kind of list. You can substitute parentheses in place of the square brackets around the list in the following example to convert it to a tuple. Everything else remains the same.

Let’s start by creating a list of fruits:

{% set my_fruit_list = ["apple", "banana", "pear", "peach", "watermelon"] %}

Now, suppose that we want to retrieve the value peach from this list. How would we do that? (Remember that list index numbers start at 0 and count upward.) In this example, we can see pretty easily that peach is stored in the fourth position, and if we’re starting at 0, then its index value should be 3.

Template:

{#        Index Numbers:   0         1        2       3          4        #}
{%- set my_fruit_list = ["apple", "banana", "pear", "peach", "watermelon"] %}
{{ my_fruit_list[3] }}

Output:

peach

Note: Notice that we created an expression above ({{ my_fruit_list[3] }}), placed the name of our list inside it, and immediately followed it with a pair of square brackets containing the Index number. Remember this syntax; this is how we reference a specific index value inside a list or tuple. You’ll also see a variation on this when we talk about dictionaries.

There, that was easy! But what if you didn’t know exactly where the value peach was stored in this list? For instance, maybe the list was automatically generated somewhere else, and you’re just receiving and storing it. How could you locate peach if you didn’t know exactly where it was stored? Lucky for you, complex variables like lists, tuples, and dictionaries are known as “iterables,” which means that you can iterate (loop) over them.

For this demonstration, we’re going to stray a bit into concepts that we haven’t covered yet—specifically, for loops and if/else conditionals. We’ll cover them in more detail in Part 2 of this tutorial. For now, we’ll keep the example very simple.

Template:

{% set my_fruit_list = ["apple", "banana", "pear", "peach", "watermelon"] %}
{% for fruit in my_fruit_list %}
{% if fruit == "peach" %}
I FOUND THE {{ fruit }}!
{% endif %}
{% endfor %}

Output:

I FOUND THE peach!

What we’ve done in the example above is set up a for loop. This loop will run over and over again, starting at index 0 of my_fruit_list and continuing until it reaches the last index number, at which point it will exit. Now, here’s where things get tricky: If we start at index 0 and continue upward, how will we know when we’ve located peach? Well, we have to give our template a little intelligence. We have to ask it to check every item that it finds in the list to look for a match, and in this case, we’re asking it to match each item against the string peach. If it finds a string during one of its iterations that matches peach (in all lowercase letters, because Jinja is case-sensitive), then it should take some action. In our template, the action that it should take is to print out the following text:

I FOUND THE !

In the middle of that text, it should print out the value of the current item that the for loop is working on (its current iteration). In this case:

I FOUND THE peach!

Editing Lists

In the previous topic, we created a list (or a tuple) and obtained a value from what was originally stored in it. What if were to add a fruit to our list–and maybe remove one from it as well? We can do that, in a limited sense, by making use of a few built-in methods that come along with the data type list. So, let’s turn our fruit list into a shopping list!

We’ll start by recreating our fruit list from the previous topic:

{% set my_fruit_list = ["apple", "banana", "pear", "peach", "watermelon"] %}
Here's our current fruit list: {{ my_fruit_list }}

Let’s remove "peach" from that list. In this example, we’re going to use a new statement keyword that we haven’t seen before: do. The do keyword tells Jinja to execute some logical command without attempting to capture the output of that command.

Template:

{% set my_fruit_list = ["apple", "banana", "pear", "peach", "watermelon"] %}
Here's our current fruit list: {{ my_fruit_list }}

{# Here's a new keyword we haven't seen: "do" #}
{%- do my_fruit_list.remove(3) %}
There, we removed "peach": {{ my_fruit_list }}

Output:

Here's our current fruit list: [apple, banana, pear, peach, watermelon]

There, we removed "peach": [apple, banana, pear, watermelon]

Next, let’s add lemon and apricot to our list. There are two ways of adding items to a list:

  1. Append them to the end of the list.
  2. Insert them at any index that we choose.

So let’s do both.

Template:

{% set my_fruit_list = ["apple", "banana", "pear", "peach", "watermelon"] %}
Here's our current fruit list: {{ my_fruit_list }}

{# Here's a new keyword we haven't seen: "do" #}
{%- do my_fruit_list.remove(3) %}
There, we removed "peach": {{ my_fruit_list }}

{# Appending a fruit to the end of our List. #}
{%- do my_fruit_list.append("lemon") %}
Now we've appended a fruit to the end: {{ my_fruit_list }}

{# Inserting a fruit in the middle of our list. #}
{%- do my_fruit_list.insert(2, "apricot") %}
And here we've inserted a fruit in the middle: {{ my_fruit_list }}

Output:

Here's our current fruit list: [apple, banana, pear, peach, watermelon]

There, we removed "peach": [apple, banana, pear, watermelon]

Now we've appended a fruit to the end: [apple, banana, pear, watermelon, lemon]

And here we've inserted a fruit in the middle: [apple, banana, apricot, pear, watermelon, lemon]

Let’s wrap up this topic with a pop quiz: What do you think would happen if we tried any of these editing methods with a tuple instead of a list?

Because you were paying such close attention earlier in the “Data Types” topic, you knew that the correct answer was B—right? The behavior of the Catalyst Center templating engine may change as new versions are rolled out, so it’s safer to say something you didn’t intend would happen. Most likely, there would be nothing displayed in the output, but the reason will always remain the same: Tuples are immutable objects, which means that once they are created, they cannot be changed.

Working with Dictionaries

Dictionaries are amazing data structures—the greatest thing since the HTTP 418 response code! OK, maybe they’re not that amazing, but they are incredibly versatile and much more efficient to use than standard lists or tuples. The power of dictionaries comes from their usage of uniquely named keys, which allow you to store just about anything as a key’s value and retrieve it immediately by simply calling upon the key name. No looping necessary (although you can loop through a dictionary, and we’ll show you an interesting use case for that).

Dictionaries are more useful and efficient than lists or tuples because as long as you know the name of the index where an item is stored, you can immediately retrieve it–no need to remember an arbitrary number. Want a more networking-related analogy? Dictionaries are to lists and tuples what a Domain Name System (DNS) is to IP addresses on the internet. It’s easier to remember a website name than its associated public IP address.

Let’s start by creating a template with a simple dictionary. This time, we’ll use a serious example—one that would be immediately useful in a Catalyst Center template. Let’s create a VLAN name database:

{% set vlan_db = {
  "1": "DO NOT USE",
  "10": "Management VLAN",
  "15": "Printer VLAN",
  "20": "Voice VLAN",
  "123": "Guest VLAN",
  "777": "POS VLAN",
  "999": "Blackhole VLAN"
  } 
%}
{% set create_vlan_str = create_vlan | string %}

In this example, we’re using strings as the dictionary keys because we’re required to by Catalyst Center, but we should really make sure that template users only enter numerical digits in the variable that we’ve created. To do that, we’re going to make use of the Data Type options on the Variable settings page in Template Hub or Template Editor—the name and location varies between these two versions—and we’ll configure our input variable create_vlan as an integer.

Template Hub: template_hub_integer_variable.png

Because we’re asking the user for an integer value, but our dictionary keys are stored as strings, we also need to perform a data type conversion so that we can match up the user’s input with its corresponding key in the dictionary. To do this, we’re making use of a Jinja filter called string to convert our integer value into a string. Also, you’ll notice that we’re storing this converted value in a new variable called create_vlan_str because we’ll need access to that value later in our template.

So far, here’s what we’ve accomplished:

There’s a lot going on in that small amount of Jinja code. We’re not done yet, though. Now that we’ve created our VLAN database and our user input variable, the next step is to do something useful with this information. The purpose of this whole exercise is to create a Catalyst Center Day-N template, allowing us to prompt a user for which VLAN they want to create while we (the administrators) supply the predefined VLAN names. We need to ensure that we follow our configuration standards, after all.

Let’s add some additional logic to our template. First, we need to take the VLAN ID value that is provided and execute the command:

vlan <vlan_id>

Then, we need to check if that VLAN exists in our VLAN database. If it does, we should use the VLAN name that is provided in the database; if not, we should probably create a generic VLAN name to use instead.

So how would we accomplish that? Did someone say “if/else conditionals”?

{% set vlan_db = {
  "1": "DO NOT USE",
  "10": "Management VLAN",
  "15": "Printer VLAN",
  "20": "Voice VLAN",
  "123": "Guest VLAN",
  "777": "POS VLAN",
  "999": "Blackhole VLAN"
  } 
%}
{% set create_vlan_str = create_vlan | string %}
vlan {{ create_vlan }}
 {% if vlan_db[create_vlan_str] is defined %}
 name {{ vlan_db[create_vlan_str] }}
 {% else %}
 name VLAN_{{ create_vlan }}
 {% endif %}

In the opening if statement, {% if vlan_db[create_vlan_str] is defined %}, there are several things going on that may not be obvious at first:

  1. We are calling the vlan_db variable, followed by square brackets ([ ]), just like we did with a list earlier, which contain … what, exactly?
    1. Normally, we would enter the string value for the name of a key, like "777". However, in this template, we’re actually asking the user for that key name.
    2. That key name is being converted from an integer to a string and then stored in the variable create_vlan_str, so we can substitute in the variable name in place of a hardcoded key name.
  2. Next, we perform a “test” along with the is operator keyword. We’ll talk more about Jinja tests in Part 2 of this tutorial, but for now, just know that the is operator checks if the value on the left passes the test on the right.
    1. In this case, the value on the right is a Jinja test named defined. It simply checks if the variable or object passed to it exists.
    2. In this test, we’re asking Jinja: Does the key provided by the user exist in the dictionary variable named vlan_db?
    3. Note: All tests and comparison operations in Jinja (including if/else conditionals) return a Boolean value: true or false.
  3. If the result of the opening if conditional statement is true, then the template will print out name <value_corresponding_to_input_key>.
    1. For example, if the user entered 777 in the template’s input variable, this if conditional would print out name POS VLAN. If the user entered 1, it would print out name DO NOT USE.
  4. If the result of the opening if conditional statement is false (meaning the user entered a VLAN that wasn’t in our vlan_db), the template would print out name VLAN_<input_value>
    1. For example, if the user entered 500, it would print out name VLAN_500.

Here’s an example of what this template would generate:

User input: 15

Output:

vlan 15
 name Printer VLAN

It’s not very efficient to write 17 lines of code and get only 2 lines of output for your effort. We should add a little more logic to this template to make it do some real work for us. Why not use a for loop to create lots of VLANs?

Let’s keep our existing code in place because it’s a good, clean example. (You would want to delete or comment out redundant code for production use.) We’ll just add some new code below it:


{# Code above is omitted #}
{% set create_vlan_list = create_vlan_list | split(",") %}
{% for vlan in create_vlan_list %}
{% set vlan_str = vlan | string %}
vlan {{ vlan }}
{% if vlan_db[vlan_str] is defined %}
 name {{ vlan_db[vlan_str] }}
 {% else %}
 name VLAN_{{ vlan }}
 {% endif %}
{% endfor %}

Note: Catalyst Center may recognize that your create_vlan_list variable is being used in a for loop and make the decision to configure it as a string variable with a multiselect input type. If so, you will need to go the Variables tab (or Form Editor if you’re using the older Template Editor interface), select the create_vlan_list variable, and change it to String data type with Text Field as the input type.

In this new example, we’re taking the same basic if/else statement that we used above and enclosing it inside a for loop. We’ve created unique variable names so that we don’t clash with the ones that already exist in the template. Also, we’re using a new filter that we haven’t seen before: split(","). The split filter is used to slice a string into individual segments each time that it finds a delimiter—in this case, a comma (,).

What we get as output from the split filter is a list object, just like we’ve used in earlier topics. What’s happening here is that we’re asking the user to give us a comma-separated string as input for the create_vlan_list variable, then we break apart that string at every comma (,) and put each segment into a list, and finally return that list as output. We’re creating a list from a string.

Next, we use a for loop to iterate through this new list that we’ve created. For every item that we find in that list, we’ll perform the same actions that we did in the previous version of our template: We create a VLAN, check to see if it exists in our vlan_db dictionary, and based on that result, we either give the VLAN a name from vlan_db or make one up in real time.

Here’s an example of what the output could look like, given some random input values:

create_vlan: 15

create_vlan_list: 1,2,5,20,50,777,999

Output:

vlan 15
 name Printer VLAN


vlan 1
 name DO NOT USE
vlan 2
 name VLAN_2
vlan 5
 name VLAN_5
vlan 20
 name Voice VLAN
vlan 50
 name VLAN_50
vlan 777
 name POS VLAN
vlan 999
 name Blackhole VLAN

Editing Dictionaries

Let’s take a look at some methods of editing the contents of a dictionary after we’ve created it. Just like in the list example earlier, there may be scenarios where you’ll want to add or delete something from a dictionary. It’s actually a little easier with dictionaries than it is with lists because there’s really no strict order to the contents of a dictionary. Keys and values aren’t stored in a serial numeric order; their position isn’t important because we can recall any value by simply asking for the key that it’s stored in.

We’ll start by reviewing how you can access values stored in a dictionary. The same syntax is used for recalling dictionary values that we would use for recalling an indexed item in a List: my_dictionary["my_key"]. This is referred to as the subscript notation. There is also one new method of recalling a value from a dictionary, which isn’t available in a list: my_dictionary.my_key. Either method will work for recalling a dictionary value.

Template:

{% set vlan_db = {
  "1": "DO NOT USE",
  "10": "Management VLAN",
  "15": "Printer VLAN",
  "20": "Voice VLAN",
  "123": "Guest VLAN",
  "777": "POS VLAN",
  "999": "Blackhole VLAN"
  } 
%}
{{ vlan_db["10"] }}
{{ vlan_db.10 }}

Output:

Management VLAN
Management VLAN

Suppose that we want to add some VLANs to the vlan_db dictionary. We can do that in much the same way as editing a list, like we did in the previous topic. However, we’ll need to specify the key name and the value instead of an index number and the value.

We’ll start with a fresh copy of the template in our previous topic’s example, removing the redundant parts:

{% set vlan_db = {
  "1": "DO NOT USE",
  "10": "Management VLAN",
  "15": "Printer VLAN",
  "20": "Voice VLAN",
  "123": "Guest VLAN",
  "777": "POS VLAN",
  "999": "Blackhole VLAN"
  } 
%}

{# Loop through List and create VLANS #}
{%- set create_vlan_list = create_vlan_list | split(",") %}
{% for vlan in create_vlan_list %}
{% set vlan_str = vlan | string %}
vlan {{ vlan }}
{% if vlan_db[vlan_str] is defined %}
 name {{ vlan_db[vlan_str] }}
 {% else %}
 name VLAN_{{ vlan }}
 {% endif %}
{% endfor %}

Now let’s add some new VLANs to our vlan_db dictionary. Unfortunately, we’ll need to hardcode this into the template rather than doing something cool like prompting the user for the new VLAN ID and name. While there may be ways to make that work, the process would require several steps and data type conversions, which would quickly become difficult to understand. For now, we’ll just say that it’s not feasible to dynamically update a dictionary based on user input into a template.

{# Code above omitted #}
{% do vlan_db.update({"600": "Warehouse VLAN", "620": "Warehouse Voice VLAN"}) %}
{# Code below omitted #}

Pay close attention to the do statement and how it was written above. We’re using a dictionary method called update(). (A “method” is basically an added feature for a data type.)

Now, unlike the list variables that we worked with before, dictionaries can be a little pickier; the update() method requires us to provide a brand new dictionary inside its parentheses. That dictionary can contain one or more key-value pairs (using a colon to separate keys and values, of course), but it must be contained inside a set of curly braces ({ }). The update() method then merges the existing dictionary (vlan_db) with the new dictionary that you just provided, giving us this end result:

{
  "1": "DO NOT USE",
  "10": "Management VLAN",
  "15": "Printer VLAN",
  "20": "Voice VLAN",
  "123": "Guest VLAN",
  "777": "POS VLAN",
  "999": "Blackhole VLAN",
  "600": "Warehouse VLAN",
  "620": "Warehouse Voice VLAN"
}

Next, we could change an existing VLAN’s name, and we could remove one entirely from the dictionary:

{# Code above omitted #}
{% do vlan_db.update({"600": "Warehouse VLAN", "620": "Warehouse Voice VLAN"}) %}
{% do vlan_db.replace("1", "UNUSED VLAN") %}
{% do vlan_db.remove("15") %}
{# Code below omitted #}

These two dictionary methods—replace() and remove()—are a bit easier to use than the update() method. The replace() method only requires us to enter two values between the parentheses: "key", "value". The remove() method only requires one—the name of the key: "key".

So let’s put all this together into a template and see what the output would look like:

Template:

{% set vlan_db = {
  "1": "DO NOT USE",
  "10": "Management VLAN",
  "15": "Printer VLAN",
  "20": "Voice VLAN",
  "123": "Guest VLAN",
  "777": "POS VLAN",
  "999": "Blackhole VLAN"
  } 
%}
{% do vlan_db.update({"600": "Warehouse VLAN", "620": "Warehouse Voice VLAN"}) %}
{% do vlan_db.replace("1", "UNUSED VLAN") %}
{% do vlan_db.remove("15") %}
Here is the updated vlan_db: {{ vlan_db }}

{# Loop through List and create VLANS #}
{%- set create_vlan_list = create_vlan_list | split(",") %}
{% for vlan in create_vlan_list %}
{% set vlan_str = vlan | string %}
vlan {{ vlan }}
{% if vlan_db[vlan_str] is defined %}
 name {{ vlan_db[vlan_str] }}
 {% else %}
 name VLAN_{{ vlan }}
 {% endif %}
{% endfor %}

Variable input:

Note: Remember that the Catalyst Center templating interface will likely notice that you’re using the variable create_vlan_list inside of a for loop. As a result, it may set this variable’s Data Type to String and the Display Type to Multiple Select. If this happens, you will need to go to the Variables tab" (or Form Editor if you’re using Template Editor) and reset this variable to String and Text Field.

Output:

Here is the updated vlan_db: {1=UNUSED VLAN, 10=Management VLAN, 20=Voice VLAN, 123=Guest VLAN, 777=POS VLAN, 999=Blackhole VLAN, 600=Warehouse VLAN, 620=Warehouse Voice VLAN}

vlan 1
 name UNUSED VLAN
vlan 2
 name VLAN_2
vlan 5
 name VLAN_5
vlan 20
 name Voice VLAN
vlan 50
 name VLAN_50
vlan 777
 name POS VLAN
vlan 999
 name Blackhole VLAN

I admit, this is a silly example. If we wanted to edit the contents of a dictionary that is hardcoded into a template, why not just change the dictionary itself? This is unnecessary effort, but it serves as an example of what is possible. Who knows, you may find a unique use for these methods that I haven’t even thought of!

Tutorial Part 1 Complete

Congratulations! You’ve completed Part 1 of the Cisco Catalyst Center Jinja Templating tutorial. With the knowledge that you’ve gained from this tutorial, you are well on your way to creating some interesting and advanced Jinja templates in Catalyst Center. Please continue on with Part 2 to complete the training. There’s much more fun and useful information yet to come!

Learn More

Why Create a Free Cisco U. Account?

A Cisco U. account helps you:

Further Learning Resources

Need Help or Want to Engage?

Finishing Up

Don’t forget to click Exit Tutorial to log your completed content.