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.