NetAuto Academy

Beginner Project

Device Inventory & Fleet Health Report

Beginner55 min

Objective

Combine JSON parsing, multi-field inventory validation, a simulated per-device health check, and report design into one owned pipeline -- deciding the report's own structure rather than following one prescribed shape.

Builds on Networking Data & Formats and Your First Network Automation and Netmiko Fundamentals.

Prerequisites

  • Comfortable parsing JSON with Python's json module (Working with JSON in Python)
  • Familiar with validating structured records field-by-field (Building the Inventory Layer)
  • Familiar with a simulated connection function that returns state or raises an exception (Simulating Device Connections Safely)

Deliverables

  • A validated device inventory that separates usable records from rejected ones, each rejection with a specific reason
  • A simulated per-device health check that classifies every valid device as healthy, degraded, or unreachable
  • One structured fleet health report combining a fleet-wide summary, the rejected records, and full per-device detail
Local/offline only. The inventory and fleet health data below are fixed sample values, not a real fleet export or live device telemetry.

The problem

Not the same project as 'Your First Network Automation'

"Your First Network Automation" already built a three-stage pipeline: load inventory, simulate a connection, write a report. This project deliberately reuses that shape -- because it's a genuinely good shape -- but is not that lesson repeated. Three things are different here, on purpose: the inventory comes from JSON with more ways to be broken (not the lesson's clean three-row CSV), each device is checked against two independent health signals instead of one pass/fail pair, and the final report's structure is a decision you make, not a json.dump(results, f) handed to you.

A field team exported a small device inventory as JSON, but the export wasn't cleaned first -- one record is missing its management IP, another has a platform value that isn't real. Loading that straight into a health check would either crash on the first bad record or silently produce a report with holes nobody can explain. Your job: build a pipeline that validates the inventory, checks the health of every device that survives validation, and produces one report a stakeholder could actually read -- accounting for every input record, whether it was rejected, degraded, healthy, or unreachable.

Starter data

devices.json -- safe sample data
[
{"hostname": "RTR1", "ip_address": "10.20.10.11", "platform": "cisco_ios"},
{"hostname": "RTR2", "ip_address": "10.20.10.12", "platform": "cisco_ios"},
{"hostname": "SW1", "ip_address": "10.20.10.21", "platform": "arista_eos"},
{"hostname": "SW2", "ip_address": "", "platform": "arista_eos"},
{"hostname": "SW3", "ip_address": "10.20.10.23", "platform": "juniper_wrong"},
{"hostname": "SW4", "ip_address": "10.20.10.24", "platform": "cisco_ios"}
]

Fixed sample data written for this project. SW2 is missing its ip_address; SW3's platform value isn't a real one this fleet supports -- both deliberate, not typos to ignore.

Alongside the inventory, a second fixed lookup table stands in for what a real connection would discover -- the same seam simulate_connection() built in "Simulating Device Connections Safely," now carrying two independent health signals instead of one NTP/VLAN pair:

FLEET_HEALTH -- simulated per-device state
FLEET_HEALTH = {
  "RTR1": {"reachable": True, "cpu_utilization": 18, "interface_errors": 0},
  "RTR2": {"reachable": True, "cpu_utilization": 92, "interface_errors": 1},
  "SW1": {"reachable": False, "cpu_utilization": None, "interface_errors": None},
  "SW4": {"reachable": True, "cpu_utilization": 22, "interface_errors": 15},
}

Notice SW2 and SW3 have no entry at all -- they never reach the health-check stage, because they're rejected by inventory validation first.

Milestones

  1. Load raw inventoryparse the JSON string into records
  2. Validate and normalizerequired fields + platform allowlist, valid vs. rejected
  3. Simulate a per-device health checkreachable, or two independent health signals
  4. Classify healthhealthy, degraded (with specific reasons), or unreachable
  5. Aggregate the fleet summarycounts across every category, including rejected
  6. Produce the final reportone structured, saved report plus a short terminal summary

These milestones are guidance, not a checklist to submit anywhere -- build and test each stage before moving to the next, the same discipline "Building the Inventory Layer" argued for: get one stage right and trustworthy before the next stage depends on it.

Task

Write four functions:

  1. load_inventory(raw_json) -- parse raw_json and split the records into (valid, rejected). A record is valid only if hostname, ip_address, and platform are all present and non-empty, and platform (when present) is one of VALID_PLATFORMS. Each rejected record should keep the original record plus a list of specific reasons it failed -- a record can fail for more than one reason at once.
  2. check_device_health(device) -- look up device["hostname"] in FLEET_HEALTH. Raise ConnectionError if the hostname isn't in the table at all, or is present but reachable is False. Otherwise return its state dict.
  3. classify_health(state) -- given a reachable device's state, return (status, reasons). status is "degraded" if cpu_utilization is at or above CPU_THRESHOLD, or interface_errors is at or above ERROR_THRESHOLD -- collect a separate reason string for each one that's true, since a device can be degraded for either reason, or both. Otherwise return "healthy" with an empty reasons list.
  4. build_fleet_report(raw_json) -- combine the three functions above into one report. Every valid device needs exactly one entry in the report's device list: "unreachable" if check_device_health raises, otherwise its classified status. Decide the report's own top-level shape yourself -- the expected output below shows what a working solution produced, not a schema you must match field-for-field.
starter.py
import json

