A Simple Configuration Generator Using Jinja2 and Python

Jinja is a templating engine available in Python programming language. Jinja2 is the latest release of this templating engine. The terms Jinja and Jinja2 are often used interchangeably. It is a text-based template language and thus can be used to generate any source code or markup. A Jinja template file is simply a text file that contains Jinja language constructs – variables, conditional statements, loops, etc. The Jinja engine takes the template file and data variables as input. The Jinja engine processes these constructs, expands variables to their values and render a text output that is free of any Jinja markup.

For example, here the variable “template” represents a Jinja template. Generally, template is read from a file, but here it is directly given for simplicity. Jinja variables inside {{}} brackets are replaced with their values in the output. The Python dictionary “data” contains the values for the variables referenced inside the template.

from jinja2 import Template

template = """
Hello {{planet}}.
My name is {{name}}.
"""

data = {"planet": "Earth", "name": "Rashid"}

t = Template(template)
r = t.render(data)

print(r)

The above code renders this output.

Hello Earth.
My name is Rashid.

With this very brief overview of the Jinja templating engine and it’s use case, a simple encapsulation Python function is presented next which takes a template config (as a string object ‘source’) and template data (as a dictionary object ‘data’) as inputs and renders it to the output. In addition, the function can validate if the input template config is a valid JSON or YAML syntax. If the syntax is invalid, it is not rendered to the output, instead a warning message is printed.

import json, yaml
from jinja2 import Environment

#### Renders a Jinja2 source template (string) along with a list of dictionary object to
#       generate a list of rendered data (string).

def render_j2_template(
    source,             # source Jinja2 template as a string
    data,               # list of dictionary
    type="text",        # type of 'source' template [text (default), json, yaml]
    validate=False,     # validate rendered output syntax? valid for type [json, yaml]
    minify=False,       # minify json. implicit validation. valid for type [json].
    ):
    env = Environment()
    env.trim_blocks = True
    env.lstrip_blocks = True
    env.rstrip_blocks = True
    
    template = env.from_string(source)

    rendered = []
    for td in data:
        tr = template.render(td)
        
        try:
            if type in ['json', 'yaml']:
                if (type == 'json' and validate) or (type == 'json' and minify):
                    jtr = json.loads(tr)
                    if minify:
                        tr = json.dumps(jtr, separators=(',', ':'))
                        
                if type == 'yaml' and validate == True:
                    yaml.load(tr, Loader=yaml.FullLoader)
                    
        except Exception:
            print(f"="*32)
            print(f"WARNING - '{type}' data validation failed. Skipping...")
            print(f"{tr}")
            print(f"-"*32)
            continue
        
        rendered.append(tr)
        
    return rendered

Here is an example code using this function with input template config read from a text file, and template data from an Excel file (read using openpyxl module).

if __name__ == "__main__":
    # import modules
    import openpyxl
    
    # INPUT VARIABLES
    wbn = "render_j2_template.xlsx"
    wsn = "data1"
    
    # read template source file
    with open("render_j2_template.src.txt") as f:
        template_source = f.read()

    # read template data file
    template_data = []
    
    wb = openpyxl.load_workbook(
        wbn,
        data_only=True,
        )
    ws = wb[wsn]
    
    header = [cell.value for cell in ws[1]]
    for row in ws[2: ws.max_row]:
        values = {}
        for key, cell in zip(header, row):
            values[key] = cell.value
        template_data.append(values)
        
    # render template - checks rendered template is valid Json and minifies it.
    rendered = render_j2_template(
        template_source, 
        template_data,
        type='json',
        minify=True,
    )
    
    # print original template and rendered data
    print(f"*** TEMPLATE (str) ***\n{template_source}\n")
    print(f"*** RENDERED (str) ***")
    for r in rendered:
        print(f"{r}")

The above results in the output,

*** TEMPLATE (str) ***

{
    "hostname": "{{hostname}}",
    "vlans": [
        {
            "vlan": 10,
            "ip": "{{vlan10_ip}}",
            "mask": "{{vlan10_mask}}"
        }
    ]
}


*** RENDERED (str) ***
{"hostname":"switch1","vlans":[{"vlan":10,"ip":"10.10.11.1","mask":"255.255.255.0"}]}
{"hostname":"switch2","vlans":[{"vlan":10,"ip":"10.10.12.1","mask":"255.255.255.0"}]}
{"hostname":"switch3","vlans":[{"vlan":10,"ip":"10.10.13.1","mask":"255.255.255.0"}]}

This is just a quick example of the function usage. The input template config can be any configuration data (systems or networking devices), JSON or YAML formatted data.

The full source code, including example input files, is available at my GitHub Site. All files names prefixed with “render_j2_template”. To run the code, the following Python modules need to be available on the system – json, yaml, jinja2, openpyxl.

 

Share this:
Tags: ,

About: Rashid