F Network Automation with Python: A Complete Practical Guide for Network Engineers (2026) - The Network DNA: Networking, Cloud, and Security Technology Blog

Network Automation with Python: A Complete Practical Guide for Network Engineers (2026)

Network Automation with Python: A Complete Practical Guide for Network Engineers (2026)

Keywords:  Network Automation Python • Python Network Scripts • Netmiko Tutorial • NAPALM Python • Nornir Framework • NETCONF Python • Network DevOps • Cisco Automation Python • Ansible Python Network • REST API Network Automation • Python Paramiko • Jinja2 Network Templates • Network as Code • Infrastructure Automation Python

The network engineer who can only configure devices through a CLI is not obsolete yet — but they will be within a decade. The one who can write a 50-line Python script that configures 200 switches in 3 minutes, validates the result, and opens a ticket if anything fails is the one organizations are actively hiring. This guide teaches exactly that, with working code, real libraries, and honest assessments of when each tool makes sense.

 May 2026  |  ⏱ 35 min read  |   Python 3.11+ • Netmiko • NAPALM • Nornir • NETCONF • REST APIs  |  ⚙ Network Engineers • NetDevOps • CCNP/CCIE Level

What This Guide Covers

Python environment setup → SSH automation with Netmiko → Multi-vendor abstraction with NAPALM → Parallel execution with Nornir → NETCONF & RESTCONF → REST API calls → Jinja2 templates → Real-world scripts for config backup, VLAN deployment, compliance checking, and change management → CI/CD pipelines for network automation

Sections in This Guide

1.  Why Python for Network Automation
2.  Environment Setup & Essential Libraries
3.  SSH Automation with Netmiko
4.  Multi-Vendor Abstraction with NAPALM
5.  Parallel Execution with Nornir
6.  NETCONF and RESTCONF with Python
7.  REST API Automation
8.  Jinja2 Templates for Config Generation
9.  Working with YAML and JSON
10. Real-World Automation Scripts
11. Error Handling and Logging
12. CI/CD Pipelines for Network Automation
13. Best Practices
14. FAQ

1. Why Python for Network Automation

Python isn’t the only language that can automate networks. Go is faster. Rust is safer. JavaScript runs everywhere. But Python dominates network automation for a specific combination of reasons: the library ecosystem (Netmiko, NAPALM, Nornir, ncclient, requests) is richer than any other language for networking, the syntax is close enough to human English that network engineers with no programming background can read and contribute to scripts, and vendors like Cisco, Arista, Juniper, and Fortinet have all built first-party Python SDKs or REST APIs that Python clients consume naturally.

The CCNA, CCNP, and CCIE exam blueprints now include Python basics. Vendor certifications for Arista (ACE) and Juniper (JNCIS-DevOps) are Python-centric. The industry has settled on Python for this domain the same way it settled on SQL for database queries.

Task Manual Time (50 devices) Python Automated
Config backup 45–90 minutes 60–90 seconds
VLAN deployment across all switches 2–4 hours 3–5 minutes
NTP compliance check 1–2 hours + manual report 30 seconds + auto report
Software upgrade (check + push + verify) Full maintenance window + 2 engineers Automated with rollback logic

The honest starting point: Network automation doesn’t replace network knowledge. A Python script that pushes wrong configurations faster than you could type them manually is worse than doing it slowly by hand. You still need to understand routing protocols, VLANs, and security policy. What automation removes is the repetitive execution of decisions you’ve already made correctly.

2. Environment Setup & Essential Libraries

Use Python 3.11 or 3.12. Virtual environments keep project dependencies isolated — don’t install automation libraries into your system Python. Every project gets its own venv.

# Set up a virtual environment for network automation

python3 -m venv netauto-env source netauto-env/bin/activate # Linux / macOS netauto-env\Scripts\activate # Windows pip install netmiko napalm nornir nornir-netmiko nornir-napalm \ ncclient requests paramiko jinja2 pyyaml rich \ nornir-utils pynetbox textfsm ttp genie pyats

Core Network Automation Libraries

Library Purpose Best For Vendor Support
Netmiko SSH connection management CLI-based automation on any SSH device 100+ vendors/device types
NAPALM Multi-vendor abstraction layer Vendor-agnostic get/set operations Cisco IOS/NX-OS/IOS-XR, EOS, JunOS, FortiOS
Nornir Automation framework with threading Running tasks on many devices in parallel Vendor-agnostic (uses plugins)
ncclient NETCONF over SSH Structured config via YANG models Cisco IOS-XE, NX-OS, Juniper, Nokia
requests HTTP REST API calls RESTCONF, vendor APIs, NetBox Any REST API
Jinja2 Template rendering Generating device configs from templates Vendor-agnostic (text templates)

requirements.txt for your project: Always pin library versions in production scripts. pip freeze > requirements.txt captures all installed versions. pip install -r requirements.txt recreates the exact environment on another machine. Unpinned dependencies mean a library update on a colleague’s machine can silently break scripts that worked on yours.

