🧪 Lab 01 · Data-Driven Config Generation (Jinja2 & YAML)¶
✅ Validated on Arista cEOS 4.32.0F & Python 3.11.
Time: ~40 minutes · Tools: Python 3, PyYAML, Jinja2
Quick Start — Step-by-Step Execution Guide (Location: labs/netdevops-lab/)
Step 1 · Deploy the Lab Fabric (if not already running)
cd labs/netdevops-lab
sudo containerlab deploy -t topology.clab.yml --max-workers 1
Step 2 · Launch the Fully Guided Interactive Walkthrough
./run.sh --guided
Alternative Execution Options (Automated Push or Manual CLI)
- Fast Automated Script Push:
./run.sh 01 # apply + verify step 01 automatically ./run.sh --all # run all steps in order - Manual Line-by-Line CLI Execution:
Interactive CLI shell on any container node:
docker exec -it clab-netdevops-lab-node1 Cli
🧠 Technology Deep Dive: Separating Data from Logic¶
In traditional network engineering, device hostname, IP addresses, and routing parameters are hardcoded directly into vendor CLI strings. If you need to change an OSPF area ID across 100 leaf switches, you have to edit 100 individual configuration files!
In NetDevOps, we separate Data from Templates:
+-------------------+ +-------------------+ +-------------------+
| DATA MODEL | | JINJA2 TEMPLATE | | RENDERED OUTPUT |
| (data/hosts.yaml)| +===>| (templates/*.j2) | +===>| (rendered/*.cfg) |
| Raw IP parameters| | CLI Structure | | Valid Vendor CLI |
+-------------------+ +-------------------+ +-------------------+
1. YAML Data Model Breakdown (data/hosts.yaml)¶
YAML (YAML Ain't Markup Language) stores structured network data using simple key-value pairs (key: value), lists (- item), and nested dictionaries:
fabric:
name: NetForge-DC1
asn: 65000
ospf_area: 0.0.0.0
spines:
- name: spine1
mgmt_ip: 172.20.20.31
router_id: 10.255.0.1
interfaces:
- name: Ethernet1
ip: 10.0.1.1/30
neighbor: leaf1
fabric: Global network variables (ASN65000, OSPF Area0.0.0.0).spines: A list (-) of spine router dictionaries containing interface arrays.
2. Jinja2 Template Breakdown (templates/spine.j2)¶
Jinja2 is a templating engine for Python that dynamically generates text files:
- {{ variable }}: Inserts a data value (e.g. {{ node.name }} → spine1).
- {% for item in list %}: Loops through an array of interfaces.
configure
hostname {{ node.name }}
!
interface Loopback0
ip address {{ node.router_id }}/32
ip ospf area {{ fabric.ospf_area }}
!
{% for intf in node.interfaces %}
interface {{ intf.name }}
no switchport
ip address {{ intf.ip }}
ip ospf area {{ fabric.ospf_area }}
!
{% endfor %}
3. Python Rendering Script (scripts/generate_configs.py)¶
Python reads the YAML file, passes data to Jinja2, and saves the generated .cfg files:
import os, yaml
from jinja2 import Environment, FileSystemLoader
with open("data/hosts.yaml", "r") as f:
data = yaml.safe_load(f)
env = Environment(loader=FileSystemLoader("templates"))
template = env.get_template("spine.j2")
for spine in data["spines"]:
rendered = template.render(node=spine, fabric=data["fabric"])
with open(f"rendered/{spine['name']}.cfg", "w") as f:
f.write(rendered)
Step 1 · Generate Configurations¶
Run the Jinja2 generator script to produce device configurations:
cd labs/netdevops-lab
python3 scripts/generate_configs.py
Rendered: labs/netdevops-lab/rendered/spine1.cfg
Rendered: labs/netdevops-lab/rendered/spine2.cfg
Rendered: labs/netdevops-lab/rendered/leaf1.cfg
Rendered: labs/netdevops-lab/rendered/leaf2.cfg
✅ Configuration rendering complete!
Step 2 · Inspect Rendered Output (rendered/spine1.cfg)¶
Verify that Jinja2 generated a 100% valid EOS configuration:
configure
hostname spine1
!
interface Loopback0
ip address 10.255.0.1/32
ip ospf area 0.0.0.0
!
interface Ethernet1
no switchport
ip address 10.0.1.1/30
ip ospf area 0.0.0.0
!
interface Ethernet2
no switchport
ip address 10.0.1.5/30
ip ospf area 0.0.0.0
!
router ospf 100
router-id 10.255.0.1
passive-interface Loopback0
✅ DONE when rendered/spine1.cfg contains valid EOS commands generated from hosts.yaml.