Project: VLAN provisioning from a spreadsheet or YAML
In short: Instead of typing VLANs into each switch, you describe them once in a YAML or CSV file. A Python script validates the data, renders configuration with a Jinja2 template, shows what would change, and only then pushes and verifies it. This is the core pattern of data-driven network automation.
Goal and what you will build
Adding VLANs by hand is slow and error-prone: a mistyped ID or a forgotten switch causes outages that are hard to trace. In this project the VLAN list lives in one file that can be reviewed like any other change. A Python script checks the data, turns it into configuration, compares it with what is already on each switch, and applies only the difference. You will practise YAML, validation, Jinja2 templates, Netmiko and safe change habits.
Use two or three lab switches, real or virtual, that you are authorised to change. Never test change automation on production equipment.
Design
# vlans.yaml (reviewed in Git before use)
switches: [acc-sw1, acc-sw2]
vlans:
- id: 10
name: STAFF
- id: 20
name: STUDENTS
- id: 30
name: VOICE
{# templates/vlan.j2 #}
{% for v in vlans %}
vlan {{ v.id }}
name {{ v.name }}
{% endfor %}
If your team prefers spreadsheets, export to CSV with columns id,name and read it with csv.DictReader; the rest of the script stays the same. That is the benefit of separating data from logic.
Build it step by step
- Install the tools:
pip install netmiko pyyaml jinja2. - Write validation rules: IDs between 2 and 4094 (avoid 1 and the reserved range 1002 to 1005 on many Cisco platforms), unique IDs and names, names using only letters, numbers, underscores and hyphens.
- Read existing VLANs from each switch, using
show vlan briefwith TextFSM parsing. - Work out which VLANs are missing or have the wrong name, and render only those.
- Default to dry run: print the planned configuration. Apply only with an explicit
--applyflag. - After applying, read the VLANs again and confirm they match the file.
#!/usr/bin/env python3
import argparse, os, re, sys
import yaml
from jinja2 import Environment, FileSystemLoader, StrictUndefined
from netmiko import ConnectHandler
NAME_OK = re.compile(r"^[A-Za-z0-9_-]{1,32}$")
RESERVED = {1, 1002, 1003, 1004, 1005}
def validate(vlans):
errors, ids, names = [], set(), set()
for v in vlans:
if not 2 <= v["id"] <= 4094 or v["id"] in RESERVED:
errors.append(f"bad id {v['id']}")
if not NAME_OK.match(v["name"]):
errors.append(f"bad name {v['name']!r}")
if v["id"] in ids or v["name"] in names:
errors.append(f"duplicate {v['id']}/{v['name']}")
ids.add(v["id"]); names.add(v["name"])
return errors
def current_vlans(conn):
rows = conn.send_command("show vlan brief", use_textfsm=True)
return {int(r["vlan_id"]): r["vlan_name"] for r in rows}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--apply", action="store_true", help="push changes")
args = ap.parse_args()
data = yaml.safe_load(open("vlans.yaml", encoding="utf-8"))
errors = validate(data["vlans"])
if errors:
sys.exit("Validation failed: " + "; ".join(errors))
env = Environment(loader=FileSystemLoader("templates"),
undefined=StrictUndefined, trim_blocks=True)
tpl = env.get_template("vlan.j2")
for sw in data["switches"]:
with ConnectHandler(device_type="cisco_ios", host=sw,
username=os.environ["NET_USER"],
password=os.environ["NET_PASS"]) as conn:
have = current_vlans(conn)
todo = [v for v in data["vlans"] if have.get(v["id"]) != v["name"]]
if not todo:
print(f"{sw}: already compliant"); continue
config = tpl.render(vlans=todo).splitlines()
print(f"{sw}: planned changes", *config, sep="\n ")
if args.apply:
conn.send_config_set(config)
conn.save_config()
missing = [v["id"] for v in todo
if current_vlans(conn).get(v["id"]) != v["name"]]
print(f"{sw}: verify", "OK" if not missing else f"FAILED {missing}")
if __name__ == "__main__":
main()
Tip: because the script only pushes VLANs that are missing or wrong, running it twice changes nothing the second time. This property, called idempotence, is what makes automation safe to rerun.
How to extend it
- Remove VLANs that exist on the switch but not in the file, behind a separate
--pruneflag and a confirmation prompt. - Add trunk allowed-VLAN lists and access-port assignments to the data model.
- Run validation automatically on every pull request in a CI pipeline, so bad data never reaches a switch.
- Rebuild it with Ansible network modules and compare the two approaches in your README.
- Pull VLANs and sites from a source of truth such as NetBox instead of YAML.
Showing it in your portfolio
Show a short recording or text log of three runs: a dry run listing planned changes, an apply run with verification, and a second apply run reporting "already compliant". That sequence proves you understand validation, dry runs and idempotence. Include a sample vlans.yaml, a diagram of the lab and a section on safety measures: lab only, dry run by default, validation before connection and credentials from the environment.
Practice
Why does the script validate the YAML before connecting to any switch?
What does StrictUndefined do in the Jinja2 environment?
What does idempotent mean for this tool?
Key takeaways
- Keep VLAN data in a reviewed YAML or CSV file, separate from logic.
- Validate IDs, names and duplicates before touching any device.
- Render with Jinja2 and default to a dry run.
- Apply only the difference, then verify on the switch.
- Idempotent tools are safe to run again and again.
Original content written by Mohamed Fayas for FM TecHub. © 2026 FM TecHub. You're welcome to share a link to this page; please don't copy the text elsewhere without permission.