NetAuto Academy

APIs and REST for Network Engineers

Capstone: An API-Driven Network Health Check

Intermediate18 min read

What you'll learn

  • How inventory, requests, JSON parsing, and validation combine into one health check
  • How HTTP status codes map onto the same classification lineage from earlier modules
  • How RESTCONF fits as a real network-specific example, without becoming a second curriculum
  • That REST is one more tool in this curriculum's lineage, not a replacement for the others
🎓 Teach This Lesson

Listen to a guided explanation of this lesson.

Spoken narration isn't supported in this browser.

The whole lineage, one more transport

  1. Phase 2.6 — Python automationhand-written inventory, connection, validation, report
  2. Phase 2.7 — Netmikoa real SSH connection
  3. Phase 2.8 — Reliable Automationretry, dry-run, pre/post-check
  4. Phase 2.9 — Jinja2 templatesstructured data + template replace hardcoded config
  5. Phase 3.0 — Ansiblethe same ideas, orchestrated declaratively
  6. Phase 3.1 — REST API automationthe same shape of task, over HTTP instead of SSH

Not a replacement — a different transport for a different target

This capstone solves the same kind of problem as every capstone before it: check each device's state, validate it, report the result. What's different is the transport — HTTP instead of SSH — because this capstone's fleet, like a real controller-managed environment, is addressed through an API. Nothing here suggests REST should replace Netmiko or Ansible for a device that's perfectly reachable over SSH.

Quick checkMyth check
Does this capstone suggest REST API automation should replace Netmiko and Ansible for devices reachable over SSH?

The complete script

api_health_check.py
import csv
import json
import requests

BASE_URL = "http://127.0.0.1:8765"
REQUEST_TIMEOUT = 2  # seconds

REQUIRED_VLANS = {
  "SW1": [{"id": 10, "name": "USERS"}, {"id": 20, "name": "VOICE"}],
  "SW2": [{"id": 10, "name": "USERS"}],
  "SW3": [{"id": 10, "name": "USERS"}, {"id": 30, "name": "MGMT"}],
  "SW4": [{"id": 10, "name": "USERS"}],
  "SW5": [{"id": 10, "name": "USERS"}],
}


def is_valid_device(device):
  """A device record needs at least a hostname to be usable."""
  return bool(device.get("hostname"))


def check_device(hostname):
  """GET this device's VLANs and classify the result -- HTTP outcome and
  business/data outcome are two separate questions, checked separately."""
  url = f"{BASE_URL}/devices/{hostname}/vlans"
  try:
      response = requests.get(url, timeout=REQUEST_TIMEOUT)
  except requests.exceptions.Timeout:
      return {"hostname": hostname, "status": "unreachable", "detail": "request timed out"}
  except requests.exceptions.ConnectionError:
      return {"hostname": hostname, "status": "unreachable", "detail": "connection failed"}

  if response.status_code == 401:
      return {"hostname": hostname, "status": "auth-failed", "detail": "authentication required"}
  if response.status_code == 404:
      return {"hostname": hostname, "status": "not-found", "detail": "device not found"}
  if response.status_code != 200:
      return {"hostname": hostname, "status": "error", "detail": f"unexpected status {response.status_code}"}

  # HTTP succeeded -- now check whether the DATA is actually correct.
  actual_vlans = response.json().get("vlans", [])
  required = REQUIRED_VLANS.get(hostname, [])
  missing = [v for v in required if v not in actual_vlans]

  if missing:
      return {
          "hostname": hostname,
          "status": "non-compliant",
          "detail": f"missing VLANs: {[v['id'] for v in missing]}",
      }
  return {"hostname": hostname, "status": "compliant", "detail": "all required VLANs present"}


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}")

results = [check_device(device["hostname"]) for device in valid_devices]

with open("api_health_check_report.json", "w") as f:
  json.dump(results, f, indent=4)

by_status = {}
for r in results:
  by_status[r["status"]] = by_status.get(r["status"], 0) + 1

print("\n--- Summary ---")
print(f"Checked: {len(results)} devices")
for status, count in by_status.items():
  print(f"{status}: {count}")
print("Report written to api_health_check_report.json")

Genuinely complete and runnable against this module's mock API (Option B) — the exact same is_valid_device()-then-loop-then-report shape as every capstone since Phase 2.6, with check_device() as the one new piece: a GET request, checked in two stages (HTTP status, then data).

Quick checkIdentify the correct code
Which part of check_device() is responsible for distinguishing 'the HTTP request succeeded' from 'the device actually has the required VLANs'?

The sample inventory

devices.csv
hostname,ip,platform
SW1,10.10.10.1,cisco_ios
SW2,10.10.10.2,cisco_ios
SW3,10.10.10.3,cisco_ios
SW4,10.10.10.4,cisco_ios
SW5,10.10.10.5,cisco_ios

Unchanged from every prior capstone — the same five devices, exercising a comparable spread of outcomes, now reached over HTTP.

Running it: expected terminal output