3. SSH Automation with Netmiko

Netmiko (created by Kirk Byers) is the most widely-used Python library for SSH-based network device automation. It handles the SSH connection, login prompts, enable mode, pagination, and device-specific quirks so you don’t have to. Under the hood, it uses Paramiko (the Python SSH library) but abstracts away all the device-specific differences between vendors.

Basic Connection and Command Execution

# netmiko_basic.py — Connect to a Cisco IOS device and run show commands

from netmiko import ConnectHandler # Device connection dictionary cisco_device = { "device_type": "cisco_ios", # see Netmiko docs for full list "host": "192.168.1.1", "username": "admin", "password": "SecurePass123", "secret": "EnableSecret456", # enable password "port": 22, } # Open connection — use context manager to auto-disconnect with ConnectHandler(**cisco_device) as net_connect: net_connect.enable() # enter enable mode # Run a show command and get output as string output = net_connect.send_command("show version") print(output) # Get interface status table interfaces = net_connect.send_command( "show ip interface brief", use_textfsm=True # parse output into structured list of dicts ) for intf in interfaces: print(f"{intf['intf']:20} {intf['ipaddr']:16} {intf['status']}")

Pushing Configuration with Netmiko

# Push configuration commands to a device

from netmiko import ConnectHandler config_commands = [ "interface GigabitEthernet0/1", "description UPLINK-TO-CORE", "ip address 10.0.1.2 255.255.255.252", "no shutdown", "exit", "ip route 0.0.0.0 0.0.0.0 10.0.1.1", "ntp server 216.239.35.0", ] with ConnectHandler(**cisco_device) as net_connect: net_connect.enable() # send_config_set enters config mode, sends commands, exits output = net_connect.send_config_set(config_commands) print(output) # Save the configuration net_connect.save_config() print("Configuration saved.")

Running Against Multiple Devices

# Loop through multiple devices — backup all configs

from netmiko import ConnectHandler from datetime import datetime import os devices = [ {"device_type": "cisco_ios", "host": "10.0.0.1", "username": "admin", "password": "Cisco123!"}, {"device_type": "cisco_ios", "host": "10.0.0.2", "username": "admin", "password": "Cisco123!"}, {"device_type": "arista_eos", "host": "10.0.0.3", "username": "admin", "password": "Arista123!"}, ] timestamp = datetime.now().strftime("%Y%m%d_%H%M") os.makedirs("backups", exist_ok=True) for device in devices: try: with ConnectHandler(**device) as conn: conn.enable() config = conn.send_command("show running-config") filename = f"backups/{device['host']}_{timestamp}.cfg" with open(filename, "w") as f: f.write(config) print(f"✓ {device['host']} backup saved → {filename}") except Exception as e: print(f"✗ {device['host']} FAILED: {e}")

Common Netmiko Device Types

Device Type String Device
cisco_iosCisco IOS, IOS-XE (ISR, Catalyst)
cisco_nxosCisco NX-OS (Nexus switches)
cisco_xrCisco IOS-XR (ASR 9000, NCS)
arista_eosArista EOS
juniper_junosJuniper JunOS
fortinetFortinet FortiOS
linuxLinux servers (SSH)

TextFSM and NTC-templates: The use_textfsm=True parameter parses unstructured CLI output into Python dictionaries using NTC-templates (a community library of TextFSM templates). Before using it, run pip install ntc-templates and verify a template exists for your command (ntc-templates/templates/ on GitHub). If no template exists for your exact command, write one or use TTP (Template Text Parser) which has a more flexible syntax.

4. Multi-Vendor Abstraction with NAPALM

NAPALM (Network Automation and Programmability Abstraction Layer with Multivendor support) provides a unified API across multiple vendor operating systems. Instead of writing vendor-specific commands for each platform, you call get_interfaces() and get the same data structure back whether the device is running IOS, NX-OS, EOS, or JunOS.

NAPALM uses getters (read operations that return structured data) and setters (configuration loading with diff-before-commit and rollback support). The rollback capability is particularly valuable — NAPALM can show you the diff between current and desired config before committing, and roll back automatically if verification fails.

NAPALM Getters

# napalm_getters.py — Read structured data from any supported device

from napalm import get_network_driver import json # Supported drivers: ios, iosxr, nxos_ssh, eos, junos, fortios driver = get_network_driver("ios") device = driver( hostname="192.168.1.1", username="admin", password="SecurePass123", optional_args={"secret": "EnableSecret456"} ) device.open() # Get device facts (hostname, FQDN, vendor, model, OS version, uptime, serial) facts = device.get_facts() print(f"Hostname: {facts['hostname']}") print(f"Model: {facts['model']}") print(f"OS: {facts['os_version']}") print(f"Uptime: {facts['uptime']} seconds") # Get interface details interfaces = device.get_interfaces() for name, data in interfaces.items(): status = "UP" if data["is_up"] else "DOWN" print(f" {name:25} {status:6} speed={data['speed']} Mbps") # Get BGP neighbors bgp = device.get_bgp_neighbors() print(json.dumps(bgp, indent=2)) # Get environment (CPU, memory, temperature, fans) env = device.get_environment() print(f"CPU: {env['cpu'][0]['%usage']}% RAM: {env['memory']['used_ram']} bytes") device.close()

