Network Config Generator
Build a Python tool that generates Cisco router configs from YAML files.
Introduction
30 Second Summary
Network engineers configuring routers by hand type the same commands over and over, one mistake away from taking down a connection. The industry solved this years ago with automation scripts that generate configs from a single source of truth.
In this project, you will build a Python command-line tool that reads a network topology defined in YAML and automatically generates complete Cisco IOS-style router configurations.
What You'll Build
You run one command, and the tool produces a ready-to-paste config file for each router in your network, complete with interface settings, OSPF routing, VLANs, and banner messages.
By the end of this project, you'll have:
- A working config generator where you define three routers, their IPs, and OSPF areas in one YAML file and get back complete Cisco configs in seconds.
- Reusable Jinja2 templates that handle device-specific logic through conditionals, producing different config blocks for headquarters, branch, and WAN routers.
- A network summary report that documents your entire topology at a glance, listing every router, interface, and OSPF area in one readable file.
- Secret Mission: Add input validation that catches IP address errors and subnet conflicts before generating configs.
Are there any prerequisites?
None. This guide walks you through installing everything you need, including Python, the Jinja2 templating library, and PyYAML. No prior networking or Python experience is required.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
Set Up Your Project Environment
Your network config generator needs two Python libraries to work. Jinja2 handles template rendering, turning placeholder-filled files into complete Cisco router configurations.
PyYAML parses the structured data files that describe your network topology. In this step, you'll create a dedicated project folder, set up a virtual environment to keep your dependencies isolated, and install both libraries.
In this step, get ready to:
- Create the network-config-generator project folder on your Desktop.
- Set up a virtual environment and install Jinja2 and PyYAML.
- Create the folder structure for templates and output files.
Create your project folder
Every project needs a home. You'll create a folder on your Desktop so it's easy to find later.
- In VS Code, open a terminal by pressing Ctrl+` (the backtick key, usually above Tab).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir network-config-generator
cd network-config-generator
- Now open this folder in VS Code. Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop and select the network-config-generator folder, then click Open.
VS Code reloads with the network-config-generator folder name visible in the Explorer sidebar on the left.
Don't see the folder name in the sidebar?
Make sure you selected the network-config-generator folder itself, not the Desktop. The Explorer sidebar should show the folder name at the top.
If VS Code asked whether you trust the authors of the files in this folder, click Yes, I trust the authors.
Help me troubleshoot this
Create a virtual environment and install libraries
A virtual environment is an isolated copy of Python for this project alone. Libraries you install here won't affect other projects on your machine.
- Open a new terminal in VS Code by pressing Ctrl+` (backtick). This terminal starts inside your project folder automatically.
- Create and activate a virtual environment by running these commands:
macOS
python3 -m venv venv
source venv/bin/activate
Windows
python -m venv venv
venv\Scripts\activate
After activation, you should see (venv) appear at the beginning of your terminal prompt. This confirms you're working inside the virtual environment.
- Install the two libraries your project needs by running:
pip install Jinja2==3.1.6 PyYAML==6.0.3
What does this command do?
pip install downloads and installs Python packages from PyPI (the Python Package Index).
The == pins each library to an exact version. This ensures your project behaves consistently no matter when you run it.
You should see output listing the packages being downloaded and installed, ending with a success message.
Seeing a permissions error or install failure?
Make sure you see (venv) in your terminal prompt. If it's missing, the virtual environment is not activated. Run the activation command from the platform tab above again.
If you see an error about building wheels, try running the install command a second time. Temporary network issues sometimes cause the first attempt to fail.
Help me fix this install error
Create the project file structure
Your project needs a templates folder for Jinja2 template files, an output folder for generated configs, and two starter files.
- Create the folders and empty files by running these commands:
macOS
mkdir templates output
touch topology.yaml generate.py
Windows
mkdir templates
mkdir output
New-Item topology.yaml -ItemType File
New-Item generate.py -ItemType File
- Confirm everything was created by running:
ls
You should see generate.py, output, templates, topology.yaml, and venv listed in the output.
Missing a file or folder?
Check that your terminal prompt still shows (venv) and that you're in the network-config-generator folder. Run pwd (macOS) or cd with no arguments (Windows) to confirm your current directory.
If a file is missing, run the touch or New-Item command again for just that file.
Help me fix my missing files
Before you run the next command, what do you think will happen if the libraries installed correctly?
- Verify that both libraries are importable by running:
python -c "import jinja2; import yaml; print('Ready')"
You should see Ready printed in your terminal. This confirms Python can find both Jinja2 and PyYAML inside your virtual environment.
Seeing ModuleNotFoundError?
This means the libraries are not installed in your active environment. Confirm (venv) appears in your prompt, then run pip install Jinja2==3.1.6 PyYAML==6.0.3 again.
If you're on macOS and used python instead of python3 to create the venv, deactivate with deactivate, delete the venv folder, and recreate it with python3 -m venv venv.
Help me troubleshoot this import error
Your environment is locked in. Next up, you'll define your network topology in YAML and describe the routers, interfaces, and IP addresses your tool will generate configs for.
Define Your Network Topology in YAML
Your project environment is ready with Jinja2 and PyYAML installed. Now you need the data that describes your network.
Before you can generate configs, you need structured data describing every router, interface, and IP address in your network. YAML is the standard format used by tools like Ansible and Nautobot because it is human-readable and machine-parseable.
In this step, get ready to:
- Define three routers with hostnames, interfaces, IP addresses, and OSPF routing configuration.
- Add VLAN definitions and a banner message for enterprise network elements.
- Verify that Python can parse your topology file correctly.
Write the router definitions
A network topology file describes every device and connection in your network. You will define three routers for a small Philippine branch office: a headquarters router, a branch router, and a WAN router connecting them.
- In VS Code, open topology.yaml from the Explorer sidebar and paste the following code to define the file header and the headquarters router:
---
network_name: "PH Branch Office Network"
routers:
- hostname: "PH-HQ-RTR01"
role: "headquarters"
ospf:
process_id: 1
router_id: "1.1.1.1"
area: 0
interfaces:
- name: "GigabitEthernet0/0"
description: "Link to WAN router"
ip_address: "10.0.0.1"
subnet_mask: "255.255.255.252"
ospf_network: "10.0.0.0 0.0.0.3"
- name: "GigabitEthernet0/1"
description: "LAN - User segment"
ip_address: "192.168.20.1"
subnet_mask: "255.255.255.0"
ospf_network: "192.168.20.0 0.0.0.255"
- name: "GigabitEthernet0/2"
description: "LAN - Server segment"
ip_address: "192.168.30.1"
subnet_mask: "255.255.255.0"
ospf_network: "192.168.30.0 0.0.0.255"
What does this YAML define?
- --- is the YAML document start marker. It signals the beginning of the file to any parser.
- network_name is a top-level key that names your entire topology.
- routers is a list (indicated by the - prefix) where each item is one network device.
- ospf holds the OSPF routing configuration. The router_id uniquely identifies this router in the OSPF domain, and area groups interfaces for route summarization.
- Each interface has an ip_address, subnet_mask, and ospf_network (the network statement with wildcard mask that OSPF uses to advertise routes).
- The HQ router connects to both the LAN segments and the WAN. Next, add the branch and WAN routers below the last line of the HQ router by pasting this code at the bottom of topology.yaml:
- hostname: "PH-BRANCH-RTR01"
role: "branch"
ospf:
process_id: 1
router_id: "2.2.2.2"
area: 1
interfaces:
- name: "GigabitEthernet0/0"
description: "Link to WAN router"
ip_address: "10.0.0.5"
subnet_mask: "255.255.255.252"
ospf_network: "10.0.0.4 0.0.0.3"
- name: "GigabitEthernet0/1"
description: "Branch LAN"
ip_address: "192.168.40.1"
subnet_mask: "255.255.255.0"
ospf_network: "192.168.40.0 0.0.0.255"
What is different about the branch router?
The branch router is in OSPF area 1 while the HQ router is in area 0 (the backbone area). This multi-area design is how real enterprise networks limit the size of routing tables at each site.
The /30 subnet mask (255.255.255.252) on the WAN-facing interface means only two usable IP addresses exist on that link. This is standard practice for point-to-point router connections.
- Save topology.yaml and confirm your YAML is valid by running this command in your terminal:
python -c "import yaml; data = yaml.safe_load(open('topology.yaml')); print(f'{len(data[\"routers\"])} routers loaded')"
You should see 2 routers loaded printed in the terminal.
Seeing a YAML parsing error?
- YAML is sensitive to indentation. Make sure the branch router's - hostname line is indented at the same level as the HQ router's - hostname line (exactly 2 spaces under routers:).
- Check that you have no tab characters. YAML only allows spaces for indentation.
- Make sure the blank line between the HQ and branch routers does not contain any hidden whitespace characters.
Help me fix a YAML indentation error in my topology file.
- Now add the WAN router below the branch router by pasting this code at the bottom of topology.yaml:
- hostname: "PH-WAN-RTR01"
role: "wan"
ospf:
process_id: 1
router_id: "3.3.3.3"
area: 0
interfaces:
- name: "GigabitEthernet0/0"
description: "Link to HQ router"
ip_address: "10.0.0.2"
subnet_mask: "255.255.255.252"
ospf_network: "10.0.0.0 0.0.0.3"
- name: "GigabitEthernet0/1"
description: "Link to Branch router"
ip_address: "10.0.0.6"
subnet_mask: "255.255.255.252"
ospf_network: "10.0.0.4 0.0.0.3"
- name: "GigabitEthernet0/2"
description: "ISP uplink"
ip_address: "203.0.113.2"
subnet_mask: "255.255.255.252"
Why does the WAN router have three interfaces?
The WAN router sits between the HQ, the branch, and the ISP. GigabitEthernet0/0 connects to HQ, GigabitEthernet0/1 connects to the branch, and GigabitEthernet0/2 is the ISP uplink.
The ISP-facing interface uses 203.0.113.2, a public IP from the documentation range (RFC 5737). In a real deployment this would be your assigned public address.
- Save topology.yaml and run the same check again:
python -c "import yaml; data = yaml.safe_load(open('topology.yaml')); print(f'{len(data[\"routers\"])} routers loaded')"
You should now see 3 routers loaded. All three devices are defined and Python can parse them.
Still seeing 2 routers?
- Make sure the WAN router's - hostname line starts with exactly 2 spaces followed by a dash, matching the other routers.
- Confirm you saved the file after pasting the WAN router block.
Why is my YAML only loading 2 routers instead of 3?
Add VLANs and a banner message
Enterprise networks use VLANs to segment traffic and banner messages to warn unauthorized users. These are common elements that your generated configs will include alongside the interface and routing blocks.
- In topology.yaml, find the line that says routers: and add the following block directly above it (between network_name and routers:):
banner_message: "Authorized access only. All activity is monitored."
vlans:
- id: 10
name: "MANAGEMENT"
- id: 20
name: "USERS"
- id: 30
name: "SERVERS"
What are VLANs in a network topology?
A VLAN (Virtual LAN) logically separates network traffic on the same physical switch. VLAN 10 isolates management traffic from user traffic on VLAN 20, and server traffic on VLAN 30.
The banner_message is the legal notice displayed when someone connects to the router. It is required by most enterprise security policies.
- While you are in the file, add a DHCP pool configuration to the HQ router. Find the line area: 0 under the HQ router's ospf section and paste the following block between area: 0 and interfaces::
dhcp_pool:
name: "USERS_POOL"
network: "192.168.20.0"
mask: "255.255.255.0"
default_router: "192.168.20.1"
dns_server: "8.8.8.8"
Why does the HQ router have a DHCP pool?
In a typical branch office setup, the headquarters router hands out IP addresses to user devices automatically via DHCP. The pool defines which network range to assign from, the default gateway, and the DNS server clients should use.
- Save topology.yaml.
Before you run the verification, what do you think the output will be? Your file still has 3 routers, so the count should be unchanged.
- Confirm the complete topology loads by running this command:
python -c "import yaml; data = yaml.safe_load(open('topology.yaml')); print(f'Loaded {len(data[\"routers\"])} routers')"
You should see Loaded 3 routers printed. The file now contains your full network topology with routers, VLANs, a banner message, and a DHCP pool.
Getting a KeyError or None value?
- Check that banner_message and vlans are at the top level (no leading spaces), at the same indentation as network_name and routers.
- Ensure the dhcp_pool block is indented at the same level as ospf and interfaces (4 spaces under the router's - hostname line).
- Make sure there is a blank line before routers: separating it from the vlans section.
Help me debug my topology.yaml after adding VLANs and DHCP pool.
✔️ Awesome, I've got everything!
Great. Double check that you have saved topology.yaml and the terminal prints Loaded 3 routers.
ⓧ I'd like to double check the full code
Your topology.yaml should contain these sections in this exact order. Scroll through your file and confirm each one is present:
- Line 1: --- (document start marker).
- network_name set to "PH Branch Office Network".
- banner_message set to "Authorized access only. All activity is monitored.".
- vlans list with 3 items (IDs 10, 20, 30).
- routers list starting with PH-HQ-RTR01 (headquarters, OSPF area 0, dhcp_pool, 3 interfaces).
- PH-BRANCH-RTR01 (branch, OSPF area 1, 2 interfaces).
- PH-WAN-RTR01 (wan, OSPF area 0, 3 interfaces).
Your network topology is fully defined. Next up, you will build a Jinja2 template that transforms this YAML data into real Cisco IOS configuration files.
Build Your First Jinja2 Template
In the previous step, you defined your entire network topology in YAML. You have three routers, their interfaces, IP addresses, and OSPF settings all structured as data.
Now you need a way to transform that data into actual Cisco IOS configuration files. A Jinja2 template defines the shape of the output, using placeholders that get filled with your YAML data. This is exactly how Ansible generates device configs under the hood.
In this step, get ready to:
- Create a Jinja2 template that outputs valid Cisco IOS router configuration.
- Write a Python script that loads YAML data and renders it through the template.
- Run the generator and inspect the output config files.
Create the router configuration template
A Jinja2 template looks like a normal text file with special placeholder syntax. Double curly braces {{ }} insert values, and {% %} tags handle logic like loops and conditionals.
- Create a new file called router.j2 inside the templates folder. Copy the following code into it:
!
! Configuration for {{ hostname }}
! Role: {{ role }}
! Generated by Network Config Generator
!
hostname {{ hostname }}
!
banner motd ^
{{ banner_message }}
^
!
{% if role == "wan" %}
ip access-list extended OUTSIDE-IN
permit icmp any any
deny ip any any log
!
{% endif %}
{% for interface in interfaces %}
interface {{ interface.name }}
description {{ interface.description }}
ip address {{ interface.ip_address }} {{ interface.subnet_mask }}
no shutdown
!
{% endfor %}
What does this template do?
The template starts with a config header, sets the hostname, and adds a login banner. The {% if role == "wan" %} block only includes an access-list for WAN-facing routers.
The {% for interface in interfaces %} loop iterates over each router's interfaces and generates the standard Cisco IOS commands: interface name, description, IP addressing, and no shutdown to activate the port.
- Still in templates/router.j2, add the following code directly below the {% endfor %} you just typed:
{% if vlans is defined %}
{% for vlan in vlans %}
vlan {{ vlan.id }}
name {{ vlan.name }}
!
{% endfor %}
{% endif %}
{% if dhcp_pool is defined %}
ip dhcp pool {{ dhcp_pool.name }}
network {{ dhcp_pool.network }} {{ dhcp_pool.mask }}
default-router {{ dhcp_pool.default_router }}
dns-server {{ dhcp_pool.dns_server }}
!
{% endif %}
router ospf {{ ospf.process_id }}
router-id {{ ospf.router_id }}
{% for interface in interfaces %}
{% if interface.ospf_network is defined %}
network {{ interface.ospf_network }} area {{ ospf.area }}
{% endif %}
{% endfor %}
!
end
What does this section do?
The VLAN block assigns VLAN IDs and names. The {% if dhcp_pool is defined %} conditional only generates DHCP config for routers that have a pool defined in the YAML (like the HQ router).
The OSPF section configures the routing process, sets the router ID, and advertises each interface's network into the correct OSPF area. The end keyword closes the Cisco IOS configuration.
- Save templates/router.j2.
Template not saving?
Make sure you created the file inside the templates folder, not in the project root. The path should be templates/router.j2.
Help me troubleshoot my template file location
- Create another file called summary.j2 inside the templates folder. Copy the following code into it:
=====================================
NETWORK SUMMARY REPORT
{{ network_name }}
=====================================
Total Routers: {{ routers | length }}
{% for router in routers %}
-------------------------------------
Device: {{ router.hostname }}
Role: {{ router.role | upper }}
OSPF: Process {{ router.ospf.process_id }}, Router-ID {{ router.ospf.router_id }}, Area {{ router.ospf.area }}
Interfaces:
{% for interface in router.interfaces %}
{{ interface.name }}
IP: {{ interface.ip_address }} / {{ interface.subnet_mask }}
Description: {{ interface.description }}
{% endfor %}
{% endfor %}
-------------------------------------
VLANs Configured:
{% for vlan in vlans %}
VLAN {{ vlan.id }} - {{ vlan.name }}
{% endfor %}
=====================================
Report generated successfully.
=====================================
What is this summary template for?
This template produces a quick-reference report listing all routers, their IPs, and OSPF areas in one view. Network engineers use reports like this for documentation and auditing.
The | length and | upper syntax uses Jinja2 filters to transform values inline.
- Save templates/summary.j2.
- Confirm both template files exist by running:
ls templates/
You should see router.j2 and summary.j2 listed.
Write the Python generator script
The generator script ties everything together. It loads the YAML topology, feeds the data into your Jinja2 templates, and writes the rendered output to individual .cfg files.
- Open generate.py in VS Code and add the imports and the YAML loading function:
import os
import yaml
from jinja2 import Environment, FileSystemLoader
def load_topology(filepath):
"""Load network topology from a YAML file."""
with open(filepath, "r") as f:
return yaml.safe_load(f)
What does this code do?
The import lines bring in the tools you need: os for file operations, yaml for parsing your topology file, and Environment plus FileSystemLoader from Jinja2 for template rendering.
The load_topology() function reads a YAML file and returns its contents as a Python dictionary using yaml.safe_load().
- Below the load_topology function, add the generate_configs function that sets up the template environment and renders each router:
def generate_configs(topology):
"""Generate router configuration files from topology data."""
env = Environment(
loader=FileSystemLoader("templates"),
trim_blocks=True,
lstrip_blocks=True,
)
router_template = env.get_template("router.j2")
summary_template = env.get_template("summary.j2")
os.makedirs("output", exist_ok=True)
for router in topology["routers"]:
config_data = {
"hostname": router["hostname"],
"role": router["role"],
"interfaces": router["interfaces"],
"ospf": router["ospf"],
"banner_message": topology["banner_message"],
"vlans": topology.get("vlans"),
"dhcp_pool": router.get("dhcp_pool"),
}
config_output = router_template.render(config_data)
output_filename = f"output/{router['hostname']}.cfg"
with open(output_filename, "w") as f:
f.write(config_output)
print(f"Generated: {output_filename}")
What does this function do?
The Environment object configures Jinja2 to load templates from the templates folder. Setting trim_blocks=True removes blank lines after template tags, and lstrip_blocks=True strips leading whitespace. Together they produce clean output without extra blank lines.
The for loop iterates over each router in the topology, builds a dictionary of that router's data, renders the template with .render(), and writes the result to a .cfg file named after the router's hostname.
- Still in generate.py, add the summary rendering and the main function below the last print statement (still inside generate_configs), then add the entry point:
summary_output = summary_template.render(topology)
with open("output/network_summary.txt", "w") as f:
f.write(summary_output)
print("Generated: output/network_summary.txt")
def main():
topology = load_topology("topology.yaml")
print(f"Loaded topology: {topology['network_name']}")
print(f"Routers to configure: {len(topology['routers'])}")
print()
generate_configs(topology)
print()
print("All configurations generated successfully.")
if __name__ == "__main__":
main()
What does this code do?
The summary rendering passes the full topology dictionary to the summary.j2 template and writes a network_summary.txt file.
The main() function orchestrates everything: load the YAML, print what was found, generate all configs, and confirm success. The if __name__ == "__main__" guard ensures main() only runs when you execute the script directly.
- Save generate.py.
✔️ Awesome, I've got everything!
Great. Double check you have saved both generate.py and both template files.
ⓧ I'd like to double check the full code
Your generate.py is built from three chunks. Scroll up and confirm, in order:
- Chunk 1: imports (os, yaml, jinja2) and the load_topology() function.
- Chunk 2: the generate_configs() function with the Environment setup and the router rendering loop.
- Chunk 3: the summary rendering lines (still indented inside generate_configs), then the main() function and the if __name__ guard.
Seeing an IndentationError?
The summary rendering lines (summary_output = ...) must be indented one level inside generate_configs. They sit at the same indentation as the for router loop above them. If you accidentally outdented them, Python will raise an error.
Help me fix indentation errors
Run the generator and inspect the output
Everything is in place. Before you run the script, think about what you expect to see. You have 3 routers in your YAML topology.
- How many .cfg files do you think will appear in the output folder? What will they be named?
- Run the generator by executing:
python generate.py
You should see output similar to this:
Loaded topology: PH Branch Office Network
Routers to configure: 3
Generated: output/PH-HQ-RTR01.cfg
Generated: output/PH-BRANCH-RTR01.cfg
Generated: output/PH-WAN-RTR01.cfg
Generated: output/network_summary.txt
All configurations generated successfully.
- Open output/PH-HQ-RTR01.cfg in VS Code to inspect the generated config.
You should see a complete Cisco IOS configuration with the hostname set to PH-HQ-RTR01, three interface blocks with the correct IP addresses from your YAML, VLAN definitions, a DHCP pool, and OSPF routing statements.
- Open output/PH-WAN-RTR01.cfg and confirm that it includes the ip access-list extended OUTSIDE-IN section (because this router has the wan role).
- Open output/PH-BRANCH-RTR01.cfg and confirm it does NOT have the access-list or DHCP pool sections.
Why are the configs different?
Your template uses {% if role == "wan" %} and {% if dhcp_pool is defined %} to conditionally include sections. Only routers that match these conditions get those config blocks. This is how production tools like Ansible handle device-specific logic.
Seeing a TemplateNotFound error?
This means Python cannot find router.j2 or summary.j2 in the templates folder. Make sure both files are saved inside the templates/ directory (not the project root) and that the filenames match exactly.
Help me fix my template path
Your config generator is working. You defined a network in YAML and got back real Cisco IOS configurations in seconds. Next up, you will add template logic to handle the different roles each router plays in the network.
Handle Multi-Device Differences with Template Logic
In the previous step, you built a template that generates complete Cisco configs and a Python script that renders each router through it.
Real networks have devices with different roles. Your HQ router needs a DHCP pool to serve user addresses, while the WAN router needs an access-list to filter inbound traffic. The branch router needs neither. Jinja2 conditionals let a single template produce role-specific configs by checking data fields in your YAML. In this step, you will trace how those conditionals work and verify that each router gets only the config blocks it needs.
In this step, get ready to:
- Understand how the role field and dhcp_pool data in YAML drive conditional template rendering.
- Verify that the HQ config includes DHCP, the WAN config includes an access-list, and the branch config has neither.
Trace how roles drive template conditionals
Your topology.yaml already assigns a role to each router and places a dhcp_pool block only on the headquarters device. The template uses these fields to decide what config blocks to include.
- In VS Code, open topology.yaml and scroll through the three router entries.
- Notice that PH-HQ-RTR01 has role: "headquarters" and a dhcp_pool section beneath it.
- Check that PH-BRANCH-RTR01 has role: "branch" and no dhcp_pool key at all.
- Confirm that PH-WAN-RTR01 has role: "wan" and also no dhcp_pool.
Now open templates/router.j2 and find the two conditional blocks that use this data.
- Find the block starting with {% if role == "wan" %}. This only renders the access-list for WAN routers.
- Find the block starting with {% if dhcp_pool is defined %}. This only renders DHCP configuration when the YAML includes a dhcp_pool key for that router.
How do these conditionals work together?
When Jinja2 renders the template for each router, it checks the data passed in. If the router's role value is "wan", the access-list block is included. If it is any other value, that entire block is skipped.
The DHCP block uses a different pattern: is defined checks whether the key exists at all. Since only the HQ router has a dhcp_pool key in the YAML, only its config gets DHCP commands.
Why two different conditional patterns?
Checking role == "wan" is useful when the feature is tied to a specific role. Checking is defined is better when the feature is optional and could appear on any device. This flexibility means you can later add a DHCP pool to a branch router just by adding the key to its YAML entry.
Run the generator and compare the output files
Now that you understand the conditional logic, run the generator and verify each router's config only contains the blocks it should have.
Before you run this, predict: which of the three output files will contain a ip dhcp pool section, and which will contain ip access-list extended?
- Regenerate all configs by running this command in your terminal:
python generate.py
You should see output confirming all three configs and the summary were generated.
- Open output/PH-HQ-RTR01.cfg in VS Code.
- Scroll through and confirm you see the ip dhcp pool USERS_POOL section with the network, default-router, and dns-server lines.
- Confirm there is NO ip access-list extended block in this file.
- Open output/PH-WAN-RTR01.cfg.
- Confirm you see the ip access-list extended OUTSIDE-IN section near the top, with permit and deny lines.
- Confirm there is NO ip dhcp pool block in this file.
- Open output/PH-BRANCH-RTR01.cfg.
- Confirm this file has NEITHER an ip dhcp pool section NOR an ip access-list extended section. The branch router gets only interfaces, VLANs, and OSPF.
What should I see in each file?
- PH-HQ-RTR01.cfg: hostname, banner, 3 interfaces, VLANs, DHCP pool (USERS_POOL), OSPF. No access-list.
- PH-WAN-RTR01.cfg: hostname, banner, access-list OUTSIDE-IN, 3 interfaces, VLANs, OSPF. No DHCP pool.
- PH-BRANCH-RTR01.cfg: hostname, banner, 2 interfaces, VLANs, OSPF. No DHCP pool and no access-list.
All three configs look identical?
- Check that your topology.yaml has a role field on each router ("headquarters", "branch", "wan").
- Verify the dhcp_pool key exists ONLY under PH-HQ-RTR01 in your YAML. If it is on all three routers, all three configs will include DHCP.
- In router.j2, check that the access-list block is wrapped in {% if role == "wan" %} and the DHCP block is wrapped in {% if dhcp_pool is defined %}.
My router configs all look the same even though I have different roles set. Help me debug.
Verify the template produces different configs per role
For a final confirmation, use a quick terminal command to search for the role-specific keywords across all output files at once.
- Check which config files contain DHCP by running:
grep -l "ip dhcp pool" output/*.cfg
You should see only output/PH-HQ-RTR01.cfg listed.
- Check which config files contain the access-list by running:
grep -l "access-list extended" output/*.cfg
You should see only output/PH-WAN-RTR01.cfg listed. Each router gets only the config blocks that match its role and data in the YAML.
This pattern scales to real networks
Production network automation tools like Ansible use the exact same approach. A single template handles dozens of device roles by checking variables in the inventory data. You just learned the foundational pattern those tools are built on.
Your template now produces tailored configs for every device role in the network. Next up, you will generate a summary report that shows all routers, interfaces, and OSPF areas in one view for documentation and auditing.
Generate a Summary Report
Your config generator now produces role-specific configs that handle the differences between HQ, branch, and WAN routers.
In production, network engineers need to verify what changed across all devices at a glance. Opening three separate .cfg files to cross-reference IP assignments and OSPF areas is slow and error-prone. A single summary report that shows every router, every interface, and every area membership in one view makes your tool useful for documentation and auditing.
In this step, get ready to:
- Create a summary template that iterates over all routers and formats a text report.
- Run the generator and confirm the summary report appears alongside the device configs.
Create the summary template
Your router template renders one device at a time. The summary template takes the full topology and loops through every router in a single pass, producing one consolidated report.
- Create a new file called summary.j2 inside your templates folder.
- Paste the following template code into templates/summary.j2:
=====================================
NETWORK SUMMARY REPORT
{{ network_name }}
=====================================
Total Routers: {{ routers | length }}
{% for router in routers %}
-------------------------------------
Device: {{ router.hostname }}
Role: {{ router.role | upper }}
OSPF: Process {{ router.ospf.process_id }}, Router-ID {{ router.ospf.router_id }}, Area {{ router.ospf.area }}
Interfaces:
{% for interface in router.interfaces %}
{{ interface.name }}
IP: {{ interface.ip_address }} / {{ interface.subnet_mask }}
Description: {{ interface.description }}
{% endfor %}
{% endfor %}
-------------------------------------
VLANs Configured:
{% for vlan in vlans %}
VLAN {{ vlan.id }} - {{ vlan.name }}
{% endfor %}
=====================================
Report generated successfully.
=====================================
What does this template do?
- {{ routers | length }} uses a Jinja2 filter to count the total number of routers in your topology.
- {{ router.role | upper }} converts the role to uppercase for a clean report format (e.g. "headquarters" becomes "HEADQUARTERS").
- The nested {% for interface in router.interfaces %} loop prints every interface's IP and description under each router.
- The final {% for vlan in vlans %} loop lists all VLANs at the bottom of the report for a complete network overview.
- Save templates/summary.j2.
Why don't I need to update generate.py?
Your generate.py already loads summary.j2 and renders it after the router configs. The summary_template = env.get_template("summary.j2") line and the rendering block at the end of generate_configs() handle everything. You just needed to create the template file it references.
Run the generator and inspect the summary report
Before you run this, what do you expect the terminal output to look like now that you have a summary template in place?
- Run the generator in your terminal:
python generate.py
You should see the three router config lines followed by a new line: Generated: output/network_summary.txt.
- Confirm the summary file exists by running:
ls output
You should see network_summary.txt listed alongside your three .cfg files.
- Open output/network_summary.txt in VS Code to inspect the report.
The report lists all 3 routers with their interfaces, IP addresses, OSPF areas, and roles. The VLAN section at the bottom shows all three VLANs (MANAGEMENT, USERS, SERVERS).
Seeing a TemplateNotFound error?
- Make sure your file is named exactly summary.j2 (not summary.j2.txt or Summary.j2). File names are case-sensitive.
- Confirm the file is inside the templates folder, not in the project root.
Why is my summary template not being found?
✔️ Awesome, I've got everything!
Great. Double check you have saved templates/summary.j2.
ⓧ I'd like to double check the full code
Here is the complete templates/summary.j2 file. Make sure yours matches exactly:
=====================================
NETWORK SUMMARY REPORT
{{ network_name }}
=====================================
Total Routers: {{ routers | length }}
{% for router in routers %}
-------------------------------------
Device: {{ router.hostname }}
Role: {{ router.role | upper }}
OSPF: Process {{ router.ospf.process_id }}, Router-ID {{ router.ospf.router_id }}, Area {{ router.ospf.area }}
Interfaces:
{% for interface in router.interfaces %}
{{ interface.name }}
IP: {{ interface.ip_address }} / {{ interface.subnet_mask }}
Description: {{ interface.description }}
{% endfor %}
{% endfor %}
-------------------------------------
VLANs Configured:
{% for vlan in vlans %}
VLAN {{ vlan.id }} - {{ vlan.name }}
{% endfor %}
=====================================
Report generated successfully.
=====================================
Your network config generator now produces both individual device configs and a consolidated summary report. This is the same pattern that production tools like Ansible use to give engineers a single-pane view of their network state. Next up, you will tackle the Secret Mission and add input validation to catch errors before they reach your devices.
Secret mission
Add Input Validation
Your config generator works perfectly with correct data. But what happens when someone makes a typo in an IP address or accidentally assigns the same subnet to two interfaces? Add defensive validation that catches these errors before they reach your network devices.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The network-config-generator project folder on your Desktop.
- The venv virtual environment inside the project folder (contains Jinja2 and PyYAML).
- Generated config files in the output/ folder.
Keep everything running
No action needed. Choose this if you want to keep your project as a portfolio piece or continue building on it.
- Your project stays in the network-config-generator folder on your Desktop, ready to run anytime.
- Push to a GitHub repository with a clear README documenting what the tool does, how to run it, and what networking concepts it demonstrates.
- You can re-run python generate.py whenever you update the topology.
Pause - I'll come back to this later
Delete the virtual environment to free up disk space. Your code, templates, and generated configs remain intact.
- Deactivate the virtual environment by running:
deactivate
- Delete the venv folder on macOS/Linux by running:
rm -rf venv
- On Windows, run:
rmdir /s /q venv
- When you come back, recreate the virtual environment and reinstall dependencies by running python -m venv venv followed by pip install Jinja2==3.1.6 PyYAML==6.0.3.
Delete - I don't want to use this again
Remove all project files and start fresh. This deletes everything you built in this project.
- Deactivate the virtual environment by running:
deactivate
- Navigate to your Desktop by running:
cd ~/Desktop
- Delete the entire project folder on macOS/Linux by running:
rm -rf network-config-generator
- On Windows, run:
rmdir /s /q network-config-generator
- This removes generate.py, topology.yaml, the templates/ folder, the output/ folder, and the venv virtual environment.
Nice Work!
Nice Work!
Nice work. You just built a production-style network automation tool that generates complete Cisco IOS configurations from structured YAML data.
You've learned how to:
- Built a Python CLI tool that generates complete Cisco IOS configurations from YAML topology data using Jinja2 templating.
- Defined a multi-router network topology in YAML, the industry-standard format used by tools like Ansible and Nautobot for network source-of-truth systems.
- Used template conditionals to handle different device roles (headquarters, branch, WAN) and generated a network summary report for documentation and auditing.
- Secret Mission: Added input validation using Python's ipaddress module to catch IP address errors and subnet conflicts before they reach devices.
Ready to quiz yourself?