terminal
--- Summary ---
Checked: 5 devices
compliant: 2
non-compliant: 1
unreachable: 1
auth-failed: 1
Report written to api_health_check_report.json

Real, verified output. SW1 and SW3 are compliant. SW2 returns a healthy 200 but is missing a required VLAN. SW4's request times out. SW5 returns 401.

api_health_check_report.json
[
  {
      "hostname": "SW1",
      "status": "compliant",
      "detail": "all required VLANs present"
  },
  {
      "hostname": "SW2",
      "status": "non-compliant",
      "detail": "missing VLANs: [10]"
  },
  {
      "hostname": "SW3",
      "status": "compliant",
      "detail": "all required VLANs present"
  },
  {
      "hostname": "SW4",
      "status": "unreachable",
      "detail": "request timed out"
  },
  {
      "hostname": "SW5",
      "status": "auth-failed",
      "detail": "authentication required"
  }
]

Real, verified output — diffed exactly against a real run of the script above. Every device has one entry with enough detail to say what actually happened, the same reporting discipline as every JSON report since Phase 2.6.

Quick checkPredict output
SW2 returns HTTP 200, but is classified as non-compliant, not compliant. Why?

Two ways to run it

Option A — a real API or controller is available
  1. A real device with RESTCONF enabled, or a controller's APIsubject to availability
  2. Real credentials, real HTTPS
  3. A real api_health_check_report.json
Option B — no external API available
  1. Nothing beyond Python and requests needed
  2. This module's stdlib-only mock API
  3. The same shape of api_health_check_report.json

Option A — if RESTCONF is enabled on a real or virtual IOS-XE device, or a controller's API is available (own hardware, a personal lab, or a currently-available sandbox — subject to that resource's own availability, which this curriculum doesn't control), BASE_URL and check_device()'s URL pattern point at the real API instead. Option B — this module's mock API is a complete, legitimate way to finish this capstone, exercising every mechanic (GET, JSON parsing, status codes, timeouts, validation) for real.

RESTCONF, concretely — per the relevant RESTCONF documentation, not independently executed in this authoring environment

RESTCONF (RFC 8040) uses a standardized URL and JSON shape, built around YANG data models. A real request against an IOS-XE device's interfaces looks like this:

restconf_example.py — per RFC 8040 and Cisco RESTCONF documentation, not independently executed in this authoring environment
import requests

response = requests.get(
  "https://192.0.2.1/restconf/data/ietf-interfaces:interfaces",
  auth=("admin", "REPLACE_ME"),  # never hardcoded for real -- getpass()/env var
  headers={"Accept": "application/yang-data+json"},
  verify=False,  # many labs use self-signed certs -- never do this in production
  timeout=5,
)
print(response.json())

A real request was not made against a real device to produce this example -- no RESTCONF-capable device was available in this authoring environment. The URL shape (/restconf/data/<model>:<container>) and the Accept header are exactly as RFC 8040 and Cisco's own RESTCONF documentation specify.

expected response shape — per the ietf-interfaces YANG model, not independently executed in this authoring environment
{
  "ietf-interfaces:interfaces": {
      "interface": [
          {
              "name": "GigabitEthernet1",
              "type": "iana-if-type:ethernetCsmacd",
              "enabled": true
          }
      ]
  }
}

The response.json() parsing skill from Lesson 3 applies identically here -- a real RESTCONF response is still just JSON, structured around a standard YANG model instead of this module's own simplified shape.

Optional, and explicitly not required to complete this module

RESTCONF requires a real or virtual device with it enabled — never claimed as tested here, since none was available. Everything this module actually teaches (GET, JSON parsing, parameters, error/timeout handling, authentication, mutation safety) is fully exercised by the mock-API capstone above, with or without ever touching RESTCONF for real.

Quick checkConcept check
Is running a real RESTCONF request against a live device required to complete this module?

Three ways to ask the same question

CLI / Netmiko / Ansible
  1. SSH to the deviceor via Ansible's agentless model
  2. send_command() / ios_commandread current state
  3. Parse CLI text or trust the module
REST API (this module)
  1. HTTP request to the APIno SSH session
  2. GET a resource
  3. Parse JSON — already structured
Quick checkScenario
A team already has a working Ansible playbook that checks VLAN compliance over SSH for their Cisco IOS switches. Should they rewrite it to use this module's REST approach instead?

What this module actually built

Key takeaways

  • This capstone follows the exact inventory → check → classify → report lineage as every capstone since Phase 2.6 — the transport changed to HTTP, the shape of the problem didn't.
  • check_device() checks HTTP status and response data as two separate questions, in that order — SW2's non-compliant result (a healthy 200 with missing data) is the concrete proof this distinction matters.
  • RESTCONF (RFC 8040) is a real, standardized way this same model applies directly to network devices — shown concretely here, honestly labeled as not independently executed in this authoring environment, and never required to complete this module.
  • REST joins Netmiko, Ansible, and everything else in this curriculum's toolkit — it doesn't retire any of them. The right tool still follows what the target actually exposes.