Available NAPALM Getters

Getter Method Returns
get_facts()Hostname, FQDN, vendor, model, serial numbers, OS version, uptime, interface list
get_interfaces()Interface status, speed, MTU, MAC, description per interface
get_interfaces_ip()IP addresses and prefix lengths per interface
get_bgp_neighbors()BGP peer details, ASN, state, prefixes received/sent
get_route_to(destination)Best path and all routes to a specific prefix
get_arp_table()ARP table entries (IP, MAC, interface, age)
get_mac_address_table()MAC table (MAC, VLAN, interface, type)
get_environment()CPU/memory/temperature/fan/PSU status

NAPALM Configuration Loading with Diff and Rollback

# napalm_config.py — Load config with diff-before-commit

from napalm import get_network_driver driver = get_network_driver("eos") # Arista EOS example device = driver("192.168.2.1", "admin", "AristaPass") device.open() # Load candidate configuration from file device.load_merge_candidate(filename="configs/switch01_new.cfg") # Show the diff between current and candidate config diff = device.compare_config() if diff: print("Changes to be applied:") print(diff) confirm = input("Apply these changes? [y/N]: ") if confirm.lower() == "y": device.commit_config() print("✓ Configuration committed.") else: device.discard_config() # abandon candidate print("✗ Changes discarded.") else: print("No changes detected — device already in desired state.") device.discard_config() device.close()

5. Parallel Execution with Nornir

Nornir is a pure Python automation framework. Where Ansible is a configuration management tool that happens to work on networks, Nornir is built specifically for network automation. The key difference: Nornir runs tasks against all devices simultaneously using Python threads. Connecting to and configuring 50 switches takes the same time as configuring 1, if your thread pool is large enough.

Nornir Inventory Files

Nornir reads from hosts.yaml, groups.yaml, and defaults.yaml. Define your entire network inventory in these files.

# hosts.yaml

sw-access-01: hostname: 10.0.1.11 groups: - cisco_access data: site: HQ role: access sw-access-02: hostname: 10.0.1.12 groups: - cisco_access data: site: HQ role: access rtr-core-01: hostname: 10.0.1.1 groups: - cisco_routers data: site: HQ role: core_router

# groups.yaml

cisco_access: platform: ios connection_options: netmiko: extras: secret: "EnableSecret456" cisco_routers: platform: ios connection_options: netmiko: extras: secret: "EnableSecret456"

# defaults.yaml

username: admin password: NetworkPass123 port: 22

Nornir Task Execution

# nornir_tasks.py — Run tasks in parallel across all devices

from nornir import InitNornir from nornir_netmiko.tasks import netmiko_send_command, netmiko_send_config from nornir_utils.plugins.functions import print_result # Initialize Nornir from config file nr = InitNornir(config_file="config.yaml") # Filter to only access switches access_switches = nr.filter(groups=["cisco_access"]) # Run 'show version' on all access switches in parallel result = access_switches.run( task=netmiko_send_command, command_string="show version", name="Get device version" ) print_result(result) # Define a custom task function def deploy_ntp(task): """Configure NTP servers on all devices.""" commands = [ "ntp server 216.239.35.0 prefer", "ntp server 216.239.35.4", "service timestamps log datetime msec", ] result = task.run( task=netmiko_send_config, config_commands=commands ) task.run( task=netmiko_send_command, command_string="write memory" ) return result # Run custom task on all devices simultaneously result = nr.run(task=deploy_ntp, name="Deploy NTP") print_result(result) # Check for failures failed_devices = [host for host, multi in result.items() if multi.failed] if failed_devices: print(f"\n⚠ FAILED: {failed_devices}") else: print(f"\n✓ All {len(nr.inventory.hosts)} devices updated successfully")

Nornir vs Ansible for network tasks: Nornir runs entirely in Python — debugging is straightforward, you can use Python’s full ecosystem (logging, type hints, unit tests), and there’s no YAML DSL to fight with. Ansible has a larger community and many pre-built roles. The choice usually comes down to team background: engineers comfortable with Python prefer Nornir; teams with more ops/DevOps background prefer Ansible. Both are production-grade; neither is wrong.

6. NETCONF and RESTCONF with Python

SSH CLI automation works, but it has a fundamental flaw: CLI output is text, and parsing text is fragile. Change the IOS version, the output format changes, your regex breaks. NETCONF and RESTCONF solve this by returning structured data (XML or JSON) based on YANG models. The data has a defined schema, so version upgrades don’t break your parser as long as the YANG model is backward compatible.