RAW_INVENTORY = """
[
{"hostname": "RTR1", "ip_address": "10.20.10.11", "platform": "cisco_ios"},
{"hostname": "RTR2", "ip_address": "10.20.10.12", "platform": "cisco_ios"},
{"hostname": "SW1", "ip_address": "10.20.10.21", "platform": "arista_eos"},
{"hostname": "SW2", "ip_address": "", "platform": "arista_eos"},
{"hostname": "SW3", "ip_address": "10.20.10.23", "platform": "juniper_wrong"},
{"hostname": "SW4", "ip_address": "10.20.10.24", "platform": "cisco_ios"}
]
"""

REQUIRED_FIELDS = ["hostname", "ip_address", "platform"]
VALID_PLATFORMS = {"cisco_ios", "arista_eos", "junos"}

FLEET_HEALTH = {
  "RTR1": {"reachable": True, "cpu_utilization": 18, "interface_errors": 0},
  "RTR2": {"reachable": True, "cpu_utilization": 92, "interface_errors": 1},
  "SW1": {"reachable": False, "cpu_utilization": None, "interface_errors": None},
  "SW4": {"reachable": True, "cpu_utilization": 22, "interface_errors": 15},
}

CPU_THRESHOLD = 90
ERROR_THRESHOLD = 10


def load_inventory(raw_json):
  """
  Parse raw_json into records, then split them into (valid, rejected).
  A record is valid only if hostname, ip_address, and platform are all
  present and non-empty, AND platform (when present) is one of
  VALID_PLATFORMS. Each rejected record should keep the original
  record plus a list of specific reasons it failed.
  """
  # Your code here.
  ...


def check_device_health(device):
  """
  Look up device["hostname"] in FLEET_HEALTH. Raise ConnectionError if
  the hostname isn't in the table, or is present but not reachable.
  Otherwise return its state dict.
  """
  # Your code here.
  ...


def classify_health(state):
  """
  Given a reachable device's state, return (status, reasons): status is
  "degraded" if cpu_utilization >= CPU_THRESHOLD or interface_errors >=
  ERROR_THRESHOLD (collect a reason string for each one that's true),
  otherwise "healthy" with an empty reasons list.
  """
  # Your code here.
  ...


def build_fleet_report(raw_json):
  """
  Combine load_inventory, check_device_health, and classify_health into
  one report. Every valid device needs exactly one entry -- "unreachable"
  if check_device_health raises, otherwise its classified status.
  """
  # Your code here.
  ...


if __name__ == "__main__":
  report = build_fleet_report(RAW_INVENTORY)
  print(report)

Copy into a local .py file and run with plain python3 -- json is standard library, no install needed. The four functions are intentionally incomplete.

Hints

Expected output

terminal output -- real, verified run
--- Fleet Health Report ---
Inventory records:  6
Rejected records:   2
Valid devices:      4
Healthy:            1
Degraded:           2
Unreachable:        1

RTR1: healthy
RTR2: degraded (CPU utilization 92% at or above 90%)
SW1: unreachable (device did not respond)
SW4: degraded (15 interface errors at or above 10)

Rejected SW2: missing ip_address
Rejected SW3: unknown platform 'juniper_wrong'

Full report written to fleet_health_report.json

Real, verified output from a working solution against the exact starter data above -- your own report's exact formatting can differ, but the counts and classifications must match.

fleet_health_report.json -- real, verified output
{
"summary": {
  "total_inventory_records": 6,
  "rejected_records": 2,
  "valid_devices": 4,
  "healthy": 1,
  "degraded": 2,
  "unreachable": 1
},
"rejected": [
  {
    "record": {"hostname": "SW2", "ip_address": "", "platform": "arista_eos"},
    "errors": ["missing ip_address"]
  },
  {
    "record": {"hostname": "SW3", "ip_address": "10.20.10.23", "platform": "juniper_wrong"},
    "errors": ["unknown platform 'juniper_wrong'"]
  }
],
"devices": [
  {"hostname": "RTR1", "ip_address": "10.20.10.11", "status": "healthy", "reasons": [], "cpu_utilization": 18, "interface_errors": 0},
  {"hostname": "RTR2", "ip_address": "10.20.10.12", "status": "degraded", "reasons": ["CPU utilization 92% at or above 90%"], "cpu_utilization": 92, "interface_errors": 1},
  {"hostname": "SW1", "ip_address": "10.20.10.21", "status": "unreachable", "reasons": ["device did not respond"]},
  {"hostname": "SW4", "ip_address": "10.20.10.24", "status": "degraded", "reasons": ["15 interface errors at or above 10"], "cpu_utilization": 22, "interface_errors": 15}
]
}

One reasonable report shape -- yours can be structured differently, as long as it accounts for all 6 input records and reaches the same summary counts.

Validation checklist

Self-check only -- nothing here is submitted or graded. Compare against your own output manually.

Solution

Solution

Try the task yourself first -- the full solution and explanation stay hidden until you click above.

Key takeaways

  • This project chains three stages built separately elsewhere in this curriculum -- JSON parsing, inventory validation, and a simulated connection -- into one pipeline none of them built alone.
  • A record can be rejected for more than one reason, and a device can be degraded for more than one reason -- collecting every applicable reason, not just the first one found, is what makes a report actually useful to read.
  • check_device_health() treats a hostname missing from FLEET_HEALTH and one explicitly marked unreachable the same way -- both mean the same thing to anything downstream: this device didn't respond.
  • Every one of the 6 starting records ends up somewhere in the final report -- as a rejected record or a classified device -- nothing is silently dropped at any stage.
  • This is fixed, offline sample data -- not a real fleet export or live device telemetry.

Related lessons

Done with this project?