Build an automated network discovery system that uses SNMP and LLDP protocols to map your infrastructure topology, detect device relationships, and create visual network diagrams with real-time monitoring integration.
Prerequisites
- Root access to server
- Network devices with SNMP enabled
- Python 3.8+
- Network connectivity to target devices
What this solves
Network topology discovery helps you automatically map your infrastructure by scanning devices, detecting their relationships through SNMP and LLDP protocols, and creating visual representations of your network. This approach eliminates manual network documentation, reduces configuration drift detection time, and provides real-time visibility into your infrastructure changes.
Step-by-step installation
Update system packages
Start by updating your package manager to ensure you have the latest security patches and package versions.
sudo apt update && sudo apt upgrade -y
sudo dnf update -y
Install SNMP tools and LLDP utilities
Install the core SNMP utilities, LLDP daemon, and network scanning tools needed for device discovery and protocol communication.
sudo apt install -y snmp snmp-mibs-downloader lldpd nmap python3-pip python3-venv git
sudo dnf install -y net-snmp net-snmp-utils lldpd nmap python3-pip python3-virtualenv git
Configure SNMP daemon
Set up the SNMP daemon with proper community strings and access controls for network device communication.
# Community string configuration
rocommunity public 127.0.0.1
rocommunity netdiscovery 10.0.0.0/8
rocommunity netdiscovery 172.16.0.0/12
rocommunity netdiscovery 192.168.0.0/16
# System information
sysLocation "Network Operations Center"
sysContact "admin@example.com"
sysServices 72
# Access control
com2sec readonly default netdiscovery
group MyROGroup v1 readonly
group MyROGroup v2c readonly
# OID access restrictions
view all included .1.3.6.1.2.1.1
view all included .1.3.6.1.2.1.2
view all included .1.3.6.1.2.1.4
view all included .1.3.6.1.4.1
access MyROGroup "" any noauth exact all none none
# Disable SNMP v1/v2c write access
rwcommunity private 127.0.0.1
Configure LLDP daemon
Enable LLDP on network interfaces to discover directly connected devices and their capabilities.
# Configure LLDP daemon
configure lldp tx-interval 30
configure lldp tx-hold 4
configure system hostname discovery-server
configure system description "Network Discovery Server"
configure system platform "Linux"
# Enable CDP compatibility for Cisco devices
configure lldp custom-tlv oui 00,00,0c subtype 1
# Interface configuration
configure ports eth0 lldp portidsubtype local eth0
configure med fast-start enable
Enable and start services
Start the SNMP and LLDP daemons and enable them to start automatically on boot.
sudo systemctl enable --now snmpd
sudo systemctl enable --now lldpd
sudo systemctl status snmpd lldpd
Create Python virtual environment
Set up an isolated Python environment for the network discovery scripts and required libraries.
sudo mkdir -p /opt/network-discovery
sudo chown $(whoami):$(whoami) /opt/network-discovery
cd /opt/network-discovery
python3 -m venv venv
source venv/bin/activate
pip install pysnmp pysnmp-mibs netaddr netifaces graphviz networkx matplotlib
Create network scanner script
Build the core network discovery script that performs SNMP walks and LLDP neighbor detection across your network ranges.
#!/usr/bin/env python3
import json
import subprocess
import socket
from ipaddress import IPv4Network, AddressValueError
from pysnmp.hlapi import *
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class NetworkScanner:
def __init__(self, community='public', timeout=5, retries=1):
self.community = community
self.timeout = timeout
self.retries = retries
self.discovered_devices = {}
def snmp_get(self, target, oid):
"""Perform SNMP GET operation"""
try:
for (errorIndication, errorStatus, errorIndex, varBinds) in nextCmd(
SnmpEngine(),
CommunityData(self.community),
UdpTransportTarget((target, 161), timeout=self.timeout, retries=self.retries),
ContextData(),
ObjectType(ObjectIdentity(oid)),
lexicographicMode=False,
maxRows=50):
if errorIndication:
break
elif errorStatus:
break
else:
return [str(varBind[1]) for varBind in varBinds]
return []
except Exception as e:
logger.debug(f"SNMP error for {target}: {e}")
return []
def get_device_info(self, ip):
"""Extract device information via SNMP"""
device_info = {
'ip': ip,
'hostname': '',
'system_desc': '',
'uptime': '',
'interfaces': [],
'neighbors': [],
'vendor': '',
'model': ''
}
# System information OIDs
hostname_result = self.snmp_get(ip, '1.3.6.1.2.1.1.5.0') # sysName
if hostname_result:
device_info['hostname'] = hostname_result[0]
desc_result = self.snmp_get(ip, '1.3.6.1.2.1.1.1.0') # sysDescr
if desc_result:
device_info['system_desc'] = desc_result[0]
# Parse vendor info from description
desc_lower = desc_result[0].lower()
if 'cisco' in desc_lower:
device_info['vendor'] = 'Cisco'
elif 'juniper' in desc_lower:
device_info['vendor'] = 'Juniper'
elif 'hp' in desc_lower or 'hewlett' in desc_lower:
device_info['vendor'] = 'HP'
elif 'dell' in desc_lower:
device_info['vendor'] = 'Dell'
uptime_result = self.snmp_get(ip, '1.3.6.1.2.1.1.3.0') # sysUpTime
if uptime_result:
device_info['uptime'] = uptime_result[0]
# Interface information
interfaces = self.snmp_get(ip, '1.3.6.1.2.1.2.2.1.2') # ifDescr
if interfaces:
device_info['interfaces'] = interfaces[:10] # Limit to first 10
# LLDP neighbors (if supported)
lldp_neighbors = self.snmp_get(ip, '1.0.8802.1.1.2.1.4.1.1.9') # lldpRemSysName
if lldp_neighbors:
device_info['neighbors'] = lldp_neighbors
return device_info
def scan_network_range(self, network_range, max_workers=50):
"""Scan a network range for SNMP-enabled devices"""
logger.info(f"Scanning network range: {network_range}")
try:
network = IPv4Network(network_range, strict=False)
except AddressValueError:
logger.error(f"Invalid network range: {network_range}")
return
# Skip network and broadcast addresses for /24 and smaller
if network.prefixlen >= 24:
hosts = list(network.hosts())
else:
hosts = list(network)
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_ip = {executor.submit(self.check_snmp_device, str(ip)): ip for ip in hosts}
for future in as_completed(future_to_ip, timeout=300):
ip = future_to_ip[future]
try:
result = future.result()
if result:
self.discovered_devices[str(ip)] = result
logger.info(f"Discovered device: {result['hostname']} ({ip})")
except Exception as e:
logger.debug(f"Error scanning {ip}: {e}")
def check_snmp_device(self, ip):
"""Check if device responds to SNMP and gather info"""
# Quick port check first
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(2)
sock.connect((ip, 161))
sock.close()
except:
return None
# Try SNMP
device_info = self.get_device_info(ip)
if device_info['hostname'] or device_info['system_desc']:
return device_info
return None
def get_local_lldp_neighbors(self):
"""Get LLDP neighbors from local daemon"""
try:
result = subprocess.run(['lldpctl', '-f', 'json'],
capture_output=True, text=True, timeout=10)
if result.returncode == 0:
return json.loads(result.stdout)
except Exception as e:
logger.error(f"Error getting LLDP neighbors: {e}")
return {}
def generate_topology_map(self):
"""Generate network topology relationships"""
topology = {
'devices': self.discovered_devices,
'connections': [],
'generated_at': time.strftime('%Y-%m-%d %H:%M:%S')
}
# Add LLDP-discovered connections
for device_ip, device_info in self.discovered_devices.items():
for neighbor in device_info.get('neighbors', []):
# Find neighbor device by hostname
for neighbor_ip, neighbor_info in self.discovered_devices.items():
if neighbor_info.get('hostname', '').lower() == neighbor.lower():
connection = {
'source': device_ip,
'target': neighbor_ip,
'type': 'lldp',
'source_name': device_info.get('hostname', device_ip),
'target_name': neighbor_info.get('hostname', neighbor_ip)
}
if connection not in topology['connections']:
topology['connections'].append(connection)
return topology
def save_results(self, filename='network_topology.json'):
"""Save discovery results to JSON file"""
topology = self.generate_topology_map()
with open(filename, 'w') as f:
json.dump(topology, f, indent=2)
logger.info(f"Results saved to {filename}")
return topology
if __name__ == "__main__":
scanner = NetworkScanner(community='netdiscovery')
# Define networks to scan
networks = [
'192.168.1.0/24',
'10.0.1.0/24'
]
for network in networks:
scanner.scan_network_range(network)
# Save results
topology = scanner.save_results('/opt/network-discovery/topology.json')
print(f"Discovered {len(topology['devices'])} devices")
print(f"Found {len(topology['connections'])} connections")
Create topology visualizer
Build a script that creates visual network diagrams from the discovered topology data using NetworkX and Matplotlib.
#!/usr/bin/env python3
import json
import networkx as nx
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from datetime import datetime
import argparse
class TopologyVisualizer:
def __init__(self):
self.graph = nx.Graph()
self.vendor_colors = {
'Cisco': '#1BA0D7',
'Juniper': '#84BD00',
'HP': '#0096D6',
'Dell': '#007DB8',
'Unknown': '#666666'
}
def load_topology(self, filename):
"""Load topology data from JSON file"""
try:
with open(filename, 'r') as f:
return json.load(f)
except FileNotFoundError:
print(f"Error: Topology file {filename} not found")
return None
except json.JSONDecodeError:
print(f"Error: Invalid JSON in {filename}")
return None
def build_graph(self, topology_data):
"""Build NetworkX graph from topology data"""
devices = topology_data.get('devices', {})
connections = topology_data.get('connections', [])
# Add nodes
for ip, device_info in devices.items():
hostname = device_info.get('hostname', ip)
vendor = device_info.get('vendor', 'Unknown')
interfaces = len(device_info.get('interfaces', []))
self.graph.add_node(ip,
hostname=hostname,
vendor=vendor,
interfaces=interfaces,
system_desc=device_info.get('system_desc', '')[:50])
# Add edges
for connection in connections:
source = connection['source']
target = connection['target']
if source in self.graph and target in self.graph:
self.graph.add_edge(source, target,
connection_type=connection.get('type', 'unknown'))
def create_visualization(self, output_file='network_topology.png', layout='spring'):
"""Create network topology visualization"""
if len(self.graph.nodes()) == 0:
print("No devices to visualize")
return
# Set up the plot
plt.figure(figsize=(16, 12))
ax = plt.gca()
# Choose layout algorithm
if layout == 'spring':
pos = nx.spring_layout(self.graph, k=3, iterations=50)
elif layout == 'circular':
pos = nx.circular_layout(self.graph)
elif layout == 'kamada_kawai':
pos = nx.kamada_kawai_layout(self.graph)
else:
pos = nx.spring_layout(self.graph)
# Draw edges
nx.draw_networkx_edges(self.graph, pos,
edge_color='#CCCCCC',
width=2,
alpha=0.7)
# Draw nodes by vendor
for vendor, color in self.vendor_colors.items():
vendor_nodes = [n for n, d in self.graph.nodes(data=True)
if d.get('vendor', 'Unknown') == vendor]
if vendor_nodes:
nx.draw_networkx_nodes(self.graph, pos,
nodelist=vendor_nodes,
node_color=color,
node_size=800,
alpha=0.8)
# Add labels
labels = {}
for node, data in self.graph.nodes(data=True):
hostname = data.get('hostname', node)
if len(hostname) > 15:
hostname = hostname[:12] + '...'
labels[node] = f"{hostname}\n({node})"
nx.draw_networkx_labels(self.graph, pos, labels, font_size=8, font_weight='bold')
# Create legend
legend_elements = []
for vendor, color in self.vendor_colors.items():
vendor_nodes = [n for n, d in self.graph.nodes(data=True)
if d.get('vendor', 'Unknown') == vendor]
if vendor_nodes:
legend_elements.append(patches.Patch(color=color, label=f'{vendor} ({len(vendor_nodes)})'))
plt.legend(handles=legend_elements, loc='upper left', bbox_to_anchor=(0, 1))
# Add title and info
plt.title(f"Network Topology Discovery\nGenerated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
f"Devices: {len(self.graph.nodes())} | Connections: {len(self.graph.edges())}",
fontsize=14, pad=20)
# Remove axis
ax.set_axis_off()
# Save the plot
plt.tight_layout()
plt.savefig(output_file, dpi=300, bbox_inches='tight',
facecolor='white', edgecolor='none')
plt.close()
print(f"Network topology visualization saved to {output_file}")
def generate_report(self, topology_data, output_file='network_report.txt'):
"""Generate text report of discovered network"""
devices = topology_data.get('devices', {})
connections = topology_data.get('connections', [])
report = []
report.append("NETWORK TOPOLOGY DISCOVERY REPORT")
report.append("=" * 50)
report.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
report.append(f"Total Devices: {len(devices)}")
report.append(f"Total Connections: {len(connections)}")
report.append("")
# Device summary by vendor
vendor_count = {}
for device_info in devices.values():
vendor = device_info.get('vendor', 'Unknown')
vendor_count[vendor] = vendor_count.get(vendor, 0) + 1
report.append("DEVICES BY VENDOR:")
report.append("-" * 20)
for vendor, count in sorted(vendor_count.items()):
report.append(f"{vendor}: {count}")
report.append("")
# Detailed Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m' # No Color
# Default configuration
COMMUNITY_STRING="${1:-netdiscovery}"
CONTACT_EMAIL="${2:-admin@example.com}"
LOCATION="${3:-Network Operations Center}"
# Print usage if help requested
if [[ "${1:-}" == "-h" ]] || [[ "${1:-}" == "--help" ]]; then
echo "Usage: $0 [COMMUNITY_STRING] [CONTACT_EMAIL] [LOCATION]"
echo "Example: $0 mynetwork admin@company.com 'Data Center'"
exit 0
fi
# Logging functions
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Cleanup on error
cleanup() {
log_error "Installation failed. Cleaning up..."
if [[ -d /opt/network-discovery ]]; then
rm -rf /opt/network-discovery
fi
systemctl stop snmpd lldpd 2>/dev/null || true
}
trap cleanup ERR
# Check prerequisites
check_prerequisites() {
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root"
exit 1
fi
if ! command -v systemctl &> /dev/null; then
log_error "systemd is required"
exit 1
fi
}
# Auto-detect distribution
detect_distro() {
if [[ ! -f /etc/os-release ]]; then
log_error "Cannot detect distribution - /etc/os-release not found"
exit 1
fi
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update && apt upgrade -y"
SNMP_PACKAGES="snmp snmp-mibs-downloader lldpd nmap python3-pip python3-venv git"
SNMP_CONFIG="/etc/snmp/snmpd.conf"
LLDP_CONFIG="/etc/lldpd.d/01-custom.conf"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf update -y"
SNMP_PACKAGES="net-snmp net-snmp-utils lldpd nmap python3-pip python3-virtualenv git"
SNMP_CONFIG="/etc/snmp/snmpd.conf"
LLDP_CONFIG="/etc/lldpd.d/01-custom.conf"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum update -y"
SNMP_PACKAGES="net-snmp net-snmp-utils lldpd nmap python3-pip python3-virtualenv git"
SNMP_CONFIG="/etc/snmp/snmpd.conf"
LLDP_CONFIG="/etc/lldpd.d/01-custom.conf"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
log_info "Detected distribution: $PRETTY_NAME"
}
main() {
log_info "Starting network topology discovery setup..."
echo "[1/8] Checking prerequisites..."
check_prerequisites
detect_distro
echo "[2/8] Updating system packages..."
eval "$PKG_UPDATE"
echo "[3/8] Installing SNMP and LLDP packages..."
eval "$PKG_INSTALL $SNMP_PACKAGES"
echo "[4/8] Configuring SNMP daemon..."
mkdir -p "$(dirname "$SNMP_CONFIG")"
cat > "$SNMP_CONFIG" << EOF
# Community string configuration
rocommunity public 127.0.0.1
rocommunity $COMMUNITY_STRING 10.0.0.0/8
rocommunity $COMMUNITY_STRING 172.16.0.0/12
rocommunity $COMMUNITY_STRING 192.168.0.0/16
# System information
sysLocation "$LOCATION"
sysContact "$CONTACT_EMAIL"
sysServices 72
# Access control
com2sec readonly default $COMMUNITY_STRING
group MyROGroup v1 readonly
group MyROGroup v2c readonly
# OID access restrictions
view all included .1.3.6.1.2.1.1
view all included .1.3.6.1.2.1.2
view all included .1.3.6.1.2.1.4
view all included .1.3.6.1.4.1
access MyROGroup "" any noauth exact all none none
# Disable SNMP v1/v2c write access
rwcommunity private 127.0.0.1
EOF
chmod 644 "$SNMP_CONFIG"
echo "[5/8] Configuring LLDP daemon..."
mkdir -p "$(dirname "$LLDP_CONFIG")"
cat > "$LLDP_CONFIG" << EOF
# Configure LLDP daemon
configure lldp tx-interval 30
configure lldp tx-hold 4
configure system hostname discovery-server
configure system description "Network Discovery Server"
configure system platform "Linux"
# Enable CDP compatibility for Cisco devices
configure lldp custom-tlv oui 00,00,0c subtype 1
# Interface configuration for primary interface
configure med fast-start enable
EOF
chmod 644 "$LLDP_CONFIG"
echo "[6/8] Creating Python virtual environment..."
mkdir -p /opt/network-discovery
cd /opt/network-discovery
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install pysnmp pysnmp-mibs netaddr netifaces graphviz networkx matplotlib
echo "[7/8] Creating network scanner script..."
cat > /opt/network-discovery/network_scanner.py << 'EOF'
#!/usr/bin/env python3
import json
import subprocess
import socket
from ipaddress import IPv4Network, AddressValueError
from pysnmp.hlapi import *
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class NetworkScanner:
def __init__(self, community='netdiscovery', timeout=5, retries=1):
self.community = community
self.timeout = timeout
self.retries = retries
self.discovered_devices = {}
def snmp_get(self, target, oid):
"""Perform SNMP GET operation"""
try:
iterator = getCmd(
SnmpEngine(),
CommunityData(self.community),
UdpTransportTarget((target, 161), timeout=self.timeout, retries=self.retries),
ContextData(),
ObjectType(ObjectIdentity(oid))
)
errorIndication, errorStatus, errorIndex, varBinds = next(iterator)
if errorIndication or errorStatus:
return None
return str(varBinds[0][1])
except Exception:
return None
def scan_device(self, ip):
"""Scan a single device for SNMP information"""
device_info = {'ip': ip, 'reachable': False}
# Check if device responds to SNMP
sysname = self.snmp_get(ip, '1.3.6.1.2.1.1.5.0')
if sysname:
device_info['reachable'] = True
device_info['hostname'] = sysname
device_info['sysdesc'] = self.snmp_get(ip, '1.3.6.1.2.1.1.1.0')
device_info['uptime'] = self.snmp_get(ip, '1.3.6.1.2.1.1.3.0')
return device_info
def scan_network(self, network_range):
"""Scan network range for SNMP-enabled devices"""
try:
network = IPv4Network(network_range, strict=False)
except AddressValueError:
logger.error(f"Invalid network range: {network_range}")
return
logger.info(f"Scanning network range: {network}")
with ThreadPoolExecutor(max_workers=20) as executor:
futures = {executor.submit(self.scan_device, str(ip)): ip for ip in network.hosts()}
for future in as_completed(futures):
result = future.result()
if result['reachable']:
self.discovered_devices[result['ip']] = result
logger.info(f"Found device: {result['ip']} - {result.get('hostname', 'Unknown')}")
def save_results(self, filename='network_topology.json'):
"""Save discovery results to JSON file"""
with open(filename, 'w') as f:
json.dump(self.discovered_devices, f, indent=2)
logger.info(f"Results saved to {filename}")
if __name__ == '__main__':
scanner = NetworkScanner()
# Scan common private network ranges
scanner.scan_network('192.168.1.0/24')
scanner.scan_network('10.0.0.0/24')
scanner.save_results()
EOF
chmod 755 /opt/network-discovery/network_scanner.py
chown -R root:root /opt/network-discovery
# Create systemd service file
cat > /etc/systemd/system/network-discovery.service << EOF
[Unit]
Description=Network Discovery Scanner
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=root
WorkingDirectory=/opt/network-discovery
Environment=PATH=/opt/network-discovery/venv/bin
ExecStart=/opt/network-discovery/venv/bin/python /opt/network-discovery/network_scanner.py
[Install]
WantedBy=multi-user.target
EOF
chmod 644 /etc/systemd/system/network-discovery.service
systemctl daemon-reload
echo "[8/8] Starting and enabling services..."
systemctl enable --now snmpd
systemctl enable --now lldpd
# Configure firewall if active
if systemctl is-active --quiet firewalld; then
firewall-cmd --permanent --add-port=161/udp --add-port=162/udp
firewall-cmd --reload
log_info "Configured firewalld for SNMP"
elif systemctl is-active --quiet ufw; then
ufw allow 161/udp
ufw allow 162/udp
log_info "Configured UFW for SNMP"
fi
# Verification
log_info "Verifying installation..."
if systemctl is-active --quiet snmpd && systemctl is-active --quiet lldpd; then
log_info "✓ SNMP and LLDP services are running"
else
log_error "✗ Services failed to start"
exit 1
fi
if [[ -x /opt/network-discovery/network_scanner.py ]]; then
log_info "✓ Network scanner script installed"
else
log_error "✗ Network scanner script missing"
exit 1
fi
log_info "Network topology discovery setup completed successfully!"
log_info "Usage: cd /opt/network-discovery && source venv/bin/activate && python network_scanner.py"
log_info "Or run: systemctl start network-discovery"
}
main "$@"
Review the script before running. Execute with: bash install.sh