Free tech learning for everyone, from basics to pro

Automation ProjectsProgramming and Application SecurityIntermediate

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.

VLAN provisioning workflowYAML filedesiredVLANsValidateIDs, names,dupesRenderJinja2templateDry runshow diffonlyApplyNetmikoconfigVerifyshow vlanbrief
Figure 1. Data flows from a reviewed YAML file through validation and a template to the switch, with a dry run before any change.

Design

Separation of concernsDatavlans.yaml: whatTemplatevlan.j2: howLogicprovision.py: checksTransportNetmiko over SSH
Figure 2. Keeping data, template, logic and transport separate means you can change one without rewriting the others.
# 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

  1. Install the tools: pip install netmiko pyyaml jinja2.
  2. 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.
  3. Read existing VLANs from each switch, using show vlan brief with TextFSM parsing.
  4. Work out which VLANs are missing or have the wrong name, and render only those.
  5. Default to dry run: print the planned configuration. Apply only with an explicit --apply flag.
  6. 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 --prune flag 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?
So bad data such as an invalid ID or duplicate name is caught early and never produces partial changes on some switches.
What does StrictUndefined do in the Jinja2 environment?
It raises an error if the template uses a variable that was not supplied, instead of silently rendering an empty value into the configuration.
What does idempotent mean for this tool?
Running it again with the same data makes no further changes, because it only applies the difference between the file and the switch.

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.
Tracks your learning on this device only. See your progress on the learning paths.

About the author

Mohamed Fayas

Founder of FM TecHub. CCNA certified, with a BTEC HND in Network Engineering, and BEng (Hons) in Computer Networking and Cloud Security. He writes practical, plain-English guides on networking, security and cloud.

More about the author

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.

Stay up to date by email

New notes, tools and security news from FM TecHub, explained simply. Free.

We only use your email to send these updates. See our privacy policy.