Configuration & Template Automation
Rendering, Validating, and Reviewing Configuration Safely
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
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.
{% 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.
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:
vlan 10
name USERS
vlan 20
name VOICESame output as before — only where the template text came from changed, from an inline Python string to a real file on disk.
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?
result = template.render() # no vlans passed at all
print(repr(result))No exception, no crash — just call render() with nothing.
This prints:
''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.
There's a stricter option — StrictUndefined — that does raise loudly:
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 passedundefined=StrictUndefined changes the Environment's behavior: any missing variable now raises immediately instead of silently rendering empty.
Running this raises:
jinja2.exceptions.UndefinedError: 'vlans' is undefinedA real, loud failure — this is what most engineers actually want from missing template data, and it's an opt-in, not the default.
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:
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?
{% 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.
rendered = broken_template.render(vlans=required_vlans)
print(rendered)
print(validate_rendered_config(rendered, required_vlans))Run against the broken template above.
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.
Render → validate → deploy
- Structured datathis device's required VLANs
- Templateloaded from a real .j2 file
- Renderproduces configuration text
- Inspect / validatecheck the rendered text itself — not just that it rendered
- Dry-run previewReliable Automation's existing pattern
- 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.
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