NetAuto Academy

Configuration & Template Automation

Rendering, Validating, and Reviewing Configuration Safely

Intermediate13 min read

What you'll learn

  • How to load a template from a real .j2 file with FileSystemLoader
  • Why 'rendered without an error' is not the same as 'rendered correctly'
  • Jinja2's actual behavior with missing template variables — verified, not assumed
  • How to write a validation check against rendered configuration before it's ever deployed
🎓 Teach This Lesson

Listen to a guided explanation of this lesson.

Spoken narration isn't supported in this browser.

A template deserves its own file

Every example so far wrote the template as an inline Python string — fine for learning, but not how real templates are kept. devices.csv has been its own file, separate from the script, since "Your First Network Automation." A Jinja2 template deserves the same treatment: its own .j2 file, loaded by the script rather than embedded in it.

templates/vlan_config.j2
{% for vlan in vlans %}
vlan {{ vlan.id }}
name {{ vlan.name }}
{% endfor %}

A real, standalone template file — no Python in it at all. This is the exact content a non-programmer could open and understand.

load_template.py
from jinja2 import Environment, FileSystemLoader

env = Environment(
  loader=FileSystemLoader("templates"),
  trim_blocks=True,
  lstrip_blocks=True,
)
template = env.get_template("vlan_config.j2")

required_vlans = [
  {"id": 10, "name": "USERS"},
  {"id": 20, "name": "VOICE"},
]

rendered = template.render(vlans=required_vlans)
print(rendered)

FileSystemLoader('templates') tells Jinja2 to look for template files in that directory; get_template() reads the actual file from disk. Everything else — render(), the data — works exactly as it did with an inline Template().

This renders exactly as Lesson 4's inline version did:

output
vlan 10
name USERS
vlan 20
name VOICE

Same output as before — only where the template text came from changed, from an inline Python string to a real file on disk.

Quick checkConcept check
What does FileSystemLoader("templates") actually do?

Rendered without error ≠ correct

Here's the moment this module has been building toward. What happens if the data passed to render() is missing the vlans variable entirely?

missing_variable.py
result = template.render()  # no vlans passed at all
print(repr(result))

No exception, no crash — just call render() with nothing.

This prints:

output
''

An empty string. No error was raised. The script would happily continue as if it had produced a valid, empty configuration change — silently.

This is Jinja2's real, default behavior — verified directly

By default, Jinja2's Undefined type treats a missing variable as an empty value rather than raising an error. A missing loop variable simply iterates zero times. This isn't a bug — it's a deliberate default that makes templates forgiving. For network configuration, it means a silently incomplete render is a real, ordinary possibility, not an edge case.

Quick checkPredict output
template.render() is called with the required 'vlans' variable missing entirely. What happens, by Jinja2's default behavior?

There's a stricter option — StrictUndefined — that does raise loudly:

strict_undefined.py
from jinja2 import Environment, FileSystemLoader, StrictUndefined

strict_env = Environment(
  loader=FileSystemLoader("templates"),
  trim_blocks=True,
  lstrip_blocks=True,
  undefined=StrictUndefined,
)
strict_template = strict_env.get_template("vlan_config.j2")
strict_template.render()  # vlans still not passed

undefined=StrictUndefined changes the Environment's behavior: any missing variable now raises immediately instead of silently rendering empty.

Running this raises:

output
jinja2.exceptions.UndefinedError: 'vlans' is undefined

A real, loud failure — this is what most engineers actually want from missing template data, and it's an opt-in, not the default.

Quick checkConcept check
What changes about missing-variable behavior when an Environment uses undefined=StrictUndefined?

Validate the rendered text itself

StrictUndefined only catches a variable that's completely missing. It won't catch a template that's simply missing a line it should have — the template itself could have a typo, or a required field could be silently wrong rather than absent. The only way to actually be sure the output is right is to check the output itself:

validate_rendered_config.py
def validate_rendered_config(rendered_text, required_vlans):
  """Checks that every required VLAN's 'vlan <id>' line actually made it into the rendered text."""
  missing = [v["id"] for v in required_vlans if f"vlan {v['id']}" not in rendered_text]
  return {"valid": len(missing) == 0, "missing_vlan_ids": missing}

This doesn't care why a line is missing — a StrictUndefined exception avoided, a template typo, mismatched data — it just checks: is every required line actually present in what got rendered?

a_broken_template.j2
{% for vlan in vlans %}
name {{ vlan.name }}
{% endfor %}

A deliberately broken template — the 'vlan {{ vlan.id }}' line was accidentally deleted, but 'name' is still there. This renders without any error at all.

catch_it.py
rendered = broken_template.render(vlans=required_vlans)
print(rendered)
print(validate_rendered_config(rendered, required_vlans))

Run against the broken template above.

output
 name USERS
name VOICE

{'valid': False, 'missing_vlan_ids': [10, 20]}

The render 'succeeded' — no exception anywhere. But the VLAN ID lines are simply gone. validate_rendered_config() is the only thing in this whole pipeline that actually caught it.

The lesson in one sentence

Neither 'it rendered without an exception' nor 'StrictUndefined didn't complain' proves the configuration is correct. Only checking the actual rendered text against what's actually required does.

Quick checkScenario
A template has a typo that accidentally deletes the line that would configure a required VLAN's ID. The template still renders without raising any exception. What actually catches this problem?

Render → validate → deploy

  1. Structured datathis device's required VLANs
  2. Templateloaded from a real .j2 file
  3. Renderproduces configuration text
  4. Inspect / validatecheck the rendered text itself — not just that it rendered
  5. Dry-run previewReliable Automation's existing pattern
  6. Optional safe deploymentonly after every prior stage

This is the exact workflow Lesson 6 assembles into the module's capstone — rendered configuration is treated as a reviewed artifact, the same discipline Reliable Automation already applied to a device's live state, now applied one stage earlier, to the text a template produces before any connection is even opened.

Quick checkMyth check
Once a Jinja2 template renders without raising an exception, is the resulting configuration safe to deploy?

Key takeaways

  • A template deserves its own .j2 file, loaded with Environment(loader=FileSystemLoader(...)).get_template(...) — the same 'separate file, not embedded' pattern devices.csv already established.
  • Jinja2's default behavior silently renders a missing variable as empty, with no exception — verified directly, not assumed.
  • undefined=StrictUndefined makes a missing variable raise loudly instead — an opt-in stricter mode, not Jinja2's default.
  • Neither default nor strict Undefined catches a template that's simply missing a line — only checking the rendered output itself does.
  • 'Rendered without an error' is never the same as 'safe to deploy' — validating the actual rendered text is the layer that closes that gap, and it's what the capstone builds on next.

Done with this lesson?

Next lesson

A Template-Based Configuration Automation Project