NETCONF with ncclient

# netconf_example.py — NETCONF on Cisco IOS-XE (enable with: netconf-yang)

from ncclient import manager import xmltodict import json # Connect via NETCONF (port 830) with manager.connect( host="192.168.1.1", port=830, username="admin", password="SecurePass123", hostkey_verify=False, device_params={"name": "iosxe"} ) as m: # Get all interfaces using YANG filter (ietf-interfaces model) interface_filter = """ <filter> <interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces"> <interface/> </interfaces> </filter> """ response = m.get_config(source="running", filter=interface_filter) # Convert XML to Python dict data = xmltodict.parse(response.xml) interfaces = data["rpc-reply"]["data"]["interfaces"]["interface"] for intf in interfaces: print(f"Interface: {intf['name']:25} Type: {intf.get('type','N/A')}") # Configure an interface via NETCONF edit-config config_payload = """ <config> <interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces"> <interface> <name>GigabitEthernet2</name> <description>PYTHON-CONFIGURED</description> <enabled>true</enabled> <ipv4 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip"> <address> <ip>10.1.1.1</ip> <prefix-length>24</prefix-length> </address> </ipv4> </interface> </interfaces> </config> """ m.edit_config(target="running", config=config_payload) print("✓ Interface configured via NETCONF")

RESTCONF with Python requests

# restconf_example.py — RESTCONF on IOS-XE (enable: ip http secure-server + restconf)

import requests import json requests.packages.urllib3.disable_warnings() # suppress self-signed cert warnings BASE_URL = "https://192.168.1.1:443/restconf" HEADERS = { "Content-Type": "application/yang-data+json", "Accept": "application/yang-data+json", } AUTH = ("admin", "SecurePass123") # GET — retrieve interface configuration url = f"{BASE_URL}/data/ietf-interfaces:interfaces" response = requests.get(url, headers=HEADERS, auth=AUTH, verify=False) response.raise_for_status() data = response.json() print(json.dumps(data, indent=2)) # GET — BGP configuration bgp_url = f"{BASE_URL}/data/Cisco-IOS-XE-bgp-oper:bgp-state-data" bgp_resp = requests.get(bgp_url, headers=HEADERS, auth=AUTH, verify=False) print(json.dumps(bgp_resp.json(), indent=2)) # PATCH — update interface description patch_url = f"{BASE_URL}/data/ietf-interfaces:interfaces/interface=GigabitEthernet1" payload = { "ietf-interfaces:interface": { "name": "GigabitEthernet1", "description": "WAN-LINK-CONFIGURED-BY-PYTHON", "enabled": True } } patch_resp = requests.patch( patch_url, headers=HEADERS, auth=AUTH, data=json.dumps(payload), verify=False ) print(f"PATCH status: {patch_resp.status_code}") # 204 = success, no body

7. REST API Automation

Many modern network management platforms — Cisco DNA Center (Catalyst Center), Meraki, FortiManager, Palo Alto Panorama, NetBox — expose REST APIs. These are often the fastest path to automation for platform-level operations that NETCONF doesn’t cover.

Cisco DNA Center (Catalyst Center) API

# dnac_api.py — Interact with Cisco Catalyst Center REST API

import requests import json DNAC_HOST = "https://sandboxdnac.cisco.com" USERNAME = "devnetuser" PASSWORD = "Cisco123!" def get_auth_token(): """Get JWT token from DNA Center auth endpoint.""" url = f"{DNAC_HOST}/dna/system/api/v1/auth/token" response = requests.post(url, auth=(USERNAME, PASSWORD), verify=False) response.raise_for_status() return response.json()["Token"] def get_devices(token): """Get list of all network devices managed by DNA Center.""" headers = {"x-auth-token": token, "Content-Type": "application/json"} url = f"{DNAC_HOST}/dna/intent/api/v1/network-device" response = requests.get(url, headers=headers, verify=False) return response.json()["response"] def run_command_on_device(token, device_id, command): """Run a CLI command on a specific device and get output.""" headers = {"x-auth-token": token, "Content-Type": "application/json"} url = f"{DNAC_HOST}/dna/intent/api/v1/network-device-poller/cli/read-request" payload = { "commands": [command], "deviceUuids": [device_id] } response = requests.post(url, headers=headers, json=payload, verify=False) return response.json() # Main execution token = get_auth_token() devices = get_devices(token) for device in devices[:5]: # first 5 devices print(f"{device['hostname']:30} {device['platformId']:20} {device['softwareVersion']}")

NetBox REST API for IPAM and Inventory

# netbox_api.py — Query NetBox IPAM via pynetbox or raw requests

import pynetbox # Connect to NetBox nb = pynetbox.api( "https://netbox.example.com", token="your-netbox-api-token" ) # Get all active network devices devices = nb.dcim.devices.filter(status="active", site="hq") for device in devices: print(f"{device.name:30} {str(device.device_type):25} {device.primary_ip}") # Build Nornir inventory from NetBox automatically nornir_hosts = {} for device in nb.dcim.devices.filter(status="active"): if device.primary_ip: nornir_hosts[device.name] = { "hostname": str(device.primary_ip).split("/")[0], "platform": device.platform.slug if device.platform else "ios", "data": { "site": device.site.name, "role": device.device_role.name, } } print(f"Built inventory: {len(nornir_hosts)} devices")

Cisco DevNet Sandbox for practicing: Cisco provides free, always-on sandboxes at devnetsandbox.cisco.com where you can test against real IOS-XE, NX-OS, ACI, and DNA Center environments without needing lab hardware. The Always-On IOS-XE sandbox at sandbox-iosxe-latest-1.cisco.com is accessible via SSH and NETCONF/RESTCONF. Start your practice there before touching production.

8. Jinja2 Templates for Configuration Generation

Jinja2 is a Python templating engine that generates text (in this case, network device configurations) by combining a template with variable data. Instead of editing 50 switch configurations individually, you maintain one template and a data file per switch. Change the template once; regenerate all 50 configurations.

Jinja2 Template for Cisco Switch Baseline

# templates/switch_baseline.j2

hostname {{ hostname }} ip domain-name {{ domain }} {# NTP servers loop #} {% for ntp in ntp_servers %} ntp server {{ ntp }}{% if loop.first %} prefer{% endif %} {% endfor %} {# Configure VLANs #} {% for vlan in vlans %} vlan {{ vlan.id }} name {{ vlan.name }} {% endfor %} {# Configure interfaces #} {% for interface in interfaces %} interface {{ interface.name }} description {{ interface.description }} {% if interface.type == "access" %} switchport mode access switchport access vlan {{ interface.vlan }} spanning-tree portfast {% elif interface.type == "trunk" %} switchport mode trunk switchport trunk allowed vlan {% for v in interface.vlans %}{{ v }}{% if not loop.last %},{% endif %}{% endfor %} {% elif interface.type == "routed" %} no switchport ip address {{ interface.ip }} {{ interface.mask }} {% endif %} no shutdown ! {% endfor %} {# Management SVI #} interface Vlan{{ mgmt_vlan }} ip address {{ mgmt_ip }} {{ mgmt_mask }} no shutdown ! ip default-gateway {{ mgmt_gateway }} {# SSH hardening #} crypto key generate rsa modulus 4096 ip ssh version 2 ip ssh authentication-retries 3 line vty 0 15 transport input ssh login local !

# jinja2_render.py — Generate config from template + YAML data

from jinja2 import Environment, FileSystemLoader import yaml import os # Set up Jinja2 environment env = Environment( loader=FileSystemLoader("templates"), trim_blocks=True, lstrip_blocks=True ) template = env.get_template("switch_baseline.j2") # Load per-device data from YAML file with open("data/sw-access-01.yaml") as f: device_data = yaml.safe_load(f) # Render the template with device-specific data rendered_config = template.render(**device_data) # Save rendered configuration os.makedirs("rendered_configs", exist_ok=True) output_file = f"rendered_configs/{device_data['hostname']}.cfg" with open(output_file, "w") as f: f.write(rendered_config) print(f"✓ Config rendered → {output_file}") print(rendered_config)

# data/sw-access-01.yaml

hostname: sw-access-01 domain: corp.local mgmt_vlan: 99 mgmt_ip: 192.168.99.11 mgmt_mask: 255.255.255.0 mgmt_gateway: 192.168.99.1 ntp_servers: - 216.239.35.0 - 216.239.35.4 vlans: - id: 10 name: Corporate-Users - id: 20 name: Voice - id: 99 name: Management - id: 999 name: Native-Unused interfaces: - name: GigabitEthernet0/1 description: "PC-Port-Cubicle-101" type: access vlan: 10 - name: GigabitEthernet0/2 description: "IP-Phone-Cubicle-101" type: access vlan: 20 - name: GigabitEthernet0/24 description: "UPLINK-TO-DISTRIBUTION" type: trunk vlans: [10, 20, 99]

9. Working with YAML and JSON in Network Automation

YAML is the standard format for network automation data: device inventories, variable files for templates, and configuration as data. JSON is the return format from most REST APIs and RESTCONF. You need to be comfortable reading and writing both.

# yaml_json_basics.py — Essential operations you'll use constantly

import yaml import json from pathlib import Path # --- YAML operations --- # Read YAML file → Python dict with open("devices.yaml") as f: devices = yaml.safe_load(f) # use safe_load, never load() # Read all YAML files in a directory (multi-device inventory) device_data = {} for yaml_file in Path("data/").glob("*.yaml"): with open(yaml_file) as f: data = yaml.safe_load(f) device_data[data["hostname"]] = data # Write Python dict → YAML report = {"status": "compliant", "devices": 50, "failed": []} with open("report.yaml", "w") as f: yaml.dump(report, f, default_flow_style=False) # --- JSON operations --- # Parse JSON API response json_string = '{"hostname": "rtr-01", "version": "17.9.4"}' data = json.loads(json_string) print(data["hostname"]) # Pretty-print nested JSON/dict print(json.dumps(data, indent=2, sort_keys=True)) # Save JSON to file with open("inventory.json", "w") as f: json.dump(data, f, indent=2) # Convert between YAML and JSON (common in automation pipelines) with open("devices.yaml") as f: yaml_data = yaml.safe_load(f) json_output = json.dumps(yaml_data, indent=2) print(json_output)

Validating YAML structure with Pydantic: If your automation reads YAML device data and passes it to templates, a typo in the YAML (wrong key name, wrong data type) causes confusing runtime errors. Use Pydantic (Python library for data validation) to define a schema for your device data and validate YAML files against it before processing. This catches errors at the data level rather than partway through a 50-device deployment.

10. Real-World Automation Scripts

Script 1: Network Compliance Checker

Checks all devices against a security baseline (NTP configured, SSH version 2, no Telnet, correct SNMP version) and generates a CSV report.

# compliance_checker.py

from nornir import InitNornir from nornir_netmiko.tasks import netmiko_send_command import csv from datetime import datetime # Compliance checks — each returns True if compliant CHECKS = { "NTP_configured": lambda output: "synchronized" in output.lower(), "SSH_v2_only": lambda output: "version 2" in output.lower(), "No_Telnet": lambda output: "transport input telnet" not in output.lower(), "SNMPv3_enabled": lambda output: "snmp-server user" in output.lower(), } COMMANDS = { "NTP_configured": "show ntp status", "SSH_v2_only": "show ip ssh", "No_Telnet": "show running-config | include transport input", "SNMPv3_enabled": "show running-config | include snmp-server user", } def compliance_check(task): results = {"host": task.host.name, "ip": str(task.host.hostname)} for check_name, command in COMMANDS.items(): output = task.run(netmiko_send_command, command_string=command)[0].result results[check_name] = "PASS" if CHECKS[check_name](output) else "FAIL" task.host["compliance"] = results nr = InitNornir(config_file="config.yaml") nr.run(task=compliance_check) # Write CSV report timestamp = datetime.now().strftime("%Y%m%d_%H%M") report_file = f"compliance_report_{timestamp}.csv" fieldnames = ["host", "ip"] + list(CHECKS.keys()) with open(report_file, "w", newline="") as csvfile: writer = csv.DictWriter(csvfile, fieldnames=fieldnames) writer.writeheader() for host_obj in nr.inventory.hosts.values(): if "compliance" in host_obj: writer.writerow(host_obj["compliance"]) print(f"✓ Report saved: {report_file}")

Script 2: Automated VLAN Deployment

# vlan_deploy.py — Deploy new VLANs to all access switches

from nornir import InitNornir from nornir_netmiko.tasks import netmiko_send_command, netmiko_send_config from nornir_utils.plugins.functions import print_result import yaml # Load VLAN definition from YAML with open("vlan_deploy.yaml") as f: vlan_config = yaml.safe_load(f) def deploy_vlans(task, vlans): """Deploy VLANs to a device and add to trunk ports.""" commands = [] # Create VLANs for vlan in vlans: commands.extend([ f"vlan {vlan['id']}", f"name {vlan['name']}", "exit", ]) # Add VLANs to all trunk ports existing_trunks = task.run( netmiko_send_command, command_string="show interfaces trunk | include Gi|Te" )[0].result for line in existing_trunks.splitlines(): if line.strip(): interface = line.split()[0] vlan_ids = ",".join([str(v["id"]) for v in vlans]) commands.extend([ f"interface {interface}", f"switchport trunk allowed vlan add {vlan_ids}", "exit", ]) task.run(netmiko_send_config, config_commands=commands) task.run(netmiko_send_command, command_string="write memory") nr = InitNornir(config_file="config.yaml") access_switches = nr.filter(groups=["cisco_access"]) result = access_switches.run( task=deploy_vlans, vlans=vlan_config["vlans"], name="Deploy VLANs" ) print_result(result)

Script 3: Interface Down Alerter

# interface_monitor.py — Check for down interfaces across all devices

from napalm import get_network_driver import yaml import smtplib from email.mime.text import MIMEText with open("devices.yaml") as f: devices = yaml.safe_load(f)["devices"] down_interfaces = [] for device in devices: driver = get_network_driver(device["platform"]) d = driver(device["host"], device["username"], device["password"]) try: d.open() interfaces = d.get_interfaces() for name, data in interfaces.items(): if data["is_enabled"] and not data["is_up"]: down_interfaces.append({ "device": device["host"], "interface": name, "desc": data["description"] }) d.close() except Exception as e: print(f"Could not check {device['host']}: {e}") if down_interfaces: body = "Down interfaces detected:\n\n" for item in down_interfaces: body += f" {item['device']:20} {item['interface']:25} {item['desc']}\n" print(body) # Send email alert (configure SMTP server details)

11. Error Handling and Logging

A script that crashes with an unhandled exception mid-deployment has pushed partial configuration to some devices and none to others — the worst possible state. Proper error handling ensures partial failures are caught, logged, and reported without affecting other devices. Logging to a file gives you a record of what happened when something goes wrong three days later.

# logging_setup.py — Production-grade logging for automation scripts

import logging import sys from datetime import datetime from netmiko.exceptions import ( NetmikoTimeoutException, NetmikoAuthenticationException, NetmikoBaseException ) # Set up structured logging to file + console timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") log_file = f"logs/automation_{timestamp}.log" logging.basicConfig( level=logging.INFO, format="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s", datefmt="%Y-%m-%d %H:%M:%S", handlers=[ logging.FileHandler(log_file), logging.StreamHandler(sys.stdout) ] ) logger = logging.getLogger("netauto") def safe_connect_and_run(device_dict, commands): """Connect and run commands with full exception handling.""" host = device_dict["host"] try: from netmiko import ConnectHandler with ConnectHandler(**device_dict) as conn: conn.enable() output = conn.send_config_set(commands) conn.save_config() logger.info(f"SUCCESS | {host} | {len(commands)} commands applied") return {"host": host, "status": "success", "output": output} except NetmikoAuthenticationException: logger.error(f"AUTH FAILED | {host} | Check credentials") return {"host": host, "status": "auth_failed"} except NetmikoTimeoutException: logger.error(f"TIMEOUT | {host} | Device unreachable or SSH not enabled") return {"host": host, "status": "timeout"} except NetmikoBaseException as e: logger.error(f"NETMIKO ERROR | {host} | {str(e)}") return {"host": host, "status": "error", "detail": str(e)} except Exception as e: logger.critical(f"UNEXPECTED | {host} | {type(e).__name__}: {str(e)}") return {"host": host, "status": "unexpected_error"}

12. CI/CD Pipelines for Network Automation

A CI/CD pipeline for network automation runs your scripts through a validation and testing process before they reach production devices. Every change is a Git commit. The pipeline runs automatically on every commit: syntax check, lint, unit tests in a virtual lab, then deploy to production after a peer review.

Pipeline Stage What Runs Tools
1. Lint & Syntax Python syntax check, YAML validation, import order, code style flake8, pylint, yamllint, black
2. Unit Tests Test template rendering, data validation, function logic with mocked device responses pytest, unittest.mock, pytest-netmiko
3. Lab Test Run against virtual devices (Cisco CML, GNS3, Containerlab) to validate real execution Containerlab, Cisco CML, Eve-NG
4. Production Deploy Push to production after peer review and lab test pass; automated or manual approval gate GitLab CI, GitHub Actions, Jenkins

# .gitlab-ci.yml — GitLab CI pipeline for network automation

stages: - lint - test - deploy lint: stage: lint image: python:3.12 script: - pip install flake8 yamllint black - black --check . - flake8 . --max-line-length=100 - yamllint inventory/ unit_tests: stage: test image: python:3.12 script: - pip install -r requirements.txt pytest - pytest tests/ -v --tb=short deploy_production: stage: deploy image: python:3.12 when: manual # requires human approval in GitLab UI only: - main script: - pip install -r requirements.txt - python automation/deploy.py --env production environment: name: production

13. Best Practices for Network Automation

Practice Why It Matters
Never store credentials in code Use environment variables, HashiCorp Vault, or Ansible Vault. Credentials in source code end up in Git history permanently.
Test on one device first Even with lab testing, run on a single production device before all 200. Catch production-specific surprises before they affect everyone.
Use dry-run mode Build a --dry-run flag that shows what would be done without doing it. Essential for pre-deployment review.
Back up before changing Always pull and save running-config before pushing changes. NAPALM’s compare_config() and Netmiko’s backup scripts make this easy.
Idempotent scripts Running the same script twice should produce the same result. Check if a VLAN already exists before creating it. NAPALM and NETCONF naturally handle this; CLI-based scripts need explicit checks.
Limit thread count on Nornir Default is 100 threads. Connecting to 200 devices simultaneously can overwhelm RADIUS/TACACS servers. Set num_workers: 20 in Nornir config for safer parallel execution.
Version control everything Scripts, templates, inventory files, and variable files all belong in Git. Every change is attributable, reversible, and reviewable. Never edit automation scripts directly on a shared server.

# credentials.py — Safe credential handling with environment variables

import os from getpass import getpass # Option 1: Environment variables (set before running script) # export NET_USERNAME=admin; export NET_PASSWORD=SecurePass123 username = os.environ.get("NET_USERNAME") password = os.environ.get("NET_PASSWORD") # Option 2: Interactive prompt (for one-off scripts) if not password: password = getpass("Network device password: ") # password not echoed to terminal # Option 3: From vault (production — install: pip install hvac) import hvac client = hvac.Client(url="https://vault.example.com", token=os.environ["VAULT_TOKEN"]) secret = client.secrets.kv.read_secret_version(path="network/credentials") username = secret["data"]["data"]["username"] password = secret["data"]["data"]["password"]

14. Frequently Asked Questions

How much Python do I need to know before starting network automation?

Variables, conditionals, loops, functions, and dictionaries — that’s the core. You don’t need object-oriented programming, decorators, or metaclasses to write useful automation scripts. Kirk Byers’ free Python for Network Engineers course (free at PyNet.com) covers exactly the right fundamentals for network engineers without drowning you in computer science concepts that don’t apply to this domain.

Should I use Ansible or Python (Nornir) for network automation?

Ansible is better when your team has more ops/DevOps background and prefers YAML playbooks over Python code. Nornir is better when your team has Python skills and needs maximum flexibility, debuggability, and performance. Many teams use both: Ansible for simple, repeatable workflows (configuration deployment, compliance checks) and Nornir/custom Python for complex logic (multi-step workflows with conditional logic, real-time validation, and complex error handling).

What is the difference between Netmiko, NAPALM, and Nornir?

Think of them as complementary layers. Netmiko handles the SSH connection and device-specific quirks — it’s the transport layer. NAPALM sits on top of Netmiko and other transport methods to provide a vendor-agnostic API with structured returns (getters) and configuration management with diff/rollback. Nornir is a framework that manages running tasks against an inventory of devices in parallel — it uses Netmiko and NAPALM as plugins. Most production automation uses all three together.

How do I handle devices that require jump hosts or bastion servers?

Configure SSH ProxyJump in the device’s SSH config or pass it to Netmiko via the ssh_config_file optional argument. For Nornir, set the proxy host in the connection options per host group. Netmiko also supports sock for SOCKS proxies and has explicit proxy support via the ProxyCommand option that maps directly to standard SSH client tunneling.

Which Python library parses Cisco CLI output into structured data?

Three options. TextFSM + NTC-templates: the most established option; thousands of pre-built templates for Cisco, Arista, Juniper commands; used via use_textfsm=True in Netmiko. TTP (Template Text Parser): more flexible template syntax than TextFSM; easier to write custom templates. Cisco Genie / pyATS: Cisco’s own parsing library; works with Cisco devices specifically and returns deeply structured Python objects; best for Cisco-heavy environments that want the most complete structured output.

Where should I practice Python network automation without real hardware?

Four options in roughly increasing complexity. Cisco DevNet Sandbox: free, always-on IOS-XE, NX-OS, and DNA Center environments; no setup required. Containerlab: deploy virtual network topologies using Docker containers; supports Cisco, Arista, Juniper, Nokia, FRRouting virtual images; runs on any Linux machine. GNS3 or EVE-NG: full network emulation with real vendor images; requires hardware images from Cisco (you need a CCO account). Cisco Modeling Labs (CML): Cisco’s commercial network simulation platform; supported, regularly updated images; annual license.

Quick Reference: Essential Commands & Patterns

Code / Command What It Does
pip install netmiko napalm nornirInstall core network automation libraries
ConnectHandler(**device_dict)Open Netmiko SSH connection
conn.send_command("show version")Run show command and return output
conn.send_config_set([...])Push list of config commands
device.get_facts()NAPALM getter: hostname, model, OS, uptime
device.compare_config()Show diff before committing config
InitNornir(config_file="config.yaml")Initialize Nornir from config and inventory
nr.filter(groups=["cisco_access"])Filter Nornir inventory to specific group
nr.run(task=my_function)Run task in parallel on all devices
yaml.safe_load(file)Parse YAML file to Python dict safely

Your Network Automation Learning Path

Week 1–2 Python basics: variables, loops, functions, dicts, file I/O, error handling. Write a script that reads a YAML file and prints each device’s hostname.
Week 3–4 Netmiko: connect to a device in DevNet sandbox, run show commands, parse output. Write a backup script. Push a simple config change.
Week 5–6 Jinja2 templates: generate a complete switch config from a YAML data file. NAPALM: use getters to pull structured data from two different vendors.
Week 7–8 Nornir: run a compliance check against a 10-device inventory. NETCONF: query interface data from an IOS-XE device using ncclient.
Month 3+ CI/CD pipelines, Git-based change management, NetBox integration, REST APIs, Containerlab for automated testing. Build one complete automation project end-to-end.
Tags: Python Network Automation Netmiko NAPALM Python Nornir Framework NETCONF RESTCONF Jinja2 Templates NetDevOps Network as Code