Network Scanner Guide¶
Discover devices on your network automatically — no manual inventory required to get started.
What It Does¶
The scanner (netops.inventory.scan) discovers devices in a subnet by:
- Ping sweep — sends ICMP pings to every address in a CIDR range; only reachable hosts are probed further.
- SNMP identification — queries the MIB-II system group (RFC 1213) on each live host to read hostname, description, sysObjectID, and location.
- Vendor detection — maps
sysDescr/sysObjectIDto a Netmiko-compatible vendor string automatically. - CDP discovery (Cisco) — walks the Cisco CDP MIB to find directly connected neighbors.
- LLDP discovery (IEEE 802.1AB) — walks the LLDP-MIB to find neighbors on any 802.1AB-capable device.
The result is an inventory fragment you can write to a new file or merge into an existing one.
Prerequisites¶
Install SNMP support¶
The scanner requires pysnmp >= 7.0. Install the optional dependency group:
The ping sweep works without any extra packages. Use --skip-snmp if you only want to find live hosts.
Network requirements¶
| Feature | Port | Protocol |
|---|---|---|
| Ping sweep | — | ICMP |
| SNMP identification | 161 (UDP) | SNMPv2c |
| CDP / LLDP discovery | 161 (UDP) | SNMPv2c |
The scanner only reads — it never writes to devices.
Quick Start¶
Discover a /24 subnet¶
This prints a JSON inventory fragment to stdout:
{
"devices": {
"core-rtr-01": {
"host": "10.0.0.1",
"vendor": "cisco_ios",
"site": "Main DC, Row 3",
"tags": {
"sys_descr": "Cisco IOS Software, Version 15.7(3)M...",
"neighbors": "cdp:dist-sw-01,cdp:dist-sw-02"
}
},
"dist-sw-01": {
"host": "10.0.0.2",
"vendor": "cisco_xe",
"tags": {
"sys_descr": "Cisco IOS Software [Everest], Catalyst L3...",
"neighbors": "cdp:core-rtr-01,lldp:access-sw-03"
}
}
}
}
Save to a file¶
Merge into an existing inventory¶
New devices are added. Existing entries are only updated where the current value is empty or "unknown" — your manually-set values are never overwritten.
Step-by-Step: First Scan¶
Step 1: Decide your scan range¶
Pick a CIDR range that covers your management network. Common examples:
| Scope | Example |
|---|---|
| Small office (/24, up to 254 hosts) | 10.0.0.0/24 |
| Campus block (/22, up to 1,022 hosts) | 172.16.0.0/22 |
| Single device (/32) | 192.168.1.1/32 |
| Specific range | Use one CIDR per run |
Step 2: Run a quick ping-only scan first¶
Output summary (stderr):
This tells you how many devices are pingable before attempting SNMP.
Step 3: Run a full scan with your SNMP community¶
python -m netops.inventory.scan \
--subnet 10.0.0.0/24 \
--community mysecretcommunity \
--output scan-fragment.json
Step 4: Review the fragment¶
Check that vendors were detected correctly. Devices that couldn't be identified show "vendor": "unknown".
Step 5: Merge into your inventory¶
If you already have an inventory:
python -m netops.inventory.scan \
--subnet 10.0.0.0/24 \
--community mysecretcommunity \
--merge my-inventory.yaml
Or start a fresh one from the fragment:
Configuration: All CLI Options¶
| Option | Default | What It Does |
|---|---|---|
--subnet |
one target source required | Subnet in CIDR notation, e.g. 10.0.0.0/24 |
--csv |
— | CSV source with an ip, host, hostname, or address column |
--hosts-file |
— | One IP/hostname per line; CSV is also accepted |
--community |
public |
SNMPv2c community string |
--snmp-port |
161 |
SNMP UDP port |
--snmp-timeout |
2 |
Per-host SNMP timeout in seconds |
--ping-workers |
50 |
Thread pool size for concurrent pings |
--snmp-concurrency |
10 |
Max simultaneous SNMP sessions |
--output / -o |
— | Write an inventory fragment to this file |
--format |
json |
Output format: json or csv |
--merge / -m |
— | Merge scan results into an existing inventory file |
--skip-ping |
false | Skip ping sweep — probe every address in the subnet |
--skip-snmp |
false | Skip SNMP — perform a ping sweep only |
--verbose / -v |
false | Enable debug logging |
--user / --password / --password-stdin |
— | Enable deep SSH enrichment after discovery |
--event-stream |
false | Emit JSON Lines to stdout; cannot be combined with --output or --merge |
Tuning for large subnets¶
For a /16 (up to 65,534 hosts), increase concurrency:
python -m netops.inventory.scan \
--subnet 10.0.0.0/16 \
--ping-workers 200 \
--snmp-concurrency 30 \
--community public
Note: Higher concurrency puts more load on your network and management plane. Start with defaults and increase gradually.
Scanning without ping¶
Some firewalls block ICMP but allow SNMP. Use --skip-ping to probe all addresses directly:
python -m netops.inventory.scan \
--subnet 10.0.0.0/24 \
--skip-ping \
--community mysecretcommunity
Warning: Scanning all addresses in a /16 without ping takes significantly longer.
Using the Python API¶
from netops.inventory.scan import (
scan_subnet,
results_to_inventory_fragment,
merge_inventory,
identify_vendor,
ping_sweep,
)
ping_sweep(subnet, max_workers=50, timeout=1)¶
Returns a sorted list of reachable IP address strings for the given CIDR subnet.
from netops.inventory.scan import ping_sweep
live_hosts = ping_sweep("10.0.0.0/24", max_workers=100, timeout=2)
print(live_hosts)
# ['10.0.0.1', '10.0.0.2', '10.0.0.5']
scan_subnet(subnet, community="public", ...)¶
Full scan: ping sweep → SNMP identification → CDP/LLDP topology. Returns a list of ScanResult objects.
from netops.inventory.scan import scan_subnet
results = scan_subnet(
subnet="10.0.0.0/24",
community="mysecretcommunity",
snmp_timeout=3,
ping_workers=100,
snmp_concurrency=20,
)
for r in results:
print(r.host, r.vendor, r.hostname)
ScanResult attributes¶
| Attribute | Type | Description |
|---|---|---|
host |
str |
IP address |
reachable |
bool |
Responded to ping (or skip_ping was set) |
hostname |
Optional[str] |
sysName from SNMP (domain stripped) |
sys_descr |
Optional[str] |
sysDescr — full platform description |
sys_obj_id |
Optional[str] |
sysObjectID — enterprise OID |
vendor |
Optional[str] |
Netmiko vendor string, e.g. cisco_ios |
location |
Optional[str] |
sysLocation from SNMP |
cdp_neighbors |
list[dict] |
List of CDP neighbor dicts (keys: device_id, platform, address, protocol) |
lldp_neighbors |
list[dict] |
List of LLDP neighbor dicts (keys: sys_name, sys_desc, chassis_id, protocol) |
error |
Optional[str] |
Error message if SNMP failed |
ScanResult.to_inventory_entry()¶
Converts a single result to an inventory device dict compatible with netops.core.Inventory:
entry = result.to_inventory_entry()
# {'host': '10.0.0.1', 'vendor': 'cisco_ios', 'site': 'Main DC', 'tags': {'sys_descr': '...'}}
identify_vendor(sys_descr, sys_obj_id="")¶
Map a raw sysDescr string (and optionally sysObjectID) to a Netmiko vendor string. Useful when you already have SNMP data and just need the vendor mapping.
from netops.inventory.scan import identify_vendor
vendor = identify_vendor("Cisco IOS Software, Version 15.7(3)M")
# 'cisco_ios'
vendor = identify_vendor("IOS XE Software, Catalyst, Version 17.06.01")
# 'cisco_xe'
vendor = identify_vendor("", ".1.3.6.1.4.1.6527.1.3.2")
# 'nokia_sros'
results_to_inventory_fragment(results)¶
Convert a list of ScanResult objects to an inventory fragment dict:
from netops.inventory.scan import scan_subnet, results_to_inventory_fragment
results = scan_subnet("10.0.0.0/24")
fragment = results_to_inventory_fragment(results)
# {'devices': {'core-rtr-01': {...}, 'dist-sw-01': {...}}}
Neighbor information is encoded in the neighbors tag as a comma-separated string of cdp:<device_id> and lldp:<sys_name> entries.
merge_inventory(existing_path, fragment)¶
Merge a fragment into an existing YAML or JSON inventory. Returns the merged dict. Supports .yaml, .yml, and .json files.
from netops.inventory.scan import scan_subnet, results_to_inventory_fragment, merge_inventory
import json
from pathlib import Path
results = scan_subnet("10.0.0.0/24", community="mycommunity")
fragment = results_to_inventory_fragment(results)
merged = merge_inventory("my-inventory.yaml", fragment)
# Save the merged result
import yaml
Path("my-inventory.yaml").write_text(yaml.dump(merged, default_flow_style=False))
Merge semantics:
- New devices are added as-is.
- For existing devices, a field is only updated if its current value is None, "", or "unknown".
- Tag dicts are merged at the sub-key level with the same rules.
- Manually set values are never overwritten.
Output Format¶
The scanner produces an inventory fragment — a JSON/YAML dict with a "devices" key:
{
"devices": {
"core-rtr-01": {
"host": "10.0.0.1",
"vendor": "cisco_ios",
"site": "Main DC, Rack 3",
"tags": {
"sys_descr": "Cisco IOS Software, Version 15.7(3)M, RELEASE SOFTWARE",
"neighbors": "cdp:dist-sw-01,cdp:dist-sw-02,lldp:fw-01"
}
},
"10.0.0.7": {
"host": "10.0.0.7",
"vendor": "unknown",
"tags": {}
}
}
}
Notes:
- The device key is sysName (with domain stripped) when SNMP is successful, otherwise the IP address.
- site comes from sysLocation and is omitted when empty.
- vendor: "unknown" means the host was reachable but SNMP failed or sysDescr didn't match any known pattern.
Supported Vendors¶
The scanner detects the following platforms automatically via sysDescr pattern matching and sysObjectID enterprise prefix fallback:
| Vendor | Detected String | Vendor Code |
|---|---|---|
| Cisco IOS | "Cisco IOS Software" or OID .1.3.6.1.4.1.9. |
cisco_ios |
| Cisco IOS-XE | "IOS XE" or "IOS-XE" |
cisco_xe |
| Cisco IOS-XR | "IOS XR" |
cisco_xr |
| Cisco NX-OS | "NX-OS" or "NXOS" |
cisco_nxos |
| Nokia SR OS | "Nokia" or "TiMOS", OID .1.3.6.1.4.1.6527. |
nokia_sros |
| Nokia SR Linux | "Nokia" + "SRL" |
nokia_srl |
| Juniper JunOS | "Juniper" or "Junos", OID .1.3.6.1.4.1.2636. |
juniper_junos |
| Arista EOS | "Arista", OID .1.3.6.1.4.1.30065. |
arista_eos |
| Brocade FastIron | "Brocade" / "Foundry" / "FastIron", OID .1.3.6.1.4.1.1991. |
brocade_fastiron |
| Brocade NOS | "Brocade Network OS", OID .1.3.6.1.4.1.1588. |
brocade_nos |
| Unknown | No match | unknown |
Devices detected as unknown are still included in the fragment — add them to your inventory manually and set the vendor.
Troubleshooting¶
| Problem | Likely Cause | Fix |
|---|---|---|
ImportError: pysnmp is required |
SNMP extra not installed | pip install 'netops-toolkit[snmp]' |
0 reachable from ping sweep |
ICMP blocked | Use --skip-ping to probe all addresses directly |
All vendors show "unknown" |
Wrong community string | Check with snmpwalk -c <community> -v2c <host> sysDescr.0 |
0 identified via SNMP |
SNMP not enabled on devices | Enable SNMPv2c on your devices; check ACLs |
| Scanner hangs on large /16 | Too many concurrent sessions | Reduce --snmp-concurrency and --ping-workers |
| CDP/LLDP neighbors empty | Protocol disabled or community read-only restriction | Check that CDP/LLDP is enabled and the community allows table reads |
ImportError: PyYAML required |
YAML output requested without pyyaml | pip install pyyaml |
| Device shows IP instead of hostname | sysName not set or SNMP failed |
Set a hostname on the device, or rename in inventory manually |
Verify SNMP manually¶
Before scanning, confirm SNMP is reachable with the system snmpwalk command:
If this works, the scanner will find the device. If it times out, check firewall rules on UDP port 161.
Enable verbose logging¶
This shows per-host ping results and SNMP errors.
Integration with Inventory Management¶
Full pipeline walkthrough: Auto-Inventory Generation Pipeline explains every stage from scan to Ansible — vendor detection, deduplication, vault integration, and more.
After scanning, the fragment slots directly into your inventory workflow:
# 1. Scan and create an initial inventory
python -m netops.inventory.scan --subnet 10.0.0.0/24 --output my-inventory.json
# 2. Add credentials and groups by hand (or use the vault)
# See: docs/guides/inventory-management.md
# 3. Rescan a new subnet and merge additions
python -m netops.inventory.scan --subnet 10.0.1.0/24 --merge my-inventory.yaml
# 4. Use the inventory with other tools
python -m netops.collect.config --inventory my-inventory.yaml
See Inventory Management for full details on the inventory format, groups, tags, and Ansible export.