| name | cisco-log-to-json-parsing |
| description | Parse Cisco switch terminal log files (.log) into structured JSON. Covers IOS, IOS-XE, and NX-OS platforms. Use when asked to parse switch logs, extract network device info, build topology from CDP, or structure Cisco CLI output. |
| metadata | {"author":"louie","version":"1.0.0","argument-hint":"<log-file-or-directory>"} |
Cisco Switch Log → Structured JSON
Parse Cisco switch terminal session log files into devices.json and topology.json. Supports IOS, IOS-XE, and NX-OS platforms. Uses only Python stdlib (json, re, os, sys, pathlib).
When to Apply
- Parsing Cisco switch
.log files (terminal session captures)
- Extracting device info, interfaces, VLANs, CDP neighbors, phones
- Building network topology from CDP data
- Generating replacement/migration documentation
- Any task involving structured data extraction from Cisco CLI output
Input Format
Log files are terminal session captures (UTF-8 with BOM, CRLF line endings). Each file contains output from multiple show commands executed on one switch. Command sections are delimited by the switch prompt pattern:
hostname#show version
... output ...
hostname#show cdp nei
... output ...
Prompt Detection Regex
PROMPT_CMD_RE = re.compile(r"^(?P<host>[^#\s]+(?:\([^\)]*\))?)#\s*(?P<cmd>.*)$")
This matches lines like Switch01#show version, N5K-01(config)#show run, etc.
Supported Commands & Parsing Details
1. show version
Purpose: Extract hostname, IOS version, uptime, serial number, model, platform type.
Canonical match: Any command starting with show version, sh version.
Platform detection:
| Text in output | Platform |
|---|
Nexus Operating System or NX-OS | NX-OS |
IOS XE or IOS-XE | IOS-XE |
Cisco IOS Software (without XE) | IOS |
| None of the above | Default to IOS |
Key regex patterns:
r"^\s*(?:kickstart|system):\s*version\s+(.+)"
r"\buptime is (.+)"
r"Kernel uptime is (.+)"
r"System serial number\s*[: ]\s*(\S+)"
r"Processor\s+Board\s+ID\s+(\S+)"
r"^cisco\s+(\S+)"
r"Model Number\s*:\s*(\S+)"
Fallback: If model/serial not found in show version, fall back to show inventory (prefer entries named "chassis", "switch", or "stack").
2. show inventory
Purpose: Extract SFP/optic modules and fallback model/serial.
Canonical match: show inv, show inventory, sh inv.
Parsing: Two-line blocks:
NAME: "TenGigabitEthernet1/1", DESCR: "SFP-10G-SR"
PID: SFP-10GBase-SR , VID: V03 , SN: AVD2048S14A
Key regex:
r'NAME:\s*"([^"]+)"'
r'DESCR:\s*"([^"]+)"'
r'PID:\s*([^,]+)'
r'SN:\s*([^,\s]+)'
SFP module filter: Entry name must contain a digit AND contain / or start with letter+digit (e.g., GigabitEthernet0/1, Ethernet1/1).
3. show vlan brief
Purpose: Extract VLAN ID, name, status.
Canonical match: show vlan, sh vlan.
Table header: VLAN Name Status
Row regex:
r"^\s*(\d+)\s+(\S+)\s+(\S+)"
Deduplicate by VLAN ID (keep last seen).
4. show int description
Purpose: Extract interface operational status, protocol status, description.
Canonical match: Commands containing int + description or interface + description.
Three sub-table modes (detected by header line):
| Header pattern | Mode | Platform |
|---|
Interface Status Protocol | ios | IOS / IOS-XE |
Port Type Speed Description | nxos_physical | NX-OS physical ports |
Interface Description (no "Status") | nxos_logical | NX-OS logical interfaces |
IOS parsing regex:
r"^\s*(\S+)\s{2,}(.+?)\s{2,}(\S+)\s*(.*)$"
NX-OS physical:
r"^\s*(\S+)\s+(\S+)\s+(\S+)\s*(.*)$"
NX-OS logical:
r"^\s*(\S+)\s+(.*)$"
5. show int status
Purpose: Extract interface name (description), oper status, VLAN, duplex, speed, media type.
Canonical match: Commands containing int + status or interface + status.
Table header: Port Name Status Vlan Duplex Speed Type
CRITICAL: Column-position-based parsing. The header line is used to find the start position of each column:
col_pos = {
"Port": line.find("Port"),
"Name": line.find("Name"),
"Status": line.find("Status"),
"Vlan": line.find("Vlan"),
"Duplex": line.find("Duplex"),
"Speed": line.find("Speed"),
"Type": line.find("Type"),
}
port = line[col_pos["Port"] : col_pos["Name"]].strip()
name = line[col_pos["Name"] : col_pos["Status"]].strip()
status = line[col_pos["Status"] : col_pos["Vlan"]].strip()
vlan = line[col_pos["Vlan"] : col_pos["Duplex"]].strip()
duplex = line[col_pos["Duplex"] : col_pos["Speed"]].strip()
speed = line[col_pos["Speed"] : col_pos["Type"]].strip()
media = line[col_pos["Type"]:].strip()
Fallback (if line too short for column slicing): split by whitespace, take first 5 fields as port, status, vlan, duplex, speed, rest as media.
6. show cdp nei (brief)
Purpose: Extract CDP neighbor device ID, local interface, capability, platform, remote port.
Canonical match: Commands containing cdp + nei (but NOT detail).
Table header regex:
r"Device[\s-]ID\s+Local Intrfce\s+H[ol]*dtme\s+Capability\s+Platform\s+Port ID"
Platform detection from header:
Device-ID or Hldtme in header → NX-OS mode
- Otherwise → IOS mode
Column positions are detected from header (same approach as show int status).
NX-OS special handling: Device IDs can wrap to the next line. If a line is shorter than the Port ID column position and contains no double-spaces, it's a "pending device" that belongs to the next data line.
NX-OS multi-split parsing (by \s{2,}):
- 6 parts →
device_id, local_if, holdtme, capability, platform, port_id
- 5 parts → either
device_id is pending (starts with interface-like pattern) or last field combines platform + port_id
- 4 parts →
device_id is pending; local_if, holdtme, capability, platform+port_id
Interface-like detection (for NX-OS pending device logic):
r"^(?:Eth|Po|Port-channel|Vlan|Lo|mgmt)\S*"
Platform+Port splitting (when combined in one field):
re.split(r"\s+(?=\S+$)", text.strip(), maxsplit=1)
Deduplication key: (normalized_device_id, normalized_local_if, normalized_remote_port) — all lowercased.
7. show cdp nei detail
Purpose: Extract detailed CDP info: IP address, platform, capabilities, interfaces, native VLAN, software version, power drawn.
Canonical match: Commands containing cdp + nei + detail.
Block delimiter: Device ID: <name>
Key regex patterns:
r"IP address:\s*([0-9.]+)"
r"Platform:\s*([^,]+),\s*Capabilities:\s*(.+)$"
r"Interface:\s*([^,]+),\s*Port ID \(outgoing port\):\s*(.+)$"
r"Native VLAN:\s*(\S+)"
r"Power drawn:\s*(.+)$"
Software version: Lines after Version until blank line. Take first non-blank line.
8. show running-config
Purpose: Extract per-interface configuration and hostname.
Canonical match: show run, show running, sh run.
Hostname: hostname <name> line.
Interface blocks: Start with interface <name> line. All indented lines below belong to that interface until the next interface or non-indented line.
Config fields extracted per interface:
| Config line | JSON field | Notes |
|---|
description <text> | description | |
switchport mode access|trunk | switchport_mode | Only access or trunk |
switchport access vlan <N> | switchport_access_vlan | Integer |
switchport trunk allowed vlan [add] <list> | switchport_trunk_allowed_vlans | String, handles add keyword |
switchport voice vlan <N> | switchport_voice_vlan | Integer |
switchport trunk native vlan <N> | switchport_trunk_native_vlan | Integer |
shutdown / no shutdown | shutdown | Boolean |
spanning-tree portfast | spanning_tree_portfast | Boolean |
speed <value> | speed | String |
channel-group <N> | channel_group | Integer (group number) |
ip address <IP> <mask> | ip_address | IP string (not dhcp) |
no ip address | ip_address | Set to null |
9. show ip int brief
Purpose: Extract IP addresses assigned to interfaces.
Canonical match: Commands starting with show ip + containing int/inter + bri/brief.
Two table formats:
| Header | Platform | Columns |
|---|
Interface IP-Address OK? Method | IOS | 6+ columns: iface, ip, ok, method, status, protocol |
Interface IP Address Interface Status | NX-OS | 3+ columns: iface, ip, status (rest joined) |
Management IP selection priority:
mgmt0 / management0 (NX-OS management port)
Vlan102
Vlan1
- Any
Vlan interface
- Any other interface
- Within each tier, prefer
up status over down.
- Exclude
unassigned and 0.0.0.0.
10. show inter trunk
Purpose: Extract trunk interface native VLAN and allowed VLANs.
Canonical match: Commands containing trunk + (show inter or show int or show interface).
Two sub-tables:
-
Native VLAN table — header: Port Mode Encapsulation Status Native
- Column-position parsing (same pattern as
show int status)
-
Allowed VLANs table — header: Port Vlans allowed on trunk
- Whitespace split: first token = port, rest = allowed VLANs
Fallback: If show inter trunk yields no trunks, derive from show running-config — any interface with switchport_mode == "trunk" becomes a trunk entry.
Interface Name Normalization
All interface names are normalized to their full form:
prefix_map = {
"Gi": "GigabitEthernet",
"Gig": "GigabitEthernet",
"Te": "TenGigabitEthernet",
"Ten": "TenGigabitEthernet",
"Fa": "FastEthernet",
"Eth": "Ethernet",
"Po": "Port-channel",
"Vl": "Vlan",
}
Pattern: ^([A-Za-z]+)([\d/].*)$ → lookup prefix → {full_prefix}{rest}
Whitespace is stripped first (re.sub(r"\s+", "", raw)). Names without digits are returned as-is.
Device ID Normalization
def normalize_device_id(device_id):
base = device_id.strip()
base = re.sub(r"\(.*\)$", "", base)
if "." in base:
base = base.split(".", 1)[0]
return base
CDP Neighbor Merging & Phone Detection
Merge Logic
Brief (show cdp nei) and detail (show cdp nei detail) are parsed separately, then merged by key (normalized_device_id, normalized_local_interface). Detail enriches brief with: IP address, full capabilities list, remote interface, native VLAN, software version, power drawn.
Phone Detection
A CDP neighbor is classified as a phone when:
def is_phone_neighbor(capabilities, platform, remote_port):
caps = " ".join(capabilities).lower()
if any(kw in caps for kw in ("switch", "router", "bridge")):
return False
if "phone" in caps:
return True
if any(kw in platform.lower() for kw in ("t27g", "yealink", "spa")):
return True
if "wan port" in remote_port.lower():
return True
return False
Port-Channel Derivation
Port-channels are derived from show running-config:
- Scan all interfaces for
channel-group <N> → build member lists per group
- Scan interfaces named
Port-channel<N> → get their config (mode, etc.)
- Combine:
{ name: "Port-channel{N}", members: [...], mode: "trunk"|"access"|null }
Topology Building
Nodes
One node per parsed device: { id, model, serial, role, management_ip, platform_type }.
Role assignment:
| Model contains | Role |
|---|
N5K, NEXUS5548, C9300 | core |
C9200 | distribution |
2960, C2960 | access |
| Other | access (default) |
Links
Built from CDP neighbors (excluding phones):
- Match
neighbor_device against known hostnames (case-insensitive, normalized)
- Sort source/target alphabetically for consistent deduplication
- Dedup key:
(source, target, source_port, target_port) — all lowercased
- On collision, keep link with highest "score" (count of non-null fields: speed, media, link_type)
Link type: trunk if either endpoint's port appears in trunk map; else access.
Speed label:
| Speed value | Label |
|---|
Contains 10g or 10000 | 10G |
Contains 1000 or 1g | 1G |
Contains 100 | 100M |
External Devices
CDP neighbors not matching any known hostname → external_devices list. Type is router if capabilities contain "router"/"r" or platform contains "c1111"; else switch.
Output JSON Schema
devices.json
[
{
"hostname": "Switch01",
"model": "WS-C2960X-48FPD-L",
"serial_number": "FCW2048S14A",
"ios_version": "Cisco IOS Software, ...",
"uptime": "1 year, 2 weeks, 3 days, ...",
"platform_type": "IOS",
"management_ip": "10.1.1.1",
"vlans": [
{ "id": 1, "name": "default", "status": "active" }
],
"interfaces": {
"GigabitEthernet0/1": {
"description": "To-Server"
topology.json
{
"nodes": [
{
"id": "Switch01",
"model": "WS-C2960X-48FPD-L",
"serial": "FCW2048S14A",
"role": "access",
"management_ip": "10.1.1.1",
"platform_type": "IOS"
}
],
"links": [
{
"source": "CoreSwitch",
"source_port": "GigabitEthernet1/0/1",
"target": "Switch01",
"target_port": "GigabitEthernet0/49",
"speed": "1G",
"link_type":
Command Canonicalization
The parser recognizes abbreviated Cisco commands and maps them to canonical forms:
def canonical_command(cmd_text):
cmd = cmd_text.strip().lower()
Processing Pipeline
1. read_log() → lines[] (UTF-8-sig, error=replace)
2. parse_command_sections() → sections[] {cmd, raw_cmd, hostname, lines[]}
3. Group sections by canonical cmd
4. For each device log file:
a. parse_show_version() → hostname, model, serial, ios_version, uptime, platform_type
b. parse_inventory() → sfp_modules[], fallback model/serial
c. parse_running_config() → per-interface configs, hostname fallback
d. parse_vlan_brief() → vlans[]
e. parse_int_description() → populate interfaces{} oper_status, protocol, description
f. parse_int_status() → populate interfaces{} vlan, duplex, speed, media_type
g. parse_cdp_neighbors() → cdp brief list
h. parse_cdp_detail() → cdp detail list
i. merge_cdp() → cdp_neighbors[], phones[]
j. parse_ip_int_brief() → ip_interfaces[], management_ip
k. parse_interface_trunks() → trunks[] (fallback from running-config)
l. derive_port_channels() → port_channels[]
5. build_topology() → { nodes[], links[], external_devices[] }
6. Write devices.json, topology.json
Common Pitfalls
- NX-OS CDP wrapping: Device IDs longer than the column width wrap to the next line. Must buffer "pending device" lines.
- Column-position parsing:
show int status and show cdp nei use fixed-width columns. DO NOT split by whitespace — interface names can contain spaces in the Name column.
- Trunk fallback: Some switches don't support
show inter trunk. Fall back to running-config switchport mode trunk interfaces.
- Phone vs infrastructure: Nexus 5K with CVTA advertises "Phone" capability but is a switch. Always check for infrastructure keywords first.
- BOM handling: Log files may have UTF-8 BOM. Use
encoding="utf-8-sig" when reading.
- CDP dedup: Same neighbor can appear in both brief and detail. Merge by
(device_id, local_interface) key, don't create duplicates.