Skip to content

Build a DNS, DHCP, and IPAM Drift Report with Python

An IPAM drift report can highlight stale records before they cause conflicts or failed lookups. This tutorial compares read-only CSV exports from three sources; it does not connect to infrastructure or change DNS, DHCP, or IPAM data.

Use exports generated through your approved process, normalize each to the same columns, and protect the report because it contains internal network inventory.


01

Normalize Each Source to Address and Hostname Columns

Input Data

Create one CSV per source with address and hostname headers. Export in-scope records consistently and document whether reservations, leases, aliases, and retired IPAM entries are included.

address,hostname
10.20.40.15,app01.corp.example
10.20.40.16,app02.corp.example
❯ View Expected Console Output
ipam.csv
dns.csv
dhcp.csv
Terminal displaying a Python report comparing DNS, DHCP, and IPAM CSV exports and flagging mismatched records

Figure 1: Review proposed drift findings before updating any source of truth.


02

Compare Records with Python's Standard Library

Python Script

Save this as compare_ipam.py. It canonicalizes addresses and hostnames, reports cross-source mismatches, repeated CSV rows, and an address mapped to multiple hostnames within one source.

import csv
import ipaddress
from collections import Counter, defaultdict
from pathlib import Path
def load_records(path):
records = defaultdict(Counter)
with Path(path).open(newline="", encoding="utf-8-sig") as handle:
for row in csv.DictReader(handle):
address = str(ipaddress.ip_address(row["address"].strip()))
hostname = row["hostname"].strip().rstrip(".").casefold()
if not hostname:
raise ValueError(f"empty hostname in {path} for {address}")
records[address][hostname] += 1
return records
sources = {
"IPAM": load_records("ipam.csv"),
"DNS": load_records("dns.csv"),
"DHCP": load_records("dhcp.csv"),
}
addresses = set().union(*(set(records) for records in sources.values()))
print(f"{'Address':15} {'IPAM':24} {'DNS':24} DHCP")
def display(records):
return ",".join(
f"{hostname}(x{count})" if count > 1 else hostname
for hostname, count in sorted(records.items())
) or "-"
for address in sorted(addresses, key=ipaddress.ip_address):
values = {name: records.get(address, Counter()) for name, records in sources.items()}
hostname_sets = {name: set(items) for name, items in values.items()}
present = {tuple(sorted(items)) for items in hostname_sets.values() if items}
mismatched = len(present) > 1 or any(not items for items in hostname_sets.values())
duplicate_rows = any(
count > 1
for records in values.values()
for count in records.values()
)
multi_name_assignment = any(len(items) > 1 for items in hostname_sets.values())
if mismatched or duplicate_rows or multi_name_assignment:
print(
f"{address:15} "
f"{display(values['IPAM']):24} "
f"{display(values['DNS']):24} "
f"{display(values['DHCP'])}"
)
❯ View Expected Console Output
Address IPAM DNS DHCP
10.20.40.15 app01.corp.example app01.corp.example -
10.20.40.16 app02.corp.example old-app02.corp.example app02.corp.example

03

Compare the Three Read-Only Exports

Report

Run from the directory containing the three approved exports. The report includes addresses missing from a source, cross-source hostname differences, repeated rows marked with a count such as (x2), and addresses mapped to multiple names within one source.

Terminal window
python3 compare_ipam.py
❯ View Expected Console Output
Address IPAM DNS DHCP
10.20.40.15 app01.corp.example app01.corp.example -
10.20.40.16 app02.corp.example old-app02.corp.example app02.corp.example

04

Confirm Export Freshness and Record Ownership

Operational Follow-up

Check export times, lease state, DNS record type, aliases, and IPAM lifecycle status. Route confirmed mismatches to the source owner; update records only through the normal change process.

address | source exports | owner | verified finding | approved change reference
❯ View Expected Console Output
Static exports compared. No network queries or infrastructure changes were made.

Comments