Beginner Project
Device Inventory & Fleet Health Report
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
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
[
{"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 = {
"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
- Load raw inventoryparse the JSON string into records
- Validate and normalizerequired fields + platform allowlist, valid vs. rejected
- Simulate a per-device health checkreachable, or two independent health signals
- Classify healthhealthy, degraded (with specific reasons), or unreachable
- Aggregate the fleet summarycounts across every category, including rejected
- 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:
load_inventory(raw_json)-- parseraw_jsonand split the records into(valid, rejected). A record is valid only ifhostname,ip_address, andplatformare all present and non-empty, andplatform(when present) is one ofVALID_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.check_device_health(device)-- look updevice["hostname"]inFLEET_HEALTH. RaiseConnectionErrorif the hostname isn't in the table at all, or is present butreachableisFalse. Otherwise return its state dict.classify_health(state)-- given a reachable device's state, return(status, reasons).statusis"degraded"ifcpu_utilizationis at or aboveCPU_THRESHOLD, orinterface_errorsis at or aboveERROR_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.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"ifcheck_device_healthraises, 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.
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.
Expected output
--- 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.jsonReal, 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.
{
"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.
Self-check only -- nothing here is submitted or graded. Compare against your own output manually.
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
Working with JSON in Python
json.loads() is exactly the tool this project's inventory-loading stage builds on.
Read lessonBuilding the Inventory Layer
The required-field-plus-allowlist validation pattern this project's rejected-record handling extends.
Read lessonSimulating Device Connections Safely
The exact simulated-connection shape (return state, or raise ConnectionError) this project's health check reuses.
Read lessonDone with this project?