NetAuto Academy

NAPALM for Network Engineers

Capstone: A Multi-Vendor-Ready State & Compliance Report

Advanced18 min read

What you'll learn

  • Combining drivers, getters, and desired-state validation into one script
  • Where a real device (Option A) fits versus the mock-driver path (Option B)
  • Reading a real, verified end-to-end run and its JSON report
  • What's next: Multi-vendor Automation, where Nornir + NAPALM integrate
🎓 Teach This Lesson

Listen to a guided explanation of this lesson.

Spoken narration isn't supported in this browser.

Why this capstone isn't another VLAN check

A deliberate scenario change

Every capstone since Reliable Automation has validated VLAN compliance. This one doesn't -- not because VLANs stopped mattering, but because repeating that scenario again wouldn't demonstrate anything NAPALM specifically adds. This capstone validates facts, interface state, and management IP addressing -- exactly the structured getters this module actually taught.

  1. SW1-SW5 inventorythe same fleet since Phase 2.6
  2. get_network_driver("mock")Lesson 2 + 6
  3. get_facts() / get_interfaces() / get_interfaces_ip()Lesson 3
  4. desired-state validationhostname, interface description, management IP
  5. compliance classification -> JSON report
Quick checkConcept check
Why does this capstone check facts/interfaces/IP addressing instead of VLAN compliance, unlike every prior capstone since Reliable Automation?

The full script

napalm_state_report.py
import json
import os
from napalm import get_network_driver
from napalm.base.exceptions import ConnectionException, ConnectAuthError

MGMT_INTERFACE = "GigabitEthernet0/1"
EXPECTED_DESCRIPTION = "mgmt-uplink"
EXPECTED_IPS = {
  "SW1": "10.10.10.1", "SW2": "10.10.10.2", "SW3": "10.10.10.3",
  "SW4": "10.10.10.4", "SW5": "10.10.10.5",
}
FLEET = ["SW1", "SW2", "SW3", "SW4", "SW5"]
FAIL_ON_OPEN = {"SW4"}  # Option B: scripted "unreachable" via Lesson 6's fail_on_open

def check_device(hostname):
  driver_cls = get_network_driver("mock")
  optional_args = {"path": os.path.join("fixtures", hostname.lower())}
  if hostname in FAIL_ON_OPEN:
      optional_args["fail_on_open"] = True

  driver = driver_cls(
      hostname=hostname,
      username=os.environ.get("NAPALM_USERNAME", ""),
      password=os.environ.get("NAPALM_PASSWORD", ""),
      optional_args=optional_args,
  )

  try:
      driver.open()
  except ConnectionException as e:
      return {"host": hostname, "status": "unreachable", "detail": str(e)}

  try:
      facts = driver.get_facts()
      interfaces = driver.get_interfaces()
      ips = driver.get_interfaces_ip()
  except ConnectAuthError as e:
      driver.close()
      return {"host": hostname, "status": "auth-failed", "detail": str(e)}

  driver.close()

  issues = []
  if facts["hostname"] != hostname:
      issues.append(f"hostname mismatch: expected {hostname}, got {facts['hostname']}")

  iface = interfaces.get(MGMT_INTERFACE)
  if iface is None:
      issues.append(f"{MGMT_INTERFACE} missing from get_interfaces()")
  elif iface["description"] != EXPECTED_DESCRIPTION:
      issues.append(
          f"{MGMT_INTERFACE} description mismatch: expected "
          f"'{EXPECTED_DESCRIPTION}', got '{iface['description']}'"
      )

  iface_ips = ips.get(MGMT_INTERFACE, {}).get("ipv4", {})
  expected_ip = EXPECTED_IPS[hostname]
  if expected_ip not in iface_ips:
      issues.append(f"{MGMT_INTERFACE} IP mismatch: expected {expected_ip}, got {list(iface_ips.keys())}")

  if issues:
      return {"host": hostname, "status": "non-compliant", "detail": issues}
  return {"host": hostname, "status": "compliant", "detail": "facts/interfaces/IP all match desired state"}

report = {"compliant": [], "non_compliant": [], "unreachable": [], "auth_failed": []}
key_by_status = {"compliant": "compliant", "non-compliant": "non_compliant",
                "unreachable": "unreachable", "auth-failed": "auth_failed"}

