Netmiko Fundamentals
Your First Real Device Automation Project
What you'll learn
- How the complete project looks with a real Netmiko connection in place
- Why this capstone's default path is read-only, not configuration-changing
- Two ways to complete this module depending on what device access you have
- That the no-device path from Phase 2.6 remains fully valid and complete
- A small extension challenge to test your understanding of the whole project
Listen to a guided explanation of this lesson.
Spoken narration isn't supported in this browser.
The same project, one real piece
Six lessons in this module built toward exactly one thing: replacing simulate_connection() with something real, without touching anything else in the project from "Your First Network Automation." This lesson assembles that.
- CSV inventorydevices.csv — unchanged from Phase 2.6
- Load devicescsv.DictReader
- Process each devicefor device in valid_devices:
- connect_and_check()real SSH via Netmiko
- show commands, parsedsend_command() + is_ntp_synced() / is_vlan_present()
- Validationcheck_compliance(state) — unchanged
- Categorize resultcompliant / non-compliant / unreachable
- JSON reportcompliance_report.json — unchanged
- Terminal summarycounts + where to find the report
Compare that against Phase 2.6 Lesson 5's diagram: every stage is the same. Only one box changed what's behind it.
Read-only, by design
This capstone's default project only ever runs show commands — it never pushes a configuration change. "Making Configuration Changes Safely" exists so you understand send_config_set(), but it's separate, optional, lab-only material. This project checks a device's state; it never modifies it.
The complete script
import csv
import json
import getpass
from netmiko import ConnectHandler, NetmikoTimeoutException, NetmikoAuthenticationException
def is_valid_device(device):
"""A device record needs at least a hostname and an IP to be usable."""
return bool(device.get("hostname")) and bool(device.get("ip"))
def is_ntp_synced(show_ntp_output):
"""Check whether 'show ntp status' output indicates the clock is synchronized."""
first_line = show_ntp_output.strip().splitlines()[0] if show_ntp_output.strip() else ""
return first_line.startswith("Clock is synchronized")
def is_vlan_present(show_vlan_output, vlan_id):
"""Check whether a given VLAN ID appears in 'show vlan brief' output."""
for line in show_vlan_output.splitlines():
stripped = line.strip()
first_word = stripped.split(" ")[0] if stripped else ""
if first_word == str(vlan_id):
return True
return False
def connect_and_check(device, username, password):
"""
Connects to a real device over SSH (read-only) and returns its NTP
and VLAN compliance state.
"""
try:
connection = ConnectHandler(
device_type=device["platform"],
host=device["ip"],
username=username,
password=password,
conn_timeout=10,
)
except NetmikoTimeoutException:
raise ConnectionError(f"{device['hostname']} did not respond (timeout)")
except NetmikoAuthenticationException:
raise ConnectionError(f"{device['hostname']} rejected the credentials (authentication failure)")
ntp_output = connection.send_command("show ntp status")
vlan_output = connection.send_command("show vlan brief")
connection.disconnect()
return {
"reachable": True,
"ntp_synced": is_ntp_synced(ntp_output),
"vlan_compliant": is_vlan_present(vlan_output, 20),
}
def check_compliance(state):
"""Turn a device's raw state into a plain-language compliance status."""
if state["ntp_synced"]:
if state["vlan_compliant"]:
return "compliant"
else:
return "vlan non-compliant"
else:
return "ntp non-compliant"
with open("devices.csv") as f:
devices = list(csv.DictReader(f))
valid_devices = []
for device in devices:
if is_valid_device(device):
valid_devices.append(device)
else:
print(f"Skipping invalid inventory record: {device}")
username = input("Username: ")
password = getpass.getpass("Password: ")
results = []
for device in valid_devices:
print(f"Checking {device['hostname']}...")
try:
state = connect_and_check(device, username, password)
except ConnectionError as e:
print(f"{device['hostname']}: {e}")
results.append({
"hostname": device["hostname"],
"ip": device["ip"],
"status": "unreachable",
})
continue
status = check_compliance(state)
print(f"{device['hostname']}: {status}")
results.append({
"hostname": device["hostname"],
"ip": device["ip"],
"status": status,
})
with open("compliance_report.json", "w") as f:
json.dump(results, f, indent=4)
compliant = 0
needs_attention = 0
for result in results:
if result["status"] == "compliant":
compliant += 1
else:
needs_attention += 1
print()
print("--- Summary ---")
print(f"Checked: {len(results)} devices")
print(f"Compliant: {compliant}")
print(f"Needs attention: {needs_attention}")
print("Report written to compliance_report.json")Every function here was built across this module and the last one — nothing here is new to this lesson. devices.csv keeps the exact hostname/ip/platform format from Phase 2.6; credentials are never in that file.
Two ways to complete this project
- Your own hardwareor a personal CML / GNS3 / EVE-NG lab
- Cisco DevNet Sandboxor another currently available Cisco-hosted lab, subject to availability and access requirements
- connect_and_check()runs for real
- A real compliance_report.json
- Nothing beyond Python needed
- simulate_connection()from 'Your First Network Automation' — still fully working
- The exact same project logic
- The same shape of compliance_report.json
Option A — if you have access to a real or virtual device (your own hardware, a personal CML/GNS3/EVE-NG lab, or an external resource like Cisco DevNet Sandbox or another currently available Cisco-hosted lab — subject to that resource's own availability and access requirements, which this curriculum doesn't control), run the script above for real. Supply your own IP in devices.csv and your own credentials at the input()/getpass() prompts.
Option B — if you have no device access at all, that's a complete way to finish this module too. Swap connect_and_check() back for simulate_connection() from "Your First Network Automation" — it still works exactly as it did, unchanged. You've now seen both the mock and the real version of the same seam, and understand precisely what changes between them.
Neither path is 'the real one'
Option A proves the code works against an actual device. Option B proves the code's logic is correct — inventory handling, validation, categorization, reporting — without needing anything beyond Python. Both are legitimate ways to finish this curriculum's beginner track. This platform stays fully usable either way.
A small challenge
Whichever option you used, try this: change the required VLAN in connect_and_check() from 20 to a different number your inventory should actually have, and re-run against your setup (real or simulated). Confirm the compliance status changes exactly the way you'd expect for a device that does or doesn't have that VLAN.
What you've built
Starting from "Start Here," this curriculum's beginner track has gone from "what is network automation" to a real, working tool: an inventory layer, a real SSH connection with proper credential handling and failure recovery, real command parsing, and a JSON report — the same shape of tool real network teams actually run.
Key takeaways
- This capstone reuses the entire Phase 2.6 project unchanged, replacing only simulate_connection() with the real connect_and_check().
- The default project is read-only — it checks NTP and VLAN state, and never pushes configuration.
- Option A (a real or virtual device) and Option B (simulate_connection(), no device needed) are both complete, legitimate ways to finish this module.
- Any external lab resource mentioned here is optional and subject to its own availability — this curriculum never depends on one being reachable.
- The seam this whole project was built around — device → connection/check function → state — held from the first line of Phase 2.6 through the last line of this module.