What You’ll Learn

What You’ll Need

Assumptions:

Math Operators

Jinja was written in the Python programming language, so it inherits many of the built-in features of Python, including the ability to perform math operations. Jinja supports all of the basic math operators that you would expect in programming language:

Template:

{{ 2 + 5 }}
{{ 11 / 4 }}
{{ 11 // 4 }}...Oops, it didn't round up.
{{ 11 % 4 }}
{{ 3 * 3 }}
{{ 3 ** 3 }}

Output:

7
2.75
2...Oops, it didn't round up.
3
9
27

⚠️ Caution! Be aware of your variable data types before you perform a math operation with them. There are actually some math operations that can be performed on string data types. If you attempt to add a string to an integer, it will actually work, but the result you’ll get is not the one that you’d expect. For example:

Template:

{% set var1 = "1" %}
{{ var1 + 1 }}...Uh oh!
{% set var2 = 1 %}
{{ var2 + 1 }}...That's better!

Output:

11...Uh oh!
2...That's better!

So what happened with that first expression, where we added var1 to the integer 1? We ended up with something called string concatenation, which is where two strings are “glued” together to form one longer string. But wait a minute—the 1 in this example was an integer, not a string, right? That’s correct, and this is where the interesting part comes in: Jinja (and Python) looked at this expression and made a decision for you. It decided that because you were performing math on a string type variable, you must have wanted a string as the output, so it went ahead and converted the integer 1 into the string "1", then concatenated it onto the end of the string var1. What it printed out was the string value "11", but because we’re just looking at plaintext in the output, it actually looks like the integer 11. We would have no way of knowing what actually happened here unless we were paying very close attention. So, always be careful with your data types!

Comparison and Logic Operators

Jinja also has some powerful tools for comparing values, in the form of comparison operators and logic operators. These are special keywords or symbols that instruct a computer program to evaluate some inputs and apply certain conditions to them. They are at the heart of every intelligent decision that we can teach a programming or templating language to perform; they are hugely important.

Comparison Operators

All comparison operations return a Boolean value—either true or false.

Logic Operators

Just like comparison operations, all logic operators return a Boolean value—either true or false.

One additional logic operator is the grouping expression ( ). It’s not so much a logical operation as it is a way to group together multiple expressions or statements and return their combined result. You see this most often in math with the order of operations (remember PEMDAS?); any mathematical expression that is surrounded in parentheses must be evaluated first before moving on to the rest of the expression. For example:

{{ 20 - (5 * 3) }}

The result here would be 5. If you got 45 for an answer, it is time to revisit that math textbook!

If/Else Conditionals

We mentioned earlier that comparison and logic operators are at the heart of every intelligent decision that we can teach a computer program to perform. The most common way that we utilize comparison and logic operators is through “if/else” conditional statements. If/else conditionals create a series of tests that a program will evaluate something against, in sequential order. Based on the results of those tests, you can tell the program to perform different actions.

A good analogy would be one of those shape-sorting toys that young children play with: They start with a series of different-shaped blocks, and they must test each one against the different-shaped holes until they find one that fits the block. There’s one major difference, though: If/else conditionals can have a “catch-all” at the bottom, which will match anything that didn’t fit in the previous tests.

An if/else conditional statement can have:

Each conditional statement must evaluate to either true or false. If the result is true then the instructions that are contained beneath that statement will be executed, and the program will exit the if/else conditional. That’s an important bit of information to remember: The first true result will cause the evaluation to halt and exit once its work is complete. No further conditional statements will be evaluated.

If, instead, a conditional statement evaluates to false, the program will continue on to the next conditional statement and run that test—and so on, until a true result is found or the end of the if/else conditional is reached. Here’s a simple example:

Template:

{% set var = "bar" %}
{% if var == "foo" %}
Condition 1 is True
{% elif var == "bar" %}
Condition 2 is True
{% else %}
No conditions matched
{% endif %}

Output:

Condition 2 is True

In this example, the first condition if var == "foo" is false because bar does not equal foo. (By the way, “foobar” is a common example that you’ll see in most software programming courses.) So, because that statement was false, the template moves down to the elif var == "bar" condition, which will evaluate as true because bar does in fact equal bar. Because that elif statement evaluated true, any instructions or lines of code that are below this line and above the else catch-all statement will be executed. In this case, Condition 2 is True will be printed to the output. Then, the if/else statement will exit; no further action is taken.

If/else conditional statements are pretty simple, and they are the bedrock on which we build an “intelligent” script or template. Remember that you can have as many elif (else if) condition statements as you want—and you can also add, or leave out, an else catch-all statement. Actually, the only required lines in an if/else statement are the first if condition and the ending {% endif %} tag. This would be a totally valid template:

{% set var = "bar" %}
{% if var == "foo" %}
Condition 1 is True
{% endif %}

It wouldn’t produce any output, because the if statement will always evaluate false, but it would still work.

For Loops

Whereas if/else conditionals are the decision-makers in our templates, “for loops” are the heavy lifters and repetitive workers. They get stuff done very efficiently. A for loop could theoretically run forever—though Python has built-in “infinite loop” protection that will stop a runaway script before it crashes your computer. A for loop really only needs one thing to function: an iterable object to work on. These are most commonly lists, tuples, and dictionaries, although there are more Python objects that can be iterated over.

Similar to an if/else conditional statement, a for loop needs an opening and closing tag:

{% for <item> in <iterable> %}
Do some work
{% endfor %}

Something interesting, and often not obvious, is happening in the beginning for statement of a for loop. Take this example:

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

Simply put, what it is saying is:

For every individual item (i) in the list (range(0,10))…

So, where did i come from? What is the significance of that letter? Actually, I just made it up. The letter i in this example is just a locally significant variable, which I created for the sole purpose of storing individual items that come from the set produced by the function range(0,10). That variable is “locally significant” because it only exists inside the for loop. Once the loop is finished, that variable is destroyed. As a result, I can name it whatever I like; I could even say {% for bob in range(0,10) %}, and it wouldn’t matter.

By the way, the range() function simply takes a starting number and an ending number, and it returns to you a list containing all the numbers from the starting number up to but not including the ending number. Remember that—it is important.

We can do more with for loops than just iterate over a list of numbers. We can provide it a list of anything—and, in fact, it is quite common to use a for loop to iterate over JSON structured data and use a nested if/else conditional to locate something specific.

Template:

{# Hey, it's our fruit list again! #}
{%- set my_fruit_list = ["apple", "banana", "pear", "peach", "watermelon"] %}
{% for fruit in my_fruit_list %}
{% if "p" in fruit %}
There's a "p" in my fruit basket! {{ fruit }}
{% endif %}
{% endfor %}

Output:

There's a "p" in my fruit basket! apple
There's a "p" in my fruit basket! pear
There's a "p" in my fruit basket! peach

OK, that was kind of silly … but it is a good example.


For Loop Indexes

There’s another interesting and sometimes helpful feature of for loops in Jinja templates: You can access the number of the current iteration inside the loop. We know that for loops will continue processing their instructions until they reach the end of whichever iterable object that they are working on. But what if you needed to know which number iteration you are currently on? For loops have two special variables available for just that purpose: loop.index and loop.index0.

The loop.index “counter” variable starts at 1 and counts upward until the end of the for loop is reached. The loop.index0 variable does the same except that it starts at 0 instead of 1. Here’s an example:

Template:

{% for item in [2,3,5,7,11,13] %}
Current item: {{ item }}
Current index: {{ loop.index }}
Current index0: {{ loop.index0 }}

{% endfor %}

Output:

Current item: 2
Current index: 1
Current index0: 0

Current item: 3
Current index: 2
Current index0: 1

Current item: 5
Current index: 3
Current index0: 2

Current item: 7
Current index: 4
Current index0: 3

Current item: 11
Current index: 5
Current index0: 4

Current item: 13
Current index: 6
Current index0: 5

Jinja Filters

You’ve made it this far—congratulations! As promised, now we will dive a bit deeper into Jinja filters. Filters are powerful tools in a Jinja template, and they can range from simple to complex. Filters perform modifications on the contents of a variable; however, it is important to remember that they do not change the contents of a variable. Filters accept the original contents of a variable as their input, and they return a modified or transformed copy of that data as their output.

Filters always follow a pipe character (|); for example: {{ my_variable | upper }}. Multiple filters also can be strung together so that the output of one filter is passed to the input of the next filter:

Template:

{% set my_variable = "Filters are GREAT!" %}
{{ my_variable }}
{{ my_variable | lower | title | reverse }}

Output:

Filters are GREAT!
!taerG erA sretliF

In the ”Working with Strings” topic in Part 1 of this tutorial, we provided a list of some of the most useful filters for working with strings, so we will repeat them here as a reminder:

Here are some additional useful filters that are powerful but less commonly used:

There are several more filters provided by Jinja, some of which are only useful with certain data types, such as items, which only works with dictionaries (referred to as a “mapping” in the documentation). Some filters can transform a variable from one type to another, such as int, string, float, and list. If it is possible to convert the input data type to the output type that you’ve requested, then these filters will return a new variable value of the specified type. I say possible because sometimes, you simply can’t convert one type of variable into another. For example, you wouldn’t be able to convert a list to a floating-point number (float).

However, there are some unique situations where it is possible to convert one data type to another, but it won’t work because of the specific value that is stored in that variable. Take, for instance, the scenario of converting a string into an integer; this can work with certain values. If we create a string variable and set it equal to a value that contains only numeric digits, then we can convert that string into an integer.

Template:

{% set my_string_variable = "12345" %}
{{ type(my_string_variable) }}
{% set my_int_variable = my_string_variable | int %}
{{ type(my_int_variable) }}

Output:

str
int

Note: You may have noticed that we are using something in the template above that we haven’t seen before: type(). This is a special “function” that is available in the template code editor, and it simply returns the “class type” (aka data type) of the variable that you place inside its parentheses. A function is just a standalone, reusable block of code that performs a preprogrammed action.

We’ve proven that we can convert a string variable containing 12345 into an integer variable. What do you suppose would happen if we tried to convert the string abcde to an integer? I would expect it to throw some kind of error message, because we know that the Unicode characters abcde cannot be converted into numbers. Frankly, I was surprised by what Catalyst Center did when I tested this out:

Template:

{% set my_string_variable2 = "abcde" %}
{{ type(my_string_variable2) }}
{% set my_int_variable2 = my_string_variable2 | int %}
{{ type(my_int_variable2) }}
{{ my_int_variable2 }}

Output:

str
int
0

It sure looks like Catalyst Center converted the string abcde into an integer, because the output of the type() function clearly says my new variable, my_int_variable2, is of type int. I didn’t trust what I was seeing, so I added another expression at the end to print out the contents of the my_int_variable2—and it returned a 0. As it turns out, this is the default behavior of the int filter in Jinja; you just have to read the documentation very carefully.

So here’s an important lesson in developing templates for Catalyst Center: Never assume anything. Always test and validate your templates. You could end up with unexpected outcomes.

Filters for Complex Data Types

As we mentioned, there are many filters included with Jinja, and we really can’t cover all of them in this tutorial. The best way to learn is always to experiment with them, using the template simulator. However, there are some interesting filters that are useful with more complex data types, like dictionaries, lists, and tuples.

The items Filter

The items filter is particularly useful with dictionaries, and it gives us a very powerful capability to work with and understand the system bind variable attributes that Catalyst Center provides for us.

Note: The topic of system bind variables (called template system variables in Template Editor) is important, but it is a little outside the scope of this training. System bind variables are special variables that Catalyst Center provides you to use inside your templates. These special variables contain information that Catalyst Center collects about all devices in its inventory, as well as some global settings. When you use them in a template, Catalyst Center provides their values; users will not be prompted to provide values for them. For more information on system bind variables, you can check out our product documentation or this tutorial video on our YouTube channel.

You may have noticed that while the template interface in Catalyst Center provides a nice pop-up list of all the system bind variable names and their associated attributes, it doesn’t actually tell you what is stored in those attributes. There are so many of them that it wouldn’t be much fun to write a template with a separate expression for each system bind variable attribute (for example, {{ __device.platformId }}).

By using the items filter, we can loop through a dictionary and capture both the key name and its assigned value, and then print them to the output. The items filter actually returns an iterator, containing one or more tuples, which contain the key and value for each pair in the dictionary.

Here’s an example where things get a little interesting in Catalyst Center. The items Filter doesn’t work properly when it is used in this format in a for loop:

Template:

{% for key, value in __device|items %}
{{ key }}: {{ value }}
{% endfor %}

That’s the correct syntax, according to the documentation, but it doesn’t result in any output when used in a Jinja template in Catalyst Center. However, there is an alternative solution that we can use: the items() method! It looks basically the same, but instead of separating the filter from the system bind variable using a pipe symbol (|), we’ll use the “dot” notation:

Template:

{% for key, value in __device.items() %}
{{ key }}: {{ value }}
{% endfor %}

Output:

instanceUuid: 34daadff-b327-412c-afb6-c14737e94a48
instanceId: 14048034
authEntityId: 14048034
authEntityClass: -927529445
instanceTenantId: 5c6cd3c78476f9009032101f
...<output truncated>...

Note: The special double-underscore variable __device is one of the system bind variables provided by Catalyst Center. When you use one of these variables, you will need to specify a target device from your inventory when running the template in the simulator, because Catalyst Center will obtain information that is specific to that device and use it in your template.

Depending on the target device that you specify, the values that are printed will vary, but the end result of these 3 lines of Jinja template code are about 71 lines of output! You get to see every attribute stored in the __device variable along with its associated value. Let’s take this a step further and print out all the interface attributes as well:

Template:

********* Device Attributes **********
{% for key, value in __device.items() %}
{{ key }}: {{ value }}
{% endfor %}
======================================

******** Interface Attributes ********
{% for int in __interface %}
INTERFACE: {{ int.portName }}
++++++++++++++++++++++++++++++++++++++
{% for key, value in int.items() %}
{{ key }}: {{ value }}
{% endfor %}
======================================

{% endfor %}

Output:

********* Device Attributes **********
instanceUuid: 34daadff-b327-412c-afb6-c14737e94a48
instanceId: 14048034
authEntityId: 14048034
authEntityClass: -927529445
instanceTenantId: 5c6cd3c78476f9009032101f
...<output truncated>...

******** Interface Attributes ********
INTERFACE: TenGigabitEthernet1/0/14
++++++++++++++++++++++++++++++++++++++
pid: C9300-24UX
deviceId: 34daadff-b327-412c-afb6-c14737e94a48
ipv4Address: 
ipv4Mask: 
isisSupport: false
...<output truncated>...

Now that’s a powerful template! As you can see, I ran this template using a Cisco Catalyst 9300-24UX switch as the target, and the result was 2045 lines of output. Every attribute for the device chassis (__device) and each interface (__interface)—physical or virtual—is printed out.

If you have a keen eye, you might have noticed that for the interfaces contained in __interface, we actually used two for loops—one nested inside the other. Why did we do that? Let’s take a moment to consider the physical device that we are dealing with here: a Catalyst 9300-24UX switch. A Catalyst switch on its own is considered a “chassis” in Cisco terminology. Even if this switch were a member of a StackWise system, where two or more physical Catalyst switches are cabled together to increase port density, the resulting configuration is still treated as one “logical” switch. If you were to log in to the CLI of a StackWise Catalyst switch, you would see interface numbers that look like 1/0/1...1/0/24, 2/0/1...2/0/24, and so on, so it’s really still just one chassis. However, every Catalyst switch (and almost any network device) has more than one interface, whether they are physical ports on the front panel or virtual interfaces that are only available to the operating system.

While we can use a single dictionary to store attributes and their values for a single device (because each attribute is unique and there’s only one instance of them), we couldn’t really use a single dictionary for every interface on the device. In fact, every individual interface would have the same set of attributes, just with different values. So, we need to create some kind of “container” to hold more than one dictionary. How about a list?

Example (Note: There are many more attributes and values to an interface than what we are showing here):

[
  {
    "pid": "C9300-24UX",
    "deviceId": "34daadff-b327-412c-afb6-c14737e94a48",
    "ipv4Address": "",
    "ipv4Mask": "",
    "isisSupport": false
  },
  {
    "pid": "C9300-24UX",
    "deviceId": "34daadff-b327-412c-afb6-c14737e94a49",
    "ipv4Address": "",
    "ipv4Mask": "",
    "isisSupport": false
  },
  {
    "pid": "C9300-24UX",
    "deviceId": "34daadff-b327-412c-afb6-c14737e94a50",
    "ipv4Address": "",
    "ipv4Mask": "",
    "isisSupport": false
  }
]

We just created JSON structured data! JSON stands for JavaScript Object Notation, and it is the most popular structured data format in use today, particularly for REST-based application programming interfaces (APIs).

You might be asking yourself, “How do I know if a particular system bind variable in Catalyst Center is stored as a list or as a dictionary?” That’s a great question, and an important one as well, because it will influence how you interact with that variable in a template.

When you open the System Bind Variable pop-up menu (called Template System Variables in the older Template Editor interface), you’ll see a long list of available system variables, all starting with a double underscore (for example, __device), and some of them are expandable so that you can view the various attributes they contain. Variables that are stored as dictionaries will list their attributes in dot notation immediately after the system bind variable name, like this: __device.description or __device.hostname. However, if the system bind variable is stored as a list (containing multiple dictionaries), you will see the text [index] appear between the variable name and the dot, preceding the attribute name:

system_bind_variables_list_vs_dictionary.png

What we have now are multiple dictionaries (each containing the same keys) stored inside a list. The first thing we need to do in order to loop through this data is to take care of that pesky list; we need an “outer for loop.” For each iteration of the outer for loop, we get access to an individual dictionary that is stored in one of the indexes in that list.

******** Interface Attributes ********
{% for int in __interface %}  <--- OUTER FOR LOOP
INTERFACE: {{ int.portName }}
++++++++++++++++++++++++++++++++++++++
{% for key, value in int.items() %}
{{ key }}: {{ value }}
{% endfor %}
======================================

{% endfor %}                  <--- OUTER FOR LOOP

Inside that outer for loop, we need to build an “inner for loop” that we can use to iterate over all the keys and values in each dictionary.

******** Interface Attributes ********
{% for int in __interface %}
INTERFACE: {{ int.portName }}
++++++++++++++++++++++++++++++++++++++
{% for key, value in int.items() %} <--- INNER FOR LOOP
{{ key }}: {{ value }}
{% endfor %}                        <--- INNER FOR LOOP
======================================

{% endfor %}

You could actually continue nesting for loops inside for loops forever; at some point, you’d break the language interpreter and it would freeze up … but in theory, you could do it. Don’t, though. It is not a good idea!


The fromjson Filter

The fromjson filter is a curious one because it is not in the Jinja documentation. But it is available for you to use, and it can do something pretty cool: It can convert a specially formatted string into a JSON-formatted list or dictionary!

In the older versions of Catalyst Center, when Template Editor was the templating interface, this filter wasn’t terribly useful because text field-formatted string variables were limited to 255 characters in length. So, any time that you created a template variable and configured its input type as Text Field, the template user could only enter a maximum of 255 characters.

Lists and dictionaries tend to be rather large, though, so formatting them as a string and pasting that text into a text field normally wouldn’t work; you’d run out of characters pretty fast. And that meant that converting the string into a list or dictionary wasn’t terribly useful, because the input value could never be longer than 255 characters. However, that’s changed with the new Template Hub interface, and now Text Field input types can theoretically be more than 2 billion characters long! (Good luck testing that.)

OK, so what? Why would someone want to convert a string into a list or a dictionary? I’m glad you asked, because there are some really interesting things that you can do with JSON structured data in a template. The most useful example that I can come up with is configuring a large number of switch interfaces with custom descriptions, VLAN settings, and other configuration options.

Note: This is an example that, while it works, isn’t really practical to use at scale—but it demonstrates a capability. Actually, there’s a better way to accomplish this same task using a scripting language (like Python), a CSV-formatted input file, and the Catalyst Center templating APIs. I have created just such an example, which I’ve shared on GitHub.

The formatting of the string input is very important, because the fromjson filter is rather picky about the characters that are used. All keys and any string values in each dictionary, as well as any strings stored in list indexes, must be surrounded with double quotation marks (" "). If you are hardcoding a JSON-formatted string into a template (rather than asking the user to provide it), you must surround the entire string in single quotation marks (' ').

Here’s an example, shown both as user input through the Catalyst Center web user interface and as a hardcoded value in a template:

JSON example:

[
  {
    "int_name": "GigabitEthernet1/0/1",
    "description": "Gig 1/0/1 description",
    "port_mode": "access",
    "access_vlan": "10",
    "trunk_native_vlan": "",
    "trunk_allowed_vlans": ""
  },
  {
    "int_name": "GigabitEthernet1/0/2",
    "description": "Gig 1/0/2 description",
    "port_mode": "trunk",
    "access_vlan": "",
    "trunk_native_vlan": "5",
    "trunk_allowed_vlans": "5,10,15,20"
  }
]

User input example (json_string):

[{"int_name": "GigabitEthernet1/0/1", "description": "Gig 1/0/1 description", "port_mode": "access", "access_vlan": "10", "trunk_native_vlan": "", "trunk_allowed_vlans": ""}, {"int_name": "GigabitEthernet1/0/2", "description": "Gig 1/0/2 description", "port_mode": "trunk", "access_vlan": "", "trunk_native_vlan": "5", "trunk_allowed_vlans": "5,10,15,20"}]

Jinja template example:

{% set json_string = '[{"int_name": "GigabitEthernet1/0/1", "description": "Gig 1/0/1 description", "port_mode": "access", "access_vlan": "10", "trunk_native_vlan": "", "trunk_allowed_vlans": ""}, {"int_name": "GigabitEthernet1/0/2", "description": "Gig 1/0/2 description", "port_mode": "trunk", "access_vlan": "", "trunk_native_vlan": "5", "trunk_allowed_vlans": "5,10,15,20"}]' %}

Now for the fun part. This is a really small example of JSON data, but you can easily imagine it containing configuration information for hundreds of ports. Once we have a list or a dictionary (or, in the case of JSON, a list of multiple dictionaries) stored in a variable inside our template, we can use for loops and some if/else logic to iterate through them. If we are working with a list containing multiple dictionaries—each with identical keys—then we can reference those keys inside a for loop as if they were attributes (because actually, they are). Here’s what this Jinja template could look like:

Template:

{% set input_json = input_json_string | fromjson %}

{% for port in input_json %}
interface {{ port.int_name }}
 description {{ port.description }}
 switchport mode {{ port.port_mode }}
 {% if port.port_mode == 'access' %}
 switchport access vlan {{ port.access_vlan }}
 {% else %}
  {% if port.trunk_native_vlan != "" %}
 switchport trunk native vlan {{ port.trunk_native_vlan }}
  {% endif %}
  {% if port.trunk_allow_vlans != "" %}
 switchport trunk allowed vlans {{ port.trunk_allowed_vlans }}
  {% endif %}
 {% endif %}
{% endfor %}

Note: Because this JSON-formatted text string is longer than 255 characters, it will only work in Catalyst Center v2.3.5 or higher, using the Template Hub interface. Also, even though Template Hub now allows much longer text input values, it still defaults to a maximum of 255 characters unless you visit the Variables tab and configure a much larger number in the Maximum Characters field of your input_json_string variable.

Output:


interface GigabitEthernet1/0/1
 description Gig 1/0/1 description
 switchport mode access
 switchport access vlan 10
interface GigabitEthernet1/0/2
 description Gig 1/0/2 description
 switchport mode trunk
 switchport trunk native vlan 5
 switchport trunk allowed vlans 5,10,15,20

That’s one awesome template! In fact, I’ve tested the example above, using a JSON string that contained configuration for 96 switch ports—the string was 15,267 characters long—and it worked perfectly. Here’s the short Python script that I used, in conjunction with a CSV file containing the same formatted data that we used above, to get a properly formatted JSON string. I had to omit the allowed VLANs list; having a comma-separated list inside a CSV file would create some problems, and it would take a little extra work to get around that. Each dictionary key comes from the CSV column headers, and each row is a unique dictionary:

CSV example (port_config.csv):

int_name,description,port_mode,access_vlan,trunk_native_vlan,trunk_allowed_vlans
GigabitEthernet1/0/1,1/0/1,access,10,,
GigabitEthernet1/0/2,1/0/2,trunk,,5,

Python script:

import json
import csv
csv_data = []
with open('port_config.csv', 'rt') as f:
    reader = csv.DictReader(f)
    for row in reader:
        csv_data.append(row)
    f.close()

print(json.dumps(csv_data))

Jinja Tests

We mentioned something called a “test” earlier in this tutorial, but we didn’t go into much detail about it at that point. A Jinja test is exactly what it sounds like: a preconfigured test for evaluating some type of input, and it returns a Boolean value as a result (true or false). Pretty simple stuff.

Most Jinja tests are combined with the is keyword, which tells Jinja to take the specified action (for example, “run this test”). One exception is the in test, which doesn’t require that you combine it with is; you can combine in with is, but you just don’t have to.

There are quite a few built-in tests that are available in the Jinja templating language, and some are more useful than others. The full list can be found in Jinja’s documentation, so we will just cover the more common and useful tests here:

Include Statements

Jinja provides you with the ability to “include” (import) one template into another, using an include statement. This is particularly helpful as a way to reduce code repetition, which is a foundational principle of good software development: Don’t repeat yourself (DRY).

Include statements allow you to create one or more regular templates in Catalyst Center that contain reusable blocks of code, and import them as needed into other templates—a parent-child relationship. Once imported, all the variables, code blocks, macros, and so on from the child template became directly available to the parent template.

To import a child template into a parent template in Catalyst Center, you can use this simple statement:

{% include "My_Project/My_Template" %}

This format will allow you to import any template, from any project, into the current template that you are working in. Actually, the project name is optional; if the child template that you want to import exists in the same project as the parent template, you can omit the project name:

{% include "My_Template" %}

Note: Normally, you should place your include statements at the top of the parent template; however, you can place them anywhere else within a template. The only requirement is that you place the include statement before you make any references to variables or objects from the child template.

Here is a simple example:

Child template (import_example_project/child_jinja_template):

{% set child_var = "example_text" %}

Parent template (import_example_project/parent_jinja_template):

{% include "import_example_project/child_jinja_template" %}
{{ child_var }}

Parent template output:

example_text

Macros

Macros are similar to using the Jinja include statement. However, macros can exist either in the Parent template or in an imported child template. If a macro is defined inside the same template where you are using it, then there’s no need to use an include statement. Macros are just blocks of reusable Jinja code that are given a unique name. Macros can (optionally) accept one or more input variables as well, allowing their output to be customized by user input. For those of you familiar with Python or Java, these macros might look familiar to you; they might look like functions from Python or Java because they are. A macro is a standalone, reusable, and customizable block of code.

Macros are created by using the opening and closing {% macro ... %} and {% endmacro %} tags, along with a unique macro name, followed by a set of parentheses (). Like this:

{% macro simple_example_macro() %}
This is a simple Macro example, with no input variables.
{% endmacro %}

This example is a simple macro that has no input variables (called “arguments”), and calling it will result in the single line of text that it contains being printed to the output. To “call” a macro, simply refer to it by name, followed by the parentheses (), in a Jinja expression:

Template:

{{ simple_example_macro() }}

Output:

This is a simple Macro example, with no input variables.

You can create a more complex macro that accepts one or more input arguments so that the output can be customized based on user input:

{% macro complex_example_macro(input_variable, input_variable_2) %}
This is a complex Macro example, with two input variables.
{% if input_variable | length > 0 %}
This was passed in to input_variable: {{ input_variable }}
{% endif %}
{% if input_variable_2 | length > 0 %}
This was passed in to input_variable_2: {{ input_variable_2 }}
{% endif %}
{% endmacro %}

Template:

{{ complex_example_macro("Hello", "World!") }}

Output:

This is a complex Macro example, with two input variables.
This was passed in to input_variable: Hello
This was passed in to input_variable_2: World!

As we mentioned, macros can have one or more argument variables included between the parentheses. If a variable name is given without a default value, then that variable will be required for the macro to function properly; that is, you must provide a value for it. If, instead, you would like to include a variable that is optional, you simply have to give it a default value when creating the macro, like this:

{% macro complex_example_macro(input_variable, input_variable_2="default_value") %}
...<omitted>...
{% endmacro %}

The default value that you provide will be used to determine which data type that the variable becomes; string, integer, float, Boolean, and so on. (In the example above, we are making input_variable_2 This is the default behavior in the Python programming language, and it is inherited by the Jinja templating language as well. Variables are “dynamically typed,” meaning that the data type is defined at the same time that a value is assigned to the variable. The data type of the value defines the data type of the variable. To put it in simple terms, if you provide a default value for a variable, make sure that it is the data type that you intend to use for that variable.

A good, practical example of what you might use a macro for is device interface configurations. For instance:

{% macro access_port_config(description, access_vlan, shutdown="no") %}
 description {{ description }}
 switchport mode access
 switchport access vlan {{ access_vlan }}
 {% if shutdown == "yes" %}
 shutdown
 {% endif %}
{% endmacro %}

There are a few more features of macros, which you can read about in this documentation. However, they are not often used unless you begin writing very complex templates.

Tutorial Part 2 Complete

Congratulations! You’ve completed the Cisco Catalyst Center Jinja Templating tutorial. With the knowledge you’ve gained from this tutorial, you should be well-equipped to begin creating some interesting and advanced Jinja templates in Catalyst Center. As we’ve mentioned a few times already, this tutorial doesn’t cover every detail or possible option, so your next step should be to read through the Jinja template documentation and begin experimenting with features to 1) see what works and what doesn’t, and 2) better understand the results of each feature.

Learn More

Training Resources

Finishing Up

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