for hostname in FLEET:
  result = check_device(hostname)
  report[key_by_status[result["status"]]].append(result)

print(f"Checked: {len(FLEET)} devices")
print(f"compliant: {len(report['compliant'])}")
print(f"non-compliant: {len(report['non_compliant'])}")
print(f"unreachable: {len(report['unreachable'])}")
print(f"auth-failed: {len(report['auth_failed'])}")

with open("napalm_report.json", "w") as f:
  json.dump(report, f, indent=2)
print("Report written to napalm_report.json")

The complete, real, verified capstone script. Reuses the SW1-SW5 fleet's IP scheme (10.10.10.1-5) from every prior module, and Lesson 6's fail_on_open/exception-fixture mechanisms for the failure scenarios.

Quick checkIdentify the correct code
Why does check_device() check facts['hostname'], the interface description, and the management IP separately, rather than stopping at the first mismatch?

Real, verified end-to-end output

terminal output — real, verified run
Checked: 5 devices
compliant: 1
non-compliant: 2
unreachable: 1
auth-failed: 1
Report written to napalm_report.json

Real, verified output from an actual run of the script above.

napalm_report.json — real, verified content
{
"compliant": [
  { "host": "SW1", "status": "compliant", "detail": "facts/interfaces/IP all match desired state" }
],
"non_compliant": [
  { "host": "SW2", "status": "non-compliant",
    "detail": ["GigabitEthernet0/1 description mismatch: expected 'mgmt-uplink', got 'uplink'"] },
  { "host": "SW3", "status": "non-compliant",
    "detail": ["GigabitEthernet0/1 IP mismatch: expected 10.10.10.3, got ['10.10.10.99']"] }
],
"unreachable": [
  { "host": "SW4", "status": "unreachable", "detail": "You told me to do this" }
],
"auth_failed": [
  { "host": "SW5", "status": "auth-failed", "detail": "SW5 rejected the credentials" }
]
}

Real, verified file content -- grep-confirmed to contain no credential of any kind. Note SW2 and SW3 fail for two genuinely different reasons -- one from get_interfaces(), one from get_interfaces_ip() -- not the same check twice.

Quick checkPredict output
Based on the verified output above, why does SW3 land in non_compliant for a different reason than SW2?

Option A vs. Option B

Same pattern as every prior capstone

Option A: swap check_device()'s driver_cls from get_network_driver("mock") to get_network_driver("ios") and point optional_args at a real device instead of a fixture path -- everything else (the desired-state checks, the report structure) stays identical. Never executed here -- no real device was available in this authoring environment, consistent with every module since Phase 2.6. Option B (used above): NAPALM's own mock driver, Lesson 6's real, verified mechanism -- not a hand-rolled simulation.

Quick checkScenario
What's the minimal, correct change to run this exact capstone against real switches instead of the mock driver?

What's next

One vendor's driver so far

Every driver actually exercised in this module was IOS-family (real execution) or the vendor-neutral mock driver. Multi-vendor Automation, the next planned module, is where Nornir's orchestration (Phase 3.4) and NAPALM's normalization (this module) combine through nornir_napalm -- running the exact getters this module taught, across many devices at once, the way Nornir already runs Netmiko tasks. Not implemented here.

Quick checkConcept check
What does Multi-vendor Automation (the next module) add that this module deliberately left out?

Key takeaways

  • This capstone combines every piece of the module: driver instantiation, get_facts()/get_interfaces()/get_interfaces_ip(), and the mock-driver testing strategy -- all verified together in one real, runnable script.
  • Deliberately not another VLAN check -- this capstone validates facts, interface state, and management IP addressing, exactly what NAPALM's normalized getters actually add.
  • Real, verified run: 1 compliant, 2 non-compliant (for two different reasons), 1 unreachable, 1 auth-failed across the 5-device fleet -- written to napalm_report.json with zero credentials on disk.
  • Option A (a real driver against a real device) and Option B (the mock driver, used here) differ only in driver_cls and optional_args -- the surrounding workflow is identical.
  • Next: Multi-vendor Automation, where Nornir + NAPALM combine through nornir_napalm -- not implemented in this module.