Automate a Network Tech Refresh with AI
Generate multi-vendor network configs and validate them with Claude AI.
Introduction
30 Second Summary
When a company refreshes hundreds of network devices across multiple data centers, engineers need to generate and validate configurations for every single device. Doing this manually is slow and full of risk.
In this project, you will build a Python tool that generates multi-vendor network configurations from a YAML inventory using Jinja2 templates. You will then connect Claude AI to validate those configs and produce tech refresh planning documentation.
What You'll Build
A network tech refresh automation pipeline that generates, validates, and documents device configurations for Cisco NX-OS, Arista EOS, Palo Alto, and FortiGate platforms.
By the end of this project, you'll have:
- A multi-vendor device inventory in YAML spanning two data center sites with Cisco, Arista, Palo Alto, and FortiGate devices.
- Modular Jinja2 templates for switching, routing (BGP, OSPF, IPv6, QoS), VXLAN BGP EVPN overlays, and firewall policies.
- An AI-powered review pipeline that sends configs to Claude for validation and generates a tech refresh plan and SDN technology comparison.
- An interactive CLI that lets you select individual devices and compare old configs against new ones (Secret Mission).
Are there any prerequisites?
You need Python 3 installed on macOS. This project walks you through installing all required Python packages and setting up your Anthropic API account.
Before We Start
Before We Start...
Set Up Your Environment
Before you can automate network configs, you need the right tools in place. This project uses three Python libraries to generate, validate, and document device configurations for a multi-vendor tech refresh.
In this step, you will install those libraries, set up your Claude AI API access, and create the project folder structure.
In this step, get ready to:
- Install the Python packages for templating, YAML parsing, and AI integration.
- Create an Anthropic API account and configure your API key.
- Set up the project directory structure with template and output folders.
Install Python packages
This project depends on three Python libraries. Jinja2 handles config templating, PyYAML parses your device inventory, and the Anthropic SDK connects your scripts to the Claude API.
- Install all three packages by running this command:
pip3 install Jinja2==3.1.6 PyYAML==6.0.3 anthropic==0.97.0
What are these packages?
- Jinja2 is a templating engine. Network engineers use it to generate device configs from reusable templates. It is the industry standard for network config automation.
- PyYAML reads and writes YAML files. Your multi-vendor device inventory is defined in YAML.
- anthropic is the official Python SDK for the Claude AI API. Your scripts use it to send generated configs to Claude for validation and documentation.
- Confirm each package installed correctly by running these commands:
python3 -c "import jinja2; print(f'Jinja2 {jinja2.__version__}')"
python3 -c "import yaml; print(f'PyYAML {yaml.__version__}')"
python3 -c "import anthropic; print(f'Anthropic SDK {anthropic.__version__}')"
You should see Jinja2 3.1.6, PyYAML 6.0.3, and Anthropic SDK 0.97.0 printed on separate lines.
Seeing a ModuleNotFoundError?
- Make sure you used pip3 (not pip) to match your python3 installation.
- If you have multiple Python versions installed, check which one pip3 is linked to by running pip3 --version.
- If you see a permissions error, try adding --user to the end of the install command.
Why can't Python find my installed packages?
Create an Anthropic API account and get your API key
Your Python scripts send generated configs to the Claude API for validation and documentation. To connect, you need an API key from your Anthropic account.
- Open your browser and go to console.anthropic.com.
- Click Sign Up and create a free account.
How much does this cost?
New Anthropic accounts receive approximately $5 in free API credits with no credit card required. This project uses three API calls with the Claude Haiku model, which costs well under $1 total.
- Once logged in, click API Keys in the left sidebar.
- Click Create Key.
- Copy the key immediately. You will not be able to see it again after closing the dialog.
- Set your API key as an environment variable by running this command in your terminal, replacing your-api-key-here with the key you just copied:
export ANTHROPIC_API_KEY="your-api-key-here"
What does this command do?
The Anthropic Python SDK automatically reads your API key from an environment variable called ANTHROPIC_API_KEY. Setting it here means your Python scripts can connect to Claude without you hardcoding the key into your code.
- Make this variable available in future terminal sessions by adding it to your shell profile. Run this command, again replacing your-api-key-here with your actual key:
echo 'export ANTHROPIC_API_KEY="your-api-key-here"' >> ~/.zshrc
Pro tip: verify your key is set
Run echo $ANTHROPIC_API_KEY in your terminal. You should see your key printed back. If it is blank, re-run the export command.
Key not working later in the project?
- If you opened a new terminal tab or window, the export from this session will not carry over. Run source ~/.zshrc to reload your shell profile.
- Make sure you replaced your-api-key-here with your actual API key in both the export and echo commands.
Why is my Anthropic API key not being recognized?
Create the project directory structure
The project organizes templates and generated output into separate directories. This keeps your Jinja2 template files separate from the rendered configs that the generator produces.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create the project folder with its subdirectories and move into it by running these commands:
mkdir -p tech-refresh-automation/templates tech-refresh-automation/output
cd tech-refresh-automation
What does this create?
- tech-refresh-automation/ is your project root folder.
- templates/ holds your Jinja2 template files for switching, routing, VXLAN, and firewall configs.
- output/ is where the generator writes the rendered per-device config files and AI documentation.
- Confirm the folders were created by running:
ls
You should see output and templates listed.
- Create the empty project files by running these commands:
touch inventory.yaml generate_configs.py ai_review.py
touch templates/switching.j2 templates/routing.j2 templates/vxlan_evpn.j2 templates/firewall.j2
What are these files for?
- inventory.yaml defines every network device across your data center sites.
- generate_configs.py loads the inventory and renders Jinja2 templates into per-device config files.
- ai_review.py sends generated configs to Claude for validation and produces tech refresh documentation.
- The four .j2 files are Jinja2 templates covering switching, routing, VXLAN SDN overlays, and firewalls.
- Confirm all files were created by running:
ls
You should see ai_review.py, generate_configs.py, inventory.yaml, output, and templates listed.
- Check the template files by running:
ls templates/
You should see firewall.j2, routing.j2, switching.j2, and vxlan_evpn.j2 listed.
Missing files or folders?
- Make sure you are inside the tech-refresh-automation folder. Run pwd to check your current location.
- If the templates folder is missing, you may have run the mkdir command from a different directory. Re-run it from ~/Desktop.
Help me fix my project directory structure.
Your environment is ready. Next up, you will define the multi-vendor device inventory that powers the entire automation pipeline.
Define the Multi-Vendor Device Inventory
Your environment is all set up with Python, the required libraries, and your project directory. Now you need to give your automation tool something to work with.
In a tech refresh, the device inventory is the single source of truth. It defines every device being refreshed, its vendor, role, IP addresses, and configuration parameters. You'll define this inventory in YAML, a human-readable data format that separates your data from your automation logic.
In this step, get ready to:
- Build a YAML inventory file with multi-vendor device data across two data center sites.
- Write a Python inventory loader and validate that the file parses correctly.
Build the YAML inventory file
The inventory represents a real multi-site network being refreshed. It spans two data centers with six devices across five vendor platforms: Cisco NX-OS (spine and leaf switches), Arista EOS (leaf switch), Cisco IOS (WAN router), Palo Alto (firewall), and FortiGate (firewall).
Each device entry includes everything your templates will need to generate a full configuration. That means hostnames, IP addresses, VLAN definitions, BGP neighbors, OSPF settings, VXLAN VNI mappings, and firewall security policies.
- Open inventory.yaml in your text editor and paste the first data center site's inventory:
---
project:
name: "AMR Tech Refresh Q2 2026"
region: "Americas"
engineer: "Network Team"
sites:
- name: "NYC-DC1"
location: "New York Data Center"
devices:
- hostname: "NYC-SPINE-01"
vendor: "cisco_nxos"
role: "spine"
model: "Nexus 9300"
management_ip: "10.1.1.1"
loopback0: "10.255.0.1/32"
asn: 65001
interfaces:
- name: "Ethernet1/1"
description: "to-NYC-LEAF-01"
ip: "10.10.1.0/31"
type: "routed"
- name: "Ethernet1/2"
description: "to-NYC-LEAF-02"
ip: "10.10.1.2/31"
type: "routed"
bgp_neighbors:
- ip: "10.255.0.11"
remote_asn: 65001
description: "NYC-LEAF-01"
- ip: "10.255.0.12"
remote_asn: 65001
description: "NYC-LEAF-02"
evpn_neighbors:
- ip: "10.255.0.11"
- ip: "10.255.0.12"
- hostname: "NYC-LEAF-01"
vendor: "cisco_nxos"
role: "leaf"
model: "Nexus 9300"
management_ip: "10.1.1.11"
loopback0: "10.255.0.11/32"
loopback1: "10.255.1.11/32"
asn: 65001
vlans:
- id: 100
name: "SERVERS"
vni: 10100
- id: 200
name: "MANAGEMENT"
vni: 10200
vrf:
name: "TENANT-A"
vni: 50001
rd: "auto"
rt: "auto"
interfaces:
- name: "Ethernet1/1"
description: "to-NYC-SPINE-01"
ip: "10.10.1.1/31"
type: "routed"
- name: "Ethernet1/10"
description: "Server-Rack-A"
mode: "access"
vlan: 100
- name: "Ethernet1/11"
description: "Server-Rack-B"
mode: "trunk"
allowed_vlans: "100,200"
bgp_neighbors:
- ip: "10.255.0.1"
remote_asn: 65001
description: "NYC-SPINE-01"
- hostname: "NYC-LEAF-02"
vendor: "arista_eos"
role: "leaf"
model: "Arista 7050X"
management_ip: "10.1.1.12"
loopback0: "10.255.0.12/32"
loopback1: "10.255.1.12/32"
asn: 65001
vlans:
- id: 100
name: "SERVERS"
vni: 10100
- id: 300
name: "DMZ"
vni: 10300
interfaces:
- name: "Ethernet1"
description: "to-NYC-SPINE-01"
ip: "10.10.1.3/31"
type: "routed"
- name: "Ethernet10"
description: "DMZ-Servers"
mode: "access"
vlan: 300
bgp_neighbors:
- ip: "10.255.0.1"
remote_asn: 65001
description: "NYC-SPINE-01"
- hostname: "NYC-FW-01"
vendor: "paloalto"
role: "firewall"
model: "PA-5260"
management_ip: "10.1.1.50"
interfaces:
- name: "ethernet1/1"
zone: "OUTSIDE"
ip: "203.0.113.1/30"
- name: "ethernet1/2"
zone: "INSIDE"
ip: "10.1.100.1/24"
- name: "ethernet1/3"
zone: "DMZ"
ip: "10.1.200.1/24"
security_policies:
- name: "Allow-Web-Traffic"
source_zone: "OUTSIDE"
destination_zone: "DMZ"
application: "web-browsing"
action: "allow"
- name: "Allow-Internal-Out"
source_zone: "INSIDE"
destination_zone: "OUTSIDE"
application: "any"
action: "allow"
- name: "Deny-All"
source_zone: "any"
destination_zone: "any"
application: "any"
action: "deny"
What does this inventory define?
This first site, NYC-DC1, has four devices:
- NYC-SPINE-01 is a Cisco NX-OS spine switch. It has routed interfaces connecting to leaf switches, BGP neighbors, and EVPN neighbors for the VXLAN overlay.
- NYC-LEAF-01 is a Cisco NX-OS leaf switch. It defines VLANs with VNI mappings for VXLAN, access and trunk ports, and a tenant VRF for network segmentation.
- NYC-LEAF-02 is an Arista EOS leaf switch. This is your multi-vendor element. Same spine-leaf role, different vendor.
- NYC-FW-01 is a Palo Alto firewall with zone-based interfaces and security policies (allow web, allow internal out, deny all).
You won't see a change yet. The next chunk adds the second data center site.
- Below the NYC-DC1 site block you just added, paste the second data center site. Make sure the - name: "LAX-DC2" line is at the same indentation level as - name: "NYC-DC1" (two spaces):
- name: "LAX-DC2"
location: "Los Angeles Data Center"
devices:
- hostname: "LAX-RTR-01"
vendor: "cisco_ios"
role: "router"
model: "ISR 4451"
management_ip: "10.2.1.1"
loopback0: "10.255.2.1/32"
asn: 65002
ospf:
process_id: 1
router_id: "10.255.2.1"
networks:
- address: "10.2.0.0"
wildcard: "0.0.255.255"
area: "0.0.0.0"
- address: "10.255.2.1"
wildcard: "0.0.0.0"
area: "0.0.0.0"
bgp_neighbors:
- ip: "10.255.0.1"
remote_asn: 65001
description: "NYC-SPINE-01-WAN"
interfaces:
- name: "GigabitEthernet0/0/0"
description: "WAN-to-NYC"
ip: "172.16.0.1/30"
type: "routed"
- name: "GigabitEthernet0/0/1"
description: "LAN-Segment"
ip: "10.2.10.1/24"
type: "routed"
ipv6: "2001:db8:2::1/64"
qos:
policy_name: "WAN-QOS"
classes:
- name: "VOICE"
bandwidth_percent: 30
dscp: "ef"
- name: "VIDEO"
bandwidth_percent: 20
dscp: "af41"
- name: "DATA"
bandwidth_percent: 40
dscp: "af21"
- hostname: "LAX-FW-01"
vendor: "fortigate"
role: "firewall"
model: "FortiGate 600E"
management_ip: "10.2.1.50"
interfaces:
- name: "port1"
alias: "WAN"
ip: "203.0.113.5/30"
role: "wan"
- name: "port2"
alias: "LAN"
ip: "10.2.100.1/24"
role: "lan"
firewall_policies:
- id: 1
name: "LAN-to-WAN"
srcintf: "port2"
dstintf: "port1"
srcaddr: "all"
dstaddr: "all"
action: "accept"
nat: true
- id: 2
name: "Deny-Default"
srcintf: "any"
dstintf: "any"
srcaddr: "all"
dstaddr: "all"
action: "deny"
nat: false
What does the LAX-DC2 site add?
- LAX-RTR-01 is a Cisco IOS WAN router. It runs both OSPF and BGP, has IPv6 dual-stack addressing, and a QoS policy for voice, video, and data traffic classification with DSCP markings.
- LAX-FW-01 is a FortiGate firewall. Its policy structure uses a different syntax from Palo Alto, which is exactly why multi-vendor automation matters.
- Save inventory.yaml.
You'll verify the full inventory parses correctly in the next substep.
YAML is picky about indentation
YAML uses spaces for structure, not curly braces or brackets. Every level of nesting adds two spaces of indentation. If a key is misaligned by even one space, the parser treats it as a different level and the file breaks.
If you run into issues later, check that the LAX-DC2 site entry starts at the same indent level as the NYC-DC1 entry (two spaces in from sites:).
Help me fix a YAML indentation error in my inventory file.
Awesome, I've got everything!
Double check you've saved inventory.yaml before moving on.
I'd like to double check the full code
---
project:
name: "AMR Tech Refresh Q2 2026"
region: "Americas"
engineer: "Network Team"
sites:
- name: "NYC-DC1"
location: "New York Data Center"
devices:
- hostname: "NYC-SPINE-01"
vendor: "cisco_nxos"
role: "spine"
model: "Nexus 9300"
management_ip: "10.1.1.1"
loopback0: "10.255.0.1/32"
asn: 65001
interfaces:
- name: "Ethernet1/1"
description: "to-NYC-LEAF-01"
ip: "10.10.1.0/31"
type: "routed"
- name: "Ethernet1/2"
description: "to-NYC-LEAF-02"
ip: "10.10.1.2/31"
type: "routed"
bgp_neighbors:
- ip: "10.255.0.11"
remote_asn: 65001
description: "NYC-LEAF-01"
- ip: "10.255.0.12"
remote_asn: 65001
description: "NYC-LEAF-02"
evpn_neighbors:
- ip: "10.255.0.11"
- ip: "10.255.0.12"
- hostname: "NYC-LEAF-01"
vendor: "cisco_nxos"
role: "leaf"
model: "Nexus 9300"
management_ip: "10.1.1.11"
loopback0: "10.255.0.11/32"
loopback1: "10.255.1.11/32"
asn: 65001
vlans:
- id: 100
name: "SERVERS"
vni: 10100
- id: 200
name: "MANAGEMENT"
vni: 10200
vrf:
name: "TENANT-A"
vni: 50001
rd: "auto"
rt: "auto"
interfaces:
- name: "Ethernet1/1"
description: "to-NYC-SPINE-01"
ip: "10.10.1.1/31"
type: "routed"
- name: "Ethernet1/10"
description: "Server-Rack-A"
mode: "access"
vlan: 100
- name: "Ethernet1/11"
description: "Server-Rack-B"
mode: "trunk"
allowed_vlans: "100,200"
bgp_neighbors:
- ip: "10.255.0.1"
remote_asn: 65001
description: "NYC-SPINE-01"
- hostname: "NYC-LEAF-02"
vendor: "arista_eos"
role: "leaf"
model: "Arista 7050X"
management_ip: "10.1.1.12"
loopback0: "10.255.0.12/32"
loopback1: "10.255.1.12/32"
asn: 65001
vlans:
- id: 100
name: "SERVERS"
vni: 10100
- id: 300
name: "DMZ"
vni: 10300
interfaces:
- name: "Ethernet1"
description: "to-NYC-SPINE-01"
ip: "10.10.1.3/31"
type: "routed"
- name: "Ethernet10"
description: "DMZ-Servers"
mode: "access"
vlan: 300
bgp_neighbors:
- ip: "10.255.0.1"
remote_asn: 65001
description: "NYC-SPINE-01"
- hostname: "NYC-FW-01"
vendor: "paloalto"
role: "firewall"
model: "PA-5260"
management_ip: "10.1.1.50"
interfaces:
- name: "ethernet1/1"
zone: "OUTSIDE"
ip: "203.0.113.1/30"
- name: "ethernet1/2"
zone: "INSIDE"
ip: "10.1.100.1/24"
- name: "ethernet1/3"
zone: "DMZ"
ip: "10.1.200.1/24"
security_policies:
- name: "Allow-Web-Traffic"
source_zone: "OUTSIDE"
destination_zone: "DMZ"
application: "web-browsing"
action: "allow"
- name: "Allow-Internal-Out"
source_zone: "INSIDE"
destination_zone: "OUTSIDE"
application: "any"
action: "allow"
- name: "Deny-All"
source_zone: "any"
destination_zone: "any"
application: "any"
action: "deny"
- name: "LAX-DC2"
location: "Los Angeles Data Center"
devices:
- hostname: "LAX-RTR-01"
vendor: "cisco_ios"
role: "router"
model: "ISR 4451"
management_ip: "10.2.1.1"
loopback0: "10.255.2.1/32"
asn: 65002
ospf:
process_id: 1
router_id: "10.255.2.1"
networks:
- address: "10.2.0.0"
wildcard: "0.0.255.255"
area: "0.0.0.0"
- address: "10.255.2.1"
wildcard: "0.0.0.0"
area: "0.0.0.0"
bgp_neighbors:
- ip: "10.255.0.1"
remote_asn: 65001
description: "NYC-SPINE-01-WAN"
interfaces:
- name: "GigabitEthernet0/0/0"
description: "WAN-to-NYC"
ip: "172.16.0.1/30"
type: "routed"
- name: "GigabitEthernet0/0/1"
description: "LAN-Segment"
ip: "10.2.10.1/24"
type: "routed"
ipv6: "2001:db8:2::1/64"
qos:
policy_name: "WAN-QOS"
classes:
- name: "VOICE"
bandwidth_percent: 30
dscp: "ef"
- name: "VIDEO"
bandwidth_percent: 20
dscp: "af41"
- name: "DATA"
bandwidth_percent: 40
dscp: "af21"
- hostname: "LAX-FW-01"
vendor: "fortigate"
role: "firewall"
model: "FortiGate 600E"
management_ip: "10.2.1.50"
interfaces:
- name: "port1"
alias: "WAN"
ip: "203.0.113.5/30"
role: "wan"
- name: "port2"
alias: "LAN"
ip: "10.2.100.1/24"
role: "lan"
firewall_policies:
- id: 1
name: "LAN-to-WAN"
srcintf: "port2"
dstintf: "port1"
srcaddr: "all"
dstaddr: "all"
action: "accept"
nat: true
- id: 2
name: "Deny-Default"
srcintf: "any"
dstintf: "any"
srcaddr: "all"
dstaddr: "all"
action: "deny"
nat: false
Write the inventory loader and validate it
Before building the full config generator, you need to confirm that Python can parse your inventory correctly. PyYAML's safe_load() function converts your YAML file into a Python dictionary that the rest of your automation will use.
- Open generate_configs.py and add the inventory loader function with a quick test to print every device:
import yaml
def load_inventory(filepath):
"""Load the network device inventory from a YAML file."""
with open(filepath, 'r') as f:
return yaml.safe_load(f)
# Quick test to verify inventory loading
if __name__ == '__main__':
inventory = load_inventory('inventory.yaml')
print(f"Project: {inventory['project']['name']}")
print(f"Region: {inventory['project']['region']}")
for site in inventory['sites']:
print(f"\nSite: {site['name']} ({site['location']})")
for device in site['devices']:
print(f" - {device['hostname']}: {device['vendor']} "
f"{device['model']} ({device['role']})")
print(f"\nTotal devices found: "
f"{sum(len(s['devices']) for s in inventory['sites'])}")
What does this code do?
- load_inventory() opens the YAML file and uses yaml.safe_load() to parse it into a nested Python dictionary. The safe_load variant prevents arbitrary code execution from untrusted YAML.
- The test block under if __name__ == '__main__': only runs when you execute the file directly. It loops through every site and device to confirm the YAML structure loaded correctly.
- This test code is temporary. You'll replace it with the full config generator in the next step.
- Save generate_configs.py.
- Run the loader by executing this command in your terminal (make sure you're inside the tech-refresh-automation folder):
python3 generate_configs.py
You should see output listing all six devices across both sites, ending with Total devices found: 6.
What should I see?
Your terminal output should look similar to this:
Project: AMR Tech Refresh Q2 2026
Region: Americas
Site: NYC-DC1 (New York Data Center)
- NYC-SPINE-01: cisco_nxos Nexus 9300 (spine)
- NYC-LEAF-01: cisco_nxos Nexus 9300 (leaf)
- NYC-LEAF-02: arista_eos Arista 7050X (leaf)
- NYC-FW-01: paloalto PA-5260 (firewall)
Site: LAX-DC2 (Los Angeles Data Center)
- LAX-RTR-01: cisco_ios ISR 4451 (router)
- LAX-FW-01: fortigate FortiGate 600E (firewall)
Total devices found: 6
If you see all six devices with their correct vendors and roles, your inventory is valid.
Seeing a YAML parsing error?
- Check for tabs in your YAML file. YAML only accepts spaces for indentation. If you pasted content that contains tabs, replace them with spaces.
- Look at the error message for a line number. The issue is usually a misaligned key on that line or the line above it.
- Make sure the two site entries (NYC-DC1 and LAX-DC2) are at the same indentation level. Both should be two spaces in from sites:.
- Help me debug a YAML parsing error in my network inventory file.
Why YAML instead of JSON or a database?
YAML is the standard data format for network automation inventories. Tools like Ansible use YAML natively for inventory and playbook definitions. It is human-readable, supports comments, and maps cleanly to the nested structure of network data (sites contain devices, devices contain interfaces, interfaces contain properties).
Your inventory is loaded and validated. Next up, you'll build Jinja2 templates that turn this raw device data into real vendor-specific configurations.
Build Config Templates for Switching and Routing
Your device inventory is locked in. Every hostname, IP address, VLAN, and BGP neighbor is defined in one structured YAML file.
Now you need something that can read that data and turn it into real device configurations. In this step, you'll build Jinja2 templates that define the config patterns for switching and routing, then write the Python script that renders them into per-device config files.
In this step, get ready to:
- Create a switching template for VLAN and interface configs across Cisco NX-OS and Arista EOS.
- Create a routing template for BGP, OSPF, IPv6, and QoS configs.
- Build the config generator script that renders templates from your inventory.
Create the switching template
The switching template generates VLAN definitions and interface configurations for access ports, trunk ports, and spanning-tree settings. It uses Jinja2 conditionals to produce vendor-specific syntax for Cisco NX-OS vs Arista EOS.
What are Jinja2 templates?
Jinja2 is the industry standard for network config templating. You define a config pattern once in a .j2 file, and Jinja2 fills it with different device data each time it renders. Three core concepts:
- Variables with {{ }} insert values from your inventory data.
- For loops with {% for %} repeat a config block for each item in a list (like VLANs or interfaces).
- Conditionals with {% if %} produce different output based on the vendor or device role.
- Open templates/switching.j2 in your text editor and paste the following template code:
! =============================================
! Switching Configuration - {{ hostname }}
! Vendor: {{ vendor }} | Model: {{ model }}
! Generated: {{ timestamp }}
! =============================================
{% if vendor == 'cisco_nxos' %}
! --- VLAN Configuration ---
{% for vlan in vlans %}
vlan {{ vlan.id }}
name {{ vlan.name }}
{% if vlan.vni is defined %}
vn-segment {{ vlan.vni }}
{% endif %}
{% endfor %}
! --- Interface Configuration ---
{% for intf in interfaces %}
{% if intf.mode is defined %}
interface {{ intf.name }}
description {{ intf.description }}
{% if intf.mode == 'access' %}
switchport mode access
switchport access vlan {{ intf.vlan }}
{% elif intf.mode == 'trunk' %}
switchport mode trunk
switchport trunk allowed vlan {{ intf.allowed_vlans }}
{% endif %}
spanning-tree port type edge
no shutdown
{% endif %}
{% endfor %}
{% elif vendor == 'arista_eos' %}
! --- VLAN Configuration ---
{% for vlan in vlans %}
vlan {{ vlan.id }}
name {{ vlan.name }}
{% endfor %}
! --- Interface Configuration ---
{% for intf in interfaces %}
{% if intf.mode is defined %}
interface {{ intf.name }}
description {{ intf.description }}
{% if intf.mode == 'access' %}
switchport mode access
switchport access vlan {{ intf.vlan }}
{% elif intf.mode == 'trunk' %}
switchport mode trunk
switchport trunk allowed vlan {{ intf.allowed_vlans }}
{% endif %}
spanning-tree portfast
no shutdown
{% endif %}
{% endfor %}
{% endif %}
What does this template do?
- The top banner uses {{ hostname }}, {{ vendor }}, and {{ model }} to label each config file with the device it belongs to.
- The {% if vendor == 'cisco_nxos' %} conditional switches the entire output between NX-OS syntax (2-space indentation, spanning-tree port type edge) and EOS syntax (3-space indentation, spanning-tree portfast).
- The {% for vlan in vlans %} loop creates a VLAN definition for every VLAN in the device's inventory. If a VLAN has a vni value, it also adds a vn-segment mapping for VXLAN.
- The interface loop checks each interface's mode field. Access ports get assigned a single VLAN. Trunk ports get an allowed VLAN list.
- Save templates/switching.j2.
You won't see any output yet. The config generator you'll build in this step's final substep uses this template to produce actual device configs.
Create the routing template
The routing template handles four types of config: BGP peering, OSPF process definitions, IPv6 dual-stack addressing, and QoS policy maps with DSCP markings.
Each section is conditionally included based on whether the device has that data defined in the inventory. A spine switch with BGP neighbors gets BGP config. A WAN router with OSPF and QoS gets both of those sections too.
- Open templates/routing.j2 and paste the following template code:
! =============================================
! Routing Configuration - {{ hostname }}
! Generated: {{ timestamp }}
! =============================================
{% if bgp_neighbors is defined %}
! --- BGP Configuration ---
router bgp {{ asn }}
router-id {{ loopback0.split('/')[0] }}
{% for neighbor in bgp_neighbors %}
neighbor {{ neighbor.ip }} remote-as {{ neighbor.remote_asn }}
description {{ neighbor.description }}
update-source loopback0
address-family ipv4 unicast
send-community both
{% if evpn_neighbors is defined %}
address-family l2vpn evpn
send-community both
{% if role == 'spine' %}
route-reflector-client
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% if ospf is defined %}
! --- OSPF Configuration ---
router ospf {{ ospf.process_id }}
router-id {{ ospf.router_id }}
{% for network in ospf.networks %}
network {{ network.address }} {{ network.wildcard }} area {{ network.area }}
{% endfor %}
{% endif %}
! --- Routed Interfaces ---
{% for intf in interfaces %}
{% if intf.type is defined and intf.type == 'routed' %}
interface {{ intf.name }}
description {{ intf.description }}
no switchport
ip address {{ intf.ip }}
{% if intf.ipv6 is defined %}
ipv6 address {{ intf.ipv6 }}
ipv6 ospf 1 area 0.0.0.0
{% endif %}
no shutdown
{% endif %}
{% endfor %}
{% if qos is defined %}
! --- QoS Policy ---
policy-map {{ qos.policy_name }}
{% for cls in qos.classes %}
class {{ cls.name }}
bandwidth percent {{ cls.bandwidth_percent }}
set dscp {{ cls.dscp }}
{% endfor %}
{% endif %}
What does this template do?
- The BGP section only renders if bgp_neighbors is defined for the device. It sets up the BGP process, router ID (extracted from the loopback with .split('/')[0]), and per-neighbor peering. Devices with evpn_neighbors also get the l2vpn evpn address family for SDN overlay traffic.
- Spine switches get route-reflector-client added to their EVPN neighbors. This lets the spine act as a central BGP route reflector so leaf switches don't need full-mesh peering.
- The OSPF section creates the process and advertises networks by area. The routed interfaces section assigns IP addresses (and IPv6 where defined) to non-switchport interfaces.
- The QoS section creates a policy map with traffic classes, bandwidth allocation percentages, and DSCP markings for voice, video, and data prioritization.
- Save templates/routing.j2.
Like the switching template, you won't see output yet. The generator script in the next substep renders both templates.
Why conditionals instead of separate template files per vendor?
You could create separate files like switching_nxos.j2 and switching_eos.j2, but vendor conditionals inside one template keep all switching logic in one place. When you need to add a new VLAN standard or change a spanning-tree setting, you update one file instead of hunting through multiple vendor-specific files.
Write the config generator script
The templates are ready. Now you need a Python script that loads your inventory, feeds each device's data into the right templates, and writes the rendered configs to files.
- Open generate_configs.py and replace all the existing code (the test code from the previous step) with the imports and helper functions below:
import os
import yaml
from datetime import datetime
from jinja2 import Environment, FileSystemLoader
def load_inventory(filepath):
"""Load the network device inventory from a YAML file."""
with open(filepath, 'r') as f:
return yaml.safe_load(f)
def setup_jinja_env(template_dir):
"""Set up the Jinja2 template environment."""
return Environment(
loader=FileSystemLoader(template_dir),
trim_blocks=True,
lstrip_blocks=True
)
What does this code do?
- load_inventory() reads your inventory.yaml and parses it into a Python dictionary using yaml.safe_load().
- setup_jinja_env() creates the Jinja2 environment pointed at your templates/ folder. The trim_blocks and lstrip_blocks settings remove extra whitespace that Jinja2 control tags would otherwise leave in the output.
You won't see any output from these helpers alone. The next function uses them to render configs.
- Below setup_jinja_env(), add the function that generates the config for a single device:
def generate_device_config(env, device, timestamp):
"""Generate the full configuration for a single device."""
config_sections = []
template_vars = {**device, 'timestamp': timestamp}
# Switching config (for devices with VLANs)
if device.get('vlans'):
template = env.get_template('switching.j2')
config_sections.append(template.render(template_vars))
# Routing config (for devices with BGP or OSPF)
if device.get('bgp_neighbors') or device.get('ospf'):
template = env.get_template('routing.j2')
config_sections.append(template.render(template_vars))
# VXLAN EVPN config (for spine/leaf SDN devices)
if device.get('role') in ['spine', 'leaf']:
template = env.get_template('vxlan_evpn.j2')
config_sections.append(template.render(template_vars))
# Firewall config (for Palo Alto and FortiGate)
if device.get('vendor') in ['paloalto', 'fortigate']:
template = env.get_template('firewall.j2')
config_sections.append(template.render(template_vars))
return '\n\n'.join(config_sections)
What does this function do?
- template_vars = {**device, 'timestamp': timestamp} spreads all the device's inventory data into a dictionary and adds a timestamp. This is what Jinja2 receives as its template variables.
- Each if block checks whether the device has the right data for a template. A leaf switch with VLANs gets the switching template. A router with BGP neighbors gets the routing template. This way one function handles every device type.
- The VXLAN EVPN and firewall checks are included here too. Those templates don't exist yet. You'll create them in the next step. For now, the function is ready for them.
You won't see output from this function alone either. The main() function you add next calls it for every device in the inventory.
- Below generate_device_config(), add the main function that ties everything together:
def main():
inventory = load_inventory('inventory.yaml')
env = setup_jinja_env('templates')
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
os.makedirs('output', exist_ok=True)
all_configs = {}
print(f"{'=' * 60}")
print(f" Network Tech Refresh Config Generator")
print(f" Project: {inventory['project']['name']}")
print(f" Region: {inventory['project']['region']}")
print(f" Generated: {timestamp}")
print(f"{'=' * 60}\n")
for site in inventory['sites']:
print(f"Site: {site['name']} ({site['location']})")
print(f"{'-' * 40}")
for device in site['devices']:
config = generate_device_config(env, device, timestamp)
filename = f"output/{device['hostname']}.cfg"
with open(filename, 'w') as f:
f.write(config)
all_configs[device['hostname']] = config
print(f" Generated: {device['hostname']} "
f"({device['vendor']}, {device['role']})")
print()
print(f"All configs saved to output/ directory.")
print(f"Total devices: {len(all_configs)}")
return all_configs
if __name__ == '__main__':
configs = main()
What does the main function do?
- It loads the inventory and sets up the Jinja2 environment using the two helper functions you wrote above.
- os.makedirs('output', exist_ok=True) creates the output directory if it doesn't already exist.
- The nested loop iterates through every site, then every device in that site. For each device, it calls generate_device_config() and writes the result to a .cfg file named after the hostname.
- It returns all_configs as a dictionary so the AI review script (built in a later step) can import and reuse this function.
- Save generate_configs.py.
Awesome, I've got everything!
Double check you've saved your file before running it.
I'd like to double check the full code
import os
import yaml
from datetime import datetime
from jinja2 import Environment, FileSystemLoader
def load_inventory(filepath):
"""Load the network device inventory from a YAML file."""
with open(filepath, 'r') as f:
return yaml.safe_load(f)
def setup_jinja_env(template_dir):
"""Set up the Jinja2 template environment."""
return Environment(
loader=FileSystemLoader(template_dir),
trim_blocks=True,
lstrip_blocks=True
)
def generate_device_config(env, device, timestamp):
"""Generate the full configuration for a single device."""
config_sections = []
template_vars = {**device, 'timestamp': timestamp}
# Switching config (for devices with VLANs)
if device.get('vlans'):
template = env.get_template('switching.j2')
config_sections.append(template.render(template_vars))
# Routing config (for devices with BGP or OSPF)
if device.get('bgp_neighbors') or device.get('ospf'):
template = env.get_template('routing.j2')
config_sections.append(template.render(template_vars))
# VXLAN EVPN config (for spine/leaf SDN devices)
if device.get('role') in ['spine', 'leaf']:
template = env.get_template('vxlan_evpn.j2')
config_sections.append(template.render(template_vars))
# Firewall config (for Palo Alto and FortiGate)
if device.get('vendor') in ['paloalto', 'fortigate']:
template = env.get_template('firewall.j2')
config_sections.append(template.render(template_vars))
return '\n\n'.join(config_sections)
def main():
inventory = load_inventory('inventory.yaml')
env = setup_jinja_env('templates')
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
os.makedirs('output', exist_ok=True)
all_configs = {}
print(f"{'=' * 60}")
print(f" Network Tech Refresh Config Generator")
print(f" Project: {inventory['project']['name']}")
print(f" Region: {inventory['project']['region']}")
print(f" Generated: {timestamp}")
print(f"{'=' * 60}\n")
for site in inventory['sites']:
print(f"Site: {site['name']} ({site['location']})")
print(f"{'-' * 40}")
for device in site['devices']:
config = generate_device_config(env, device, timestamp)
filename = f"output/{device['hostname']}.cfg"
with open(filename, 'w') as f:
f.write(config)
all_configs[device['hostname']] = config
print(f" Generated: {device['hostname']} "
f"({device['vendor']}, {device['role']})")
print()
print(f"All configs saved to output/ directory.")
print(f"Total devices: {len(all_configs)}")
return all_configs
if __name__ == '__main__':
configs = main()
The script references the VXLAN EVPN and firewall templates, but those files are still empty. To avoid errors on this first run, you need to tell the script to skip templates that don't have content yet.
Pro tip: handle empty templates
The VXLAN and firewall templates are empty right now. Running the generator will still work because Jinja2 renders an empty template as an empty string. The generator will produce config files for switching and routing devices, but VXLAN and firewall sections will just be blank. You'll fill those templates in the next step.
- Run the config generator by executing this command from your tech-refresh-automation folder:
python3 generate_configs.py
You should see output listing each site and every device that was generated, like this:
============================================================
Network Tech Refresh Config Generator
Project: AMR Tech Refresh Q2 2026
Region: Americas
Generated: 2026-04-25 09:10:00
============================================================
Site: NYC-DC1 (New York Data Center)
----------------------------------------
Generated: NYC-SPINE-01 (cisco_nxos, spine)
Generated: NYC-LEAF-01 (cisco_nxos, leaf)
Generated: NYC-LEAF-02 (arista_eos, leaf)
Generated: NYC-FW-01 (paloalto, firewall)
Site: LAX-DC2 (Los Angeles Data Center)
----------------------------------------
Generated: LAX-RTR-01 (cisco_ios, router)
Generated: LAX-FW-01 (fortigate, firewall)
All configs saved to output/ directory.
Total devices: 6
- Verify the output files were created by running:
ls output/
You should see six .cfg files, one per device: NYC-SPINE-01.cfg, NYC-LEAF-01.cfg, NYC-LEAF-02.cfg, NYC-FW-01.cfg, LAX-RTR-01.cfg, and LAX-FW-01.cfg.
- Inspect a switching config to verify it rendered correctly by running:
cat output/NYC-LEAF-01.cfg
You should see VLAN definitions (VLAN 100 SERVERS, VLAN 200 MANAGEMENT), interface configs with access and trunk modes, BGP neighbor configuration, and routed interface assignments.
- Check the Arista EOS config to see the vendor-specific differences by running:
cat output/NYC-LEAF-02.cfg
Notice the 3-space indentation and spanning-tree portfast instead of NX-OS's spanning-tree port type edge. Same template, different vendor output.
- Check the WAN router config to see BGP, OSPF, IPv6, and QoS all rendered together by running:
cat output/LAX-RTR-01.cfg
This config includes the BGP peering to NYC, the OSPF process with network advertisements, the IPv6 address on GigabitEthernet0/0/1, and the WAN QoS policy map with voice, video, and data classes.
Seeing a TemplateNotFound error?
- Make sure you're running the command from inside the tech-refresh-automation folder, not the parent directory. The script looks for templates/ relative to where you run it.
- Check that your template files are named exactly switching.j2 and routing.j2 inside the templates/ folder. File names are case-sensitive.
- Verify the empty template files (vxlan_evpn.j2 and firewall.j2) still exist in templates/. Even though they're empty, they need to be present for the generator to find them.
Help me debug a TemplateNotFound error in my Jinja2 config generator.
Seeing a YAML parsing error?
- This usually means there's an indentation problem in inventory.yaml. YAML is whitespace-sensitive. Make sure you're using spaces (not tabs) and that nested items are consistently indented.
- Check that all string values with special characters are wrapped in quotes.
Help me fix a YAML parsing error in my inventory file.
Your switching and routing templates are generating real, vendor-specific configs from a single inventory. Next up, you'll add the VXLAN SDN overlay and firewall templates to complete the full multi-vendor config set.
Add VXLAN SDN and Firewall Templates
Your switching and routing templates are generating clean configs for every device in the inventory. The next layer to automate is the SDN overlay and firewall policies.
A real tech refresh covers more than switches and routers. Data center fabrics rely on VXLAN overlays to virtualize the network, and every site needs firewall rules to control traffic. In this step, you will build templates for both.
In this step, get ready to:
- Create the VXLAN BGP EVPN template for SDN overlay configuration.
- Create the firewall template for Palo Alto and FortiGate devices.
- Run the full generator and verify configs for all six devices.
Create the VXLAN BGP EVPN template
VXLAN BGP EVPN is SDN in practice. It separates the control plane (BGP EVPN distributes MAC and IP reachability) from the data plane (VXLAN encapsulates traffic in overlay tunnels). This is the core principle behind software-defined networking.
What is VXLAN BGP EVPN?
VXLAN stands for Virtual Extensible LAN. It creates Layer 2 overlay tunnels across a Layer 3 physical network, so VLANs can stretch across data center sites without the limitations of traditional spanning-tree designs.
BGP EVPN is the control plane protocol that distributes MAC and IP address information between leaf switches. Each leaf switch acts as a VTEP (VXLAN Tunnel Endpoint) that encapsulates and decapsulates traffic.
Together, they let you manage the logical network independently from the physical infrastructure. Add a new VLAN across the entire fabric by updating BGP, not by recabling switches.
- Open templates/vxlan_evpn.j2 in your text editor and paste in the following template:
! =============================================
! VXLAN BGP EVPN Configuration - {{ hostname }}
! SDN Overlay Network
! Generated: {{ timestamp }}
! =============================================
{% if role in ['leaf', 'spine'] and vendor == 'cisco_nxos' %}
! --- Enable VXLAN and EVPN Features ---
nv overlay evpn
feature nv overlay
feature vn-segment-vlan-based
! --- Anycast Gateway MAC ---
fabric forwarding anycast-gateway-mac 0001.0001.0001
{% if role == 'leaf' %}
! --- NVE Interface (VTEP) ---
interface nve1
no shutdown
host-reachability protocol bgp
source-interface loopback1
{% for vlan in vlans %}
{% if vlan.vni is defined %}
member vni {{ vlan.vni }}
ingress-replication protocol bgp
{% endif %}
{% endfor %}
{% if vrf is defined %}
member vni {{ vrf.vni }} associate-vrf
{% endif %}
{% if vrf is defined %}
! --- Tenant VRF Configuration ---
vrf context {{ vrf.name }}
vni {{ vrf.vni }}
rd {{ vrf.rd }}
address-family ipv4 unicast
route-target both {{ vrf.rt }} evpn
{% endif %}
! --- EVPN VNI Definitions ---
evpn
{% for vlan in vlans %}
{% if vlan.vni is defined %}
vni {{ vlan.vni }} l2
rd auto
route-target import auto
route-target export auto
{% endif %}
{% endfor %}
! --- Anycast Gateway SVIs ---
{% for vlan in vlans %}
{% if vlan.vni is defined %}
interface Vlan{{ vlan.id }}
no shutdown
{% if vrf is defined %}
vrf member {{ vrf.name }}
{% endif %}
fabric forwarding mode anycast-gateway
{% endif %}
{% endfor %}
{% endif %}
{% elif role == 'leaf' and vendor == 'arista_eos' %}
! --- VXLAN Interface ---
interface Vxlan1
vxlan source-interface Loopback1
vxlan udp-port 4789
{% for vlan in vlans %}
{% if vlan.vni is defined %}
vxlan vlan {{ vlan.id }} vni {{ vlan.vni }}
{% endif %}
{% endfor %}
{% endif %}
What does this template do?
This template generates VXLAN overlay configs for two vendors. The Cisco NX-OS section is the most detailed:
- nv overlay evpn, feature nv overlay, and feature vn-segment-vlan-based activate the VXLAN/EVPN subsystems on the switch.
- The NVE interface (interface nve1) is the VTEP. It handles VXLAN encapsulation and uses BGP to learn which VNI members exist.
- The VRF context isolates tenant traffic and maps it to a VNI for inter-VLAN routing across the fabric.
- EVPN VNI definitions let BGP distribute Layer 2 MAC/IP reachability with auto-generated route distinguishers and route targets.
- Anycast gateway SVIs give every leaf switch the same virtual gateway IP, so hosts can route locally without hairpinning through a centralized gateway.
- The Arista EOS section uses a simpler interface Vxlan1 syntax with VLAN-to-VNI mappings and the standard VXLAN UDP port 4789.
- Save templates/vxlan_evpn.j2.
You will not see any output yet. The next substep adds the firewall template, and then you will run the generator to test both.
Seeing a Jinja2 syntax error later?
If you hit a TemplateSyntaxError when you run the generator, check that every {% if %} has a matching {% endif %} and every {% for %} has a matching {% endfor %}. This template has several nested conditionals, so a missing tag is easy to overlook.
Help me debug a Jinja2 syntax error in my VXLAN EVPN template.
Create the firewall template
The firewall template handles two completely different vendor syntaxes. Palo Alto uses set commands (PAN-OS CLI syntax), while FortiGate uses config/edit/next/end block syntax.
- Open templates/firewall.j2 in your text editor and paste in the following template:
{% if vendor == 'paloalto' %}
! =============================================
! Palo Alto Configuration - {{ hostname }}
! Model: {{ model }}
! Generated: {{ timestamp }}
! =============================================
! --- Interface and Zone Configuration ---
{% for intf in interfaces %}
set network interface ethernet {{ intf.name }} layer3 ip {{ intf.ip }}
set zone {{ intf.zone }} network layer3 {{ intf.name }}
{% endfor %}
! --- Security Policies ---
{% for policy in security_policies %}
set rulebase security rules "{{ policy.name }}" from {{ policy.source_zone }}
set rulebase security rules "{{ policy.name }}" to {{ policy.destination_zone }}
set rulebase security rules "{{ policy.name }}" application {{ policy.application }}
set rulebase security rules "{{ policy.name }}" action {{ policy.action }}
{% endfor %}
{% elif vendor == 'fortigate' %}
# =============================================
# FortiGate Configuration - {{ hostname }}
# Model: {{ model }}
# Generated: {{ timestamp }}
# =============================================
# --- Interface Configuration ---
{% for intf in interfaces %}
config system interface
edit "{{ intf.name }}"
set alias "{{ intf.alias }}"
set ip {{ intf.ip }}
set role {{ intf.role }}
next
end
{% endfor %}
# --- Firewall Policies ---
config firewall policy
{% for policy in firewall_policies %}
edit {{ policy.id }}
set name "{{ policy.name }}"
set srcintf "{{ policy.srcintf }}"
set dstintf "{{ policy.dstintf }}"
set srcaddr "{{ policy.srcaddr }}"
set dstaddr "{{ policy.dstaddr }}"
set action {{ policy.action }}
{% if policy.nat %}
set nat enable
{% endif %}
next
{% endfor %}
end
{% endif %}
What does this template do?
- The Palo Alto section binds each interface to a security zone (OUTSIDE, INSIDE, DMZ), then creates security policy rules that control traffic between zones.
- The FortiGate section uses a different structure. Each interface is configured inside a config system interface block, and firewall policies are defined inside a config firewall policy block with numbered rule IDs.
- Notice the {% if policy.nat %} conditional. FortiGate only adds set nat enable when NAT is required for that policy, keeping the config clean.
- Save templates/firewall.j2.
You will not see any output from saving this file. The next substep runs the generator to test both new templates.
Run the full generator and review all outputs
With all four templates in place (switching, routing, VXLAN EVPN, and firewall), the generator can now produce complete configs for every device in your inventory.
- Make sure your terminal is in the tech-refresh-automation folder.
- Run the config generator by running this command:
python3 generate_configs.py
You should see output listing all six devices across both sites, including the firewall devices that are new this time around.
What should I see?
The generator prints a banner with the project name, then lists each site and its devices. You should see all six devices:
- NYC-SPINE-01 (cisco_nxos, spine).
- NYC-LEAF-01 (cisco_nxos, leaf).
- NYC-LEAF-02 (arista_eos, leaf).
- NYC-FW-01 (paloalto, firewall).
- LAX-RTR-01 (cisco_ios, router).
- LAX-FW-01 (fortigate, firewall).
The last line should read Total devices: 6.
- Verify the VXLAN config was generated by opening the leaf switch output file:
cat output/NYC-LEAF-01.cfg
You should see the switching, routing, and VXLAN EVPN sections all combined into one config file. Look for the nv overlay evpn and interface nve1 lines to confirm the VXLAN template rendered correctly.
- Verify the Palo Alto firewall config by running:
cat output/NYC-FW-01.cfg
You should see set rulebase security rules commands for each security policy defined in the inventory.
- Verify the FortiGate firewall config by running:
cat output/LAX-FW-01.cfg
You should see config system interface and config firewall policy blocks with the FortiGate's edit/next/end structure.
Seeing an error when running the generator?
- A TemplateNotFound error means your template file is not in the templates/ directory. Check the filename matches exactly: vxlan_evpn.j2 and firewall.j2.
- A TemplateSyntaxError usually means a mismatched {% if %}/{% endif %} or {% for %}/{% endfor %}. The error message includes a line number to help you find it.
- An UndefinedError means a variable in the template does not exist in the inventory data for that device. Double check that your inventory.yaml matches the inventory from Step 2.
Help me debug an error when running my config generator with the new VXLAN and firewall templates.
Your config generator now produces complete, vendor-specific configs for every device type in the inventory. Next up, you will connect Claude AI to validate these configs and generate tech refresh documentation.
Connect Claude AI for Config Validation
Your config generator now produces complete, vendor-specific configurations for every device in your inventory. The next goal is to put those configs through an AI-powered review.
Manually reviewing network configurations for syntax errors, security gaps, and cross-device inconsistencies is tedious and error-prone. In this step, you will send your generated configs to the Claude API and have it return a validation report and a tech refresh execution plan.
In this step, get ready to:
- Write a function that sends generated configs to Claude for validation.
- Write a function that generates a tech refresh execution plan from your inventory.
- Run both AI functions and save the reports as markdown files.
Write the AI validation function
The ai_review.py script connects to the Anthropic API, sends your generated configs to Claude, and gets back a structured validation report. It reuses the config generation functions you already wrote.
- Open ai_review.py and add the imports and client setup by copying this code:
import os
import anthropic
from datetime import datetime
from generate_configs import load_inventory, setup_jinja_env, generate_device_config
def get_client():
"""Initialize the Anthropic client."""
return anthropic.Anthropic()
What does this code do?
- The imports bring in the Anthropic client, datetime for timestamps, and your config generation functions from generate_configs.py.
- get_client() initializes the API client. It automatically reads your ANTHROPIC_API_KEY environment variable that you set in Step 1.
Now for the core of this step. The validation function concatenates all your device configs and sends them to Claude with a detailed review prompt.
- Add the validate_configs() function below get_client() by copying this code:
def validate_configs(client, configs):
"""Send generated configs to Claude for validation."""
# Combine all device configs into one text block
configs_text = ""
for hostname, config in configs.items():
configs_text += f"\n--- {hostname} ---\n{config}\n"
# Send to Claude with a structured review prompt
message = client.messages.create(
model="claude-3-5-haiku-20241022",
max_tokens=4096,
messages=[
{
"role": "user",
"content": (
"You are a senior network engineer reviewing "
"configurations for a tech refresh project. "
"Review these device configurations for:\n\n"
"1. Syntax errors or misconfigurations\n"
"2. Security best practices (missing hardening, "
"open policies)\n"
"3. Consistency across devices (VLAN IDs, ASN "
"numbers, IP addressing)\n"
"4. Missing recommended settings (logging, NTP, "
"SNMP, banners)\n\n"
"For each issue found, specify:\n"
"- Device hostname\n"
"- The specific issue\n"
"- Severity (Critical / Warning / Info)\n"
"- Recommended fix\n\n"
f"Configurations to review:\n{configs_text}"
)
}
]
)
return message.content[0].text
What does this code do?
- The function loops through all device configs and joins them into a single text block, prefixed with each device's hostname.
- It calls client.messages.create() with the claude-3-5-haiku-20241022 model. Haiku is the most cost-effective model, which keeps this project well within the free API credit budget.
- The prompt tells Claude to act as a senior network engineer and review for syntax errors, security gaps, cross-device consistency, and missing best practices.
- message.content[0].text extracts Claude's text response from the API response object.
- You do not need to save the file yet. You will add more functions before running the script.
Why Haiku instead of Sonnet or Opus?
Haiku 3.5 costs $0.80 per million input tokens and $4.00 per million output tokens. This project makes three API calls at roughly 2,000-4,000 tokens each, totaling well under $1. That keeps you safely within the free $5 credit.
Write the tech refresh plan generator
Config validation catches errors, but a real tech refresh also needs a plan. This second function sends your inventory summary to Claude and asks it to generate a full execution plan with sequencing, risk assessment, and rollback procedures.
- Add the generate_refresh_plan() function below validate_configs() by copying this code:
def generate_refresh_plan(client, inventory):
"""Generate a tech refresh plan using Claude."""
# Build a summary of all devices for the prompt
inventory_summary = ""
for site in inventory['sites']:
inventory_summary += (
f"\nSite: {site['name']} ({site['location']})\n"
)
for device in site['devices']:
inventory_summary += (
f" - {device['hostname']}: {device['vendor']} "
f"{device['model']} ({device['role']})\n"
)
# Ask Claude to create a tech refresh execution plan
message = client.messages.create(
model="claude-3-5-haiku-20241022",
max_tokens=4096,
messages=[
{
"role": "user",
"content": (
"You are a senior network tech refresh engineer "
"at a large enterprise. Create a detailed tech "
"refresh execution plan for the following network "
"inventory.\n\n"
"The plan should include:\n"
"1. Refresh sequence (which devices to upgrade "
"first and why)\n"
"2. Risk assessment for each device category\n"
"3. Rollback procedures\n"
"4. Maintenance window recommendations\n"
"5. Pre-refresh and post-refresh validation "
"checks\n"
"6. Dependencies between devices\n\n"
f"Network inventory:\n{inventory_summary}"
)
}
]
)
return message.content[0].text
What does this code do?
- The function builds a compact summary of every device in your inventory, listing hostname, vendor, model, and role.
- The prompt asks Claude to plan the refresh sequence (which devices to upgrade first), assess risk per device category, define rollback procedures, and recommend maintenance windows.
- This is a real-world deliverable. Network engineers produce exactly this kind of plan before any tech refresh begins.
- Keep the file open. You will wire these functions together in the next substep.
Run the AI review and save the reports
The main() function ties everything together. It regenerates all device configs from the inventory, runs both AI functions, prints the results to the terminal, and saves each report as a Markdown file.
- Add the main() function and the entry point block at the bottom of ai_review.py:
def main():
client = get_client()
inventory = load_inventory('inventory.yaml')
env = setup_jinja_env('templates')
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# Generate configs first
all_configs = {}
for site in inventory['sites']:
for device in site['devices']:
config = generate_device_config(env, device, timestamp)
all_configs[device['hostname']] = config
os.makedirs('output', exist_ok=True)
# Config validation
print("Sending configs to Claude AI for validation...")
print("=" * 60)
validation = validate_configs(client, all_configs)
print("\nCONFIG VALIDATION REPORT")
print("=" * 60)
print(validation)
with open('output/validation_report.md', 'w') as f:
f.write("# Configuration Validation Report\n\n")
f.write(f"Generated: {timestamp}\n\n")
f.write(validation)
print("\nSaved to: output/validation_report.md")
# Tech refresh plan
print("\n" + "=" * 60)
print("Generating tech refresh plan...")
refresh_plan = generate_refresh_plan(client, inventory)
print("\nTECH REFRESH EXECUTION PLAN")
print("=" * 60)
print(refresh_plan)
with open('output/tech_refresh_plan.md', 'w') as f:
f.write("# Tech Refresh Execution Plan\n\n")
f.write(f"Project: {inventory['project']['name']}\n\n")
f.write(f"Generated: {timestamp}\n\n")
f.write(refresh_plan)
print("\nSaved to: output/tech_refresh_plan.md")
print("\n" + "=" * 60)
print("All documentation generated successfully!")
print("Output directory: output/")
print("=" * 60)
if __name__ == '__main__':
main()
What does this code do?
- The function first regenerates all device configs from the inventory so the script works standalone without running generate_configs.py separately.
- It runs config validation first, printing the report and saving it to output/validation_report.md.
- It then generates the tech refresh plan and saves it to output/tech_refresh_plan.md.
- if __name__ == '__main__': ensures main() only runs when you execute the script directly, not when other files import from it.
- Save ai_review.py.
- Make sure your terminal is in the tech-refresh-automation folder, then run the script:
python3 ai_review.py
The script takes 10-30 seconds to complete because it makes two API calls to Claude. You should see the full config validation report and tech refresh plan printed in your terminal.
- Verify both markdown files were created by running:
ls output/*.md
You should see validation_report.md and tech_refresh_plan.md in the output.
Seeing an authentication error?
- Make sure your ANTHROPIC_API_KEY environment variable is set in the same terminal session where you run the script. Run echo $ANTHROPIC_API_KEY to check.
- If the variable is empty, re-run the export ANTHROPIC_API_KEY="your-key" command from Step 1.
- If you see a "credit balance" error, your free credits may have expired. Check your balance at console.anthropic.com.
Why is my ai_review.py script failing with an API error?
Pro tip: read the validation report closely
Claude often catches things you might not expect. Missing NTP servers, absent login banners, overly permissive firewall rules. These are the same findings a real network audit would surface.
Awesome, I've got everything!
Great. Double check you have saved ai_review.py and that both markdown reports appear in your output/ directory.
I'd like to double check the full code
Here is the complete ai_review.py file up to this point:
import os
import anthropic
from datetime import datetime
from generate_configs import load_inventory, setup_jinja_env, generate_device_config
def get_client():
"""Initialize the Anthropic client."""
return anthropic.Anthropic()
def validate_configs(client, configs):
"""Send generated configs to Claude for validation."""
configs_text = ""
for hostname, config in configs.items():
configs_text += f"\n--- {hostname} ---\n{config}\n"
message = client.messages.create(
model="claude-3-5-haiku-20241022",
max_tokens=4096,
messages=[
{
"role": "user",
"content": (
"You are a senior network engineer reviewing "
"configurations for a tech refresh project. "
"Review these device configurations for:\n\n"
"1. Syntax errors or misconfigurations\n"
"2. Security best practices (missing hardening, "
"open policies)\n"
"3. Consistency across devices (VLAN IDs, ASN "
"numbers, IP addressing)\n"
"4. Missing recommended settings (logging, NTP, "
"SNMP, banners)\n\n"
"For each issue found, specify:\n"
"- Device hostname\n"
"- The specific issue\n"
"- Severity (Critical / Warning / Info)\n"
"- Recommended fix\n\n"
f"Configurations to review:\n{configs_text}"
)
}
]
)
return message.content[0].text
def generate_refresh_plan(client, inventory):
"""Generate a tech refresh plan using Claude."""
inventory_summary = ""
for site in inventory['sites']:
inventory_summary += (
f"\nSite: {site['name']} ({site['location']})\n"
)
for device in site['devices']:
inventory_summary += (
f" - {device['hostname']}: {device['vendor']} "
f"{device['model']} ({device['role']})\n"
)
message = client.messages.create(
model="claude-3-5-haiku-20241022",
max_tokens=4096,
messages=[
{
"role": "user",
"content": (
"You are a senior network tech refresh engineer "
"at a large enterprise. Create a detailed tech "
"refresh execution plan for the following network "
"inventory.\n\n"
"The plan should include:\n"
"1. Refresh sequence (which devices to upgrade "
"first and why)\n"
"2. Risk assessment for each device category\n"
"3. Rollback procedures\n"
"4. Maintenance window recommendations\n"
"5. Pre-refresh and post-refresh validation "
"checks\n"
"6. Dependencies between devices\n\n"
f"Network inventory:\n{inventory_summary}"
)
}
]
)
return message.content[0].text
def main():
client = get_client()
inventory = load_inventory('inventory.yaml')
env = setup_jinja_env('templates')
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# Generate configs first
all_configs = {}
for site in inventory['sites']:
for device in site['devices']:
config = generate_device_config(env, device, timestamp)
all_configs[device['hostname']] = config
os.makedirs('output', exist_ok=True)
# Config validation
print("Sending configs to Claude AI for validation...")
print("=" * 60)
validation = validate_configs(client, all_configs)
print("\nCONFIG VALIDATION REPORT")
print("=" * 60)
print(validation)
with open('output/validation_report.md', 'w') as f:
f.write("# Configuration Validation Report\n\n")
f.write(f"Generated: {timestamp}\n\n")
f.write(validation)
print("\nSaved to: output/validation_report.md")
# Tech refresh plan
print("\n" + "=" * 60)
print("Generating tech refresh plan...")
refresh_plan = generate_refresh_plan(client, inventory)
print("\nTECH REFRESH EXECUTION PLAN")
print("=" * 60)
print(refresh_plan)
with open('output/tech_refresh_plan.md', 'w') as f:
f.write("# Tech Refresh Execution Plan\n\n")
f.write(f"Project: {inventory['project']['name']}\n\n")
f.write(f"Generated: {timestamp}\n\n")
f.write(refresh_plan)
print("\nSaved to: output/tech_refresh_plan.md")
print("\n" + "=" * 60)
print("All documentation generated successfully!")
print("Output directory: output/")
print("=" * 60)
if __name__ == '__main__':
main()
Your automation pipeline now generates configs and validates them with AI. Next up, you will add an SDN technology comparison document to round out the portfolio.
Generate SDN and Virtualization Documentation
Your AI pipeline can now validate configs and produce a tech refresh execution plan. The last piece of the puzzle is SDN and network virtualization knowledge.
Technologies like VXLAN BGP EVPN, VMware NSX, Cisco ACI, and NFV are preferred qualifications for senior network roles. You already built real VXLAN configs in Step 4. In this step, you will use Claude to generate a comprehensive comparison document covering all four technologies.
In this step, get ready to:
- Write an AI function that generates an SDN technology comparison document.
- Run the complete pipeline and review all generated documentation.
Write the SDN comparison generator
You built hands-on VXLAN BGP EVPN configs in Step 4. VMware NSX, Cisco ACI, and NFV require enterprise infrastructure that you cannot deploy locally.
Instead of skipping these technologies, you will have Claude generate a detailed comparison document. This gives you reference material for each technology's architecture, terminology, and real-world deployment scenarios.
Why use AI-generated documentation for NSX, ACI, and NFV?
NSX requires a vSphere cluster. ACI requires an APIC controller and dedicated spine-leaf fabric. NFV requires an NFVI platform. None of these can run on a single laptop.
Generating a structured comparison document is an honest approach. You learn the concepts and terminology without pretending to deploy infrastructure you do not have.
- In ai_review.py, find the generate_refresh_plan function you wrote in Step 5. Add this new function below it:
def generate_sdn_comparison(client):
"""Generate SDN and network virtualization comparison."""
# Send a detailed prompt asking Claude to compare four SDN technologies
message = client.messages.create(
model="claude-3-5-haiku-20241022",
max_tokens=4096,
messages=[
{
"role": "user",
"content": (
"You are a network architect preparing "
"documentation for a tech refresh team. Create a "
"comprehensive comparison of SDN and network "
"virtualization technologies:\n\n"
"1. VXLAN BGP EVPN (Cisco Nexus, Arista)\n"
" - Architecture (spine-leaf, VTEP, VNI, NVE)\n"
" - Use cases and benefits\n"
" - When to deploy\n\n"
"2. VMware NSX\n"
" - Architecture (NSX Manager, transport nodes, "
"overlay)\n"
" - Micro-segmentation capabilities\n"
" - Integration with vSphere\n\n"
"3. Cisco ACI (Application Centric "
"Infrastructure)\n"
" - Architecture (APIC, spine-leaf fabric, "
"EPGs, contracts)\n"
" - Policy-driven networking\n"
" - Comparison with traditional VXLAN EVPN\n\n"
"4. NFV (Network Functions Virtualization)\n"
" - Architecture (VNFs, NFVI, MANO)\n"
" - Common use cases (virtual firewall, virtual "
"router, virtual load balancer)\n"
" - Benefits over physical appliances\n\n"
"For each technology, include:\n"
"- Key concepts and terminology\n"
"- Advantages and limitations\n"
"- Real-world deployment scenarios\n"
"- How it relates to SDN principles (control "
"plane and data plane separation)"
)
}
]
)
# Return Claude's generated comparison document
return message.content[0].text
What does this function do?
- The prompt asks Claude to act as a network architect, producing documentation that a tech refresh team would reference.
- It covers four technologies: VXLAN BGP EVPN (which you already configured), VMware NSX, Cisco ACI, and NFV.
- For each technology, Claude explains architecture, key terminology, advantages, limitations, and how it relates to SDN principles like control plane and data plane separation.
- The max_tokens=4096 gives Claude room to produce a thorough comparison across all four technologies.
- Save ai_review.py.
You will not see a change yet. The next substep calls this function from main() and runs the full pipeline.
Run the complete pipeline and review all documentation
Your main() function currently runs config validation and tech refresh plan generation. Now you will add the SDN comparison call so the pipeline produces all three documents in one run.
- In ai_review.py, scroll down to your main() function and find the line that says:
print("\nSaved to: output/tech_refresh_plan.md")
- Add the following code directly below that line:
# SDN comparison
print("\n" + "=" * 60)
print("Generating SDN and virtualization comparison...")
# Call the SDN comparison function and print the result
sdn_doc = generate_sdn_comparison(client)
print("\nSDN AND NETWORK VIRTUALIZATION COMPARISON")
print("=" * 60)
print(sdn_doc)
# Save the SDN comparison as a markdown file
with open('output/sdn_comparison.md', 'w') as f:
f.write(
"# SDN and Network Virtualization "
"Technology Comparison\n\n"
)
f.write(f"Generated: {timestamp}\n\n")
f.write(sdn_doc)
print("\nSaved to: output/sdn_comparison.md")
# Print final summary
print("\n" + "=" * 60)
print("All documentation generated successfully!")
print("Output directory: output/")
print("=" * 60)
What does this code do?
- It calls generate_sdn_comparison(client) and stores the result in sdn_doc.
- The comparison is saved to output/sdn_comparison.md with a markdown header and timestamp, matching the format of your other reports.
- The final summary block prints a confirmation that all three documents were generated successfully.
- Save ai_review.py.
Awesome, I've got everything!
Double check you have saved ai_review.py before running it.
I'd like to double check the full code
import os
import anthropic
from datetime import datetime
from generate_configs import load_inventory, setup_jinja_env, generate_device_config
def get_client():
"""Initialize the Anthropic client."""
return anthropic.Anthropic()
def validate_configs(client, configs):
"""Send generated configs to Claude for validation."""
configs_text = ""
for hostname, config in configs.items():
configs_text += f"\n--- {hostname} ---\n{config}\n"
message = client.messages.create(
model="claude-3-5-haiku-20241022",
max_tokens=4096,
messages=[
{
"role": "user",
"content": (
"You are a senior network engineer reviewing "
"configurations for a tech refresh project. "
"Review these device configurations for:\n\n"
"1. Syntax errors or misconfigurations\n"
"2. Security best practices (missing hardening, "
"open policies)\n"
"3. Consistency across devices (VLAN IDs, ASN "
"numbers, IP addressing)\n"
"4. Missing recommended settings (logging, NTP, "
"SNMP, banners)\n\n"
"For each issue found, specify:\n"
"- Device hostname\n"
"- The specific issue\n"
"- Severity (Critical / Warning / Info)\n"
"- Recommended fix\n\n"
f"Configurations to review:\n{configs_text}"
)
}
]
)
return message.content[0].text
def generate_refresh_plan(client, inventory):
"""Generate a tech refresh plan using Claude."""
inventory_summary = ""
for site in inventory['sites']:
inventory_summary += (
f"\nSite: {site['name']} ({site['location']})\n"
)
for device in site['devices']:
inventory_summary += (
f" - {device['hostname']}: {device['vendor']} "
f"{device['model']} ({device['role']})\n"
)
message = client.messages.create(
model="claude-3-5-haiku-20241022",
max_tokens=4096,
messages=[
{
"role": "user",
"content": (
"You are a senior network tech refresh engineer "
"at a large enterprise. Create a detailed tech "
"refresh execution plan for the following network "
"inventory.\n\n"
"The plan should include:\n"
"1. Refresh sequence (which devices to upgrade "
"first and why)\n"
"2. Risk assessment for each device category\n"
"3. Rollback procedures\n"
"4. Maintenance window recommendations\n"
"5. Pre-refresh and post-refresh validation "
"checks\n"
"6. Dependencies between devices\n\n"
f"Network inventory:\n{inventory_summary}"
)
}
]
)
return message.content[0].text
def generate_sdn_comparison(client):
"""Generate SDN and network virtualization comparison."""
message = client.messages.create(
model="claude-3-5-haiku-20241022",
max_tokens=4096,
messages=[
{
"role": "user",
"content": (
"You are a network architect preparing "
"documentation for a tech refresh team. Create a "
"comprehensive comparison of SDN and network "
"virtualization technologies:\n\n"
"1. VXLAN BGP EVPN (Cisco Nexus, Arista)\n"
" - Architecture (spine-leaf, VTEP, VNI, NVE)\n"
" - Use cases and benefits\n"
" - When to deploy\n\n"
"2. VMware NSX\n"
" - Architecture (NSX Manager, transport nodes, "
"overlay)\n"
" - Micro-segmentation capabilities\n"
" - Integration with vSphere\n\n"
"3. Cisco ACI (Application Centric "
"Infrastructure)\n"
" - Architecture (APIC, spine-leaf fabric, "
"EPGs, contracts)\n"
" - Policy-driven networking\n"
" - Comparison with traditional VXLAN EVPN\n\n"
"4. NFV (Network Functions Virtualization)\n"
" - Architecture (VNFs, NFVI, MANO)\n"
" - Common use cases (virtual firewall, virtual "
"router, virtual load balancer)\n"
" - Benefits over physical appliances\n\n"
"For each technology, include:\n"
"- Key concepts and terminology\n"
"- Advantages and limitations\n"
"- Real-world deployment scenarios\n"
"- How it relates to SDN principles (control "
"plane and data plane separation)"
)
}
]
)
return message.content[0].text
def main():
client = get_client()
inventory = load_inventory('inventory.yaml')
env = setup_jinja_env('templates')
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# Generate configs first
all_configs = {}
for site in inventory['sites']:
for device in site['devices']:
config = generate_device_config(env, device, timestamp)
all_configs[device['hostname']] = config
os.makedirs('output', exist_ok=True)
# Config validation
print("Sending configs to Claude AI for validation...")
print("=" * 60)
validation = validate_configs(client, all_configs)
print("\nCONFIG VALIDATION REPORT")
print("=" * 60)
print(validation)
with open('output/validation_report.md', 'w') as f:
f.write("# Configuration Validation Report\n\n")
f.write(f"Generated: {timestamp}\n\n")
f.write(validation)
print("\nSaved to: output/validation_report.md")
# Tech refresh plan
print("\n" + "=" * 60)
print("Generating tech refresh plan...")
refresh_plan = generate_refresh_plan(client, inventory)
print("\nTECH REFRESH EXECUTION PLAN")
print("=" * 60)
print(refresh_plan)
with open('output/tech_refresh_plan.md', 'w') as f:
f.write("# Tech Refresh Execution Plan\n\n")
f.write(f"Project: {inventory['project']['name']}\n\n")
f.write(f"Generated: {timestamp}\n\n")
f.write(refresh_plan)
print("\nSaved to: output/tech_refresh_plan.md")
# SDN comparison
print("\n" + "=" * 60)
print("Generating SDN and virtualization comparison...")
sdn_doc = generate_sdn_comparison(client)
print("\nSDN AND NETWORK VIRTUALIZATION COMPARISON")
print("=" * 60)
print(sdn_doc)
with open('output/sdn_comparison.md', 'w') as f:
f.write(
"# SDN and Network Virtualization "
"Technology Comparison\n\n"
)
f.write(f"Generated: {timestamp}\n\n")
f.write(sdn_doc)
print("\nSaved to: output/sdn_comparison.md")
print("\n" + "=" * 60)
print("All documentation generated successfully!")
print("Output directory: output/")
print("=" * 60)
if __name__ == '__main__':
main()
- Run the complete pipeline by running this command:
python3 ai_review.py
The script makes three API calls to Claude, so it takes a minute or two to finish. You should see three sections print to your terminal: the config validation report, the tech refresh execution plan, and the SDN and network virtualization comparison.
What should I see?
Your terminal prints each document as it is generated. At the end, you should see:
All documentation generated successfully!
Output directory: output/
Your output directory should now contain these files:
- Per-device config files (NYC-SPINE-01.cfg, NYC-LEAF-01.cfg, etc.) from Step 3 and 4.
- validation_report.md with Claude's config review.
- tech_refresh_plan.md with the execution plan.
- sdn_comparison.md with the SDN technology comparison.
- Confirm all files exist by running:
ls output/
- You should see .cfg files for each device plus validation_report.md, tech_refresh_plan.md, and sdn_comparison.md in the list.
Seeing an API error?
- If you see AuthenticationError, your API key is not set. Run echo $ANTHROPIC_API_KEY to check. If it is empty, re-export the key in your current terminal session.
- If you see RateLimitError, wait 30 seconds and try again. The free tier has lower rate limits.
- If the script ran the first two calls but failed on the SDN comparison, check that your generate_sdn_comparison function is defined above main() in the file.
Why is my ai_review.py script failing with an API or import error?
- Open output/sdn_comparison.md in your text editor and read through the comparison.
What to look for in the SDN comparison
Skim each section and check that Claude covered the architecture, key terminology, and use cases for each technology. The VXLAN BGP EVPN section should feel familiar because you already built those configs in Step 4.
Pay attention to the VMware NSX and Cisco ACI sections. These are technologies you cannot lab without enterprise infrastructure, so this document is your study reference for understanding how they compare to VXLAN.
You now have a complete portfolio artifact. Your output directory contains per-device configs, an AI-generated validation report, a tech refresh execution plan, and an SDN technology comparison document.
Secret mission
Add Interactive CLI Mode with Config Diffing
Your config generator works, but it always generates everything at once. In a real tech refresh, engineers need to target individual devices and compare old configs against new ones. Can you add a command-line interface that lets you select devices, list your inventory, diff configs, and trigger AI validation with a single flag?
Before you go
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs locally with Python scripts, so there are no ongoing cloud costs. The Anthropic API only charges when you actively make API calls.
Resources you used:
- tech-refresh-automation/ project directory (inventory, templates, scripts, output configs).
- Python packages: Jinja2, PyYAML, anthropic.
- Anthropic API account and API key.
- ANTHROPIC_API_KEY environment variable in ~/.zshrc.
Keep everything running
No action needed. Choose this if you are still actively building or want to extend the project with the Secret Mission.
- Your project files, templates, and generated configs stay in place.
- Your Anthropic API key remains active. You will only be charged if you run the ai_review.py script again.
- Your free API credits will expire after the first billing period, so use them while they last.
Pause - I'll come back to this later
Remove the API key from your active shell to prevent accidental API calls. Keep your project files for later.
- Remove the ANTHROPIC_API_KEY export line from your ~/.zshrc file.
- Run source ~/.zshrc to apply the change.
- Leave your tech-refresh-automation/ directory as-is. You can re-add the API key whenever you return.
- Optionally, go to console.anthropic.com and disable your API key. You can re-enable it later.
Delete - I don't want to use this again
Remove all project resources and clean up your environment completely.
- Delete the project directory by running rm -rf tech-refresh-automation/ from its parent folder.
- Remove the ANTHROPIC_API_KEY export line from your ~/.zshrc file. Then run source ~/.zshrc to apply the change.
- Go to console.anthropic.com, navigate to API Keys, and delete the key you created for this project.
- Uninstall the Python packages if you no longer need them by running pip3 uninstall Jinja2 PyYAML anthropic -y.
Mission accomplished
That's a Wrap!
Nice work! You just built a complete network tech refresh automation pipeline from scratch using Python, Jinja2, and the Claude AI API.
You've learned how to:
- Build a multi-vendor YAML inventory and modular Jinja2 templates that generate switching, routing (BGP/OSPF/IPv6/QoS), VXLAN BGP EVPN overlay, and firewall configurations for Cisco, Arista, Palo Alto, and FortiGate devices.
- Write a Python config generator that renders per-device configurations from templates and inventory data, outputting ready-to-deploy config files.
- Connect the Claude AI API to validate configs, generate a tech refresh execution plan, and produce an SDN technology comparison covering VXLAN, VMware NSX, Cisco ACI, and NFV.
- Secret Mission: Add an interactive CLI with device selection, inventory listing, before/after config diffing, and one-command validation.
Ready to quiz yourself?