Contents
The Problem with Manual IP Documentation
In large network environments, keeping an accurate record of every interface IP address across dozens of switches, routers, and firewalls is tedious and error-prone. Engineers copy-paste from show ip interface brief, miss secondary addresses, forget HSRP VIPs, and the document is out of date before the change window closes.
This script solves that by reading the device's own running config โ the source of truth โ and generating a clean, aligned hosts-style output file automatically.
What the Script Does
Given one or more Cisco running config files (IOS, ASA, or Nexus), it produces a text file in this format:
# Single device output: <hostname>_interfaces_hosts.txt# Folder of devices: interfaces_hosts_<timestamp>.txtEach line follows a consistent pattern:
<ip-address> <hostname>_<interface>_<description> #<network>/<prefix>A real output looks like this:
10.0.10.1 CORE-SW-01_Vlan10_Management #10.0.10.0/2410.0.20.1 CORE-SW-01_Vlan20_ServerFarm #10.0.20.0/2410.0.20.254 CORE-SW-01_Vlan20_ServerFarm_VIP #10.0.20.0/2410.0.30.1 CORE-SW-01_Vlan30_VoIP #10.0.30.0/2410.0.0.1 CORE-SW-01_Gig0-0_uplink-to-BORDER-01 #10.0.0.0/3010.0.0.5 CORE-SW-01_Gig0-1_uplink-to-BORDER-02 #10.0.0.4/3010.1.1.1 CORE-SW-01_loopback0_RouterID #10.1.1.1/32 CORE-SW-01_Vlan99_Quarantine203.0.113.10 CORE-SW-01_Peer_IPNotice:
- HSRP VIP on Vlan20 is captured as
_VIP - SVIs with no IP address still appear as alias-only lines (useful for VLAN inventory)
- Crypto map peer IPs appear at the bottom
- The
#CIDRcolumn is perfectly aligned across all rows
Supported Platforms and Config Formats
The script handles three config styles automatically:
! Cisco IOS / IOS-XE โ dotted subnet maskinterface Vlan10 description Management ip address 10.0.10.1 255.255.255.0 standby 1 ip 10.0.10.254! Cisco ASA โ uses nameif instead of descriptioninterface GigabitEthernet0/0 nameif outside ip address 203.0.113.2 255.255.255.248! Cisco Nexus (NX-OS) โ CIDR notation with optional taginterface Ethernet1/1 description uplink-to-SPINE-01 ip address 10.0.0.1/30 tag 100 vrf member UNDERLAYAll three produce the same clean alias format in the output.
How the Parsing Works
The script does two passes through each config file.
Pass 1 โ Interface Blocks
It walks line by line looking for interface <name> to start a block, then collects everything inside until it hits another interface or a ! / end line.
Each interface block collects:
- description (or nameif on ASA)
- ip address (dotted mask OR CIDR)
- standby/vrrp/glbp VIP addresses
- vrf member (Nexus)
The alias is built as:
alias = hostname + "_" + sanitized_interface + "_" + compact_description# Example: CORE-SW-01_Vlan10_Management# Example: BORDER-01_TenG1-1_uplink-to-ISP-AInterface names are abbreviated to keep aliases readable:
GigabitEthernet -> GigTenGigabitEthernet -> TenGForward slashes in interface numbers are replaced with dashes so the alias is filesystem-safe:
Ethernet1/1 -> Ethernet1-1GigabitEthernet0/0 -> Gig0-0Pass 2 โ Crypto Map Peers
A second pass scans the entire config for crypto map peer statements and collects any peer IPs that aren't already listed as interface addresses:
crypto map OUTSIDE-MAP 10 set peer 203.0.113.50 203.0.113.51! Produces:! 203.0.113.50 BORDER-01_Peer_IP! 203.0.113.51 BORDER-01_Peer_IPVIP Detection (HSRP / VRRP / GLBP)
Any first-hop redundancy protocol VIP is automatically captured and tagged with _VIP. The subnet is inferred from the primary IP on the same interface:
! Configinterface Vlan20 description ServerFarm ip address 10.0.20.1 255.255.255.0 standby 1 ip 10.0.20.254Output:
10.0.20.1 CORE-SW-01_Vlan20_ServerFarm #10.0.20.0/2410.0.20.254 CORE-SW-01_Vlan20_ServerFarm_VIP #10.0.20.0/24The same logic applies to VRRP (vrrp 1 ip) and GLBP (glbp 1 ip).
No-IP SVIs โ VLAN Inventory for Free
SVIs that exist in the config but have no IP address assigned still appear in the output as alias-only lines. This is useful for VLAN inventory โ you can see every VLAN SVI that exists on the device even if it's not yet routed:
! Configinterface Vlan99 description Quarantine no ip addressOutput (alias-only, indented under the IP column):
CORE-SW-01_Vlan99_QuarantineVlan1 is excluded entirely since it's almost always unconfigured and creates noise.
Requirements
Before running the script, you need two things in place.
1. A Cisco Device Configuration File
Export the running configuration from your device using the appropriate command and save it as a plain text file. The script accepts any of the following formats: .txt, .cfg, .conf, .log, .config, or .running-config. Files with no extension are also supported.
The script handles three Cisco platform types automatically โ no manual configuration required:
- Cisco Catalyst / IOS / IOS-XE โ standard routers and campus switches using dotted subnet mask notation
- Cisco ASA / FTD Firewalls โ security appliances using
nameiffor interface naming and dotted mask notation - Cisco Nexus (NX-OS) โ data center switches using CIDR slash notation, VRF membership, and optional route tags
To export the running config from your device, run show running-config in privileged exec mode and save the output to a file. Most network management tools (SolarWinds, RANCID, Oxidized) already archive these automatically โ you can point the script directly at those backup files.
2. The Python Script
Download convert_hostfile.py from the bottom of this page. The script requires Python 3.6 or later and uses only modules from the standard library โ no pip install needed.
Once downloaded, make it executable and run it directly against your configuration file:
chmod +x convert_hostfile.py./convert_hostfile.py CORE-SW-01.txtOr run it with python3 explicitly if you prefer not to chmod:
python3 convert_hostfile.py CORE-SW-01.txtThe general invocation pattern is:
./convert_hostfile.py <configuration-file.txt>Pass a single file to generate a per-device output, or pass a folder path to batch-process an entire directory of configs into one consolidated file.
Running the Script
Single device
python3 convert_hostfile.py CORE-SW-01.cfg# Output: CORE-SW-01_interfaces_hosts.txtEntire folder of configs
python3 convert_hostfile.py /backups/configs/# Output: interfaces_hosts_20260304_142301.txtSupported file extensions: .txt, .cfg, .conf, .log, .config, .running-config. Files with no extension are also picked up.
Practical Use Cases
Once you have the output file you can use it directly in several workflows:
# 1. Append to your team hosts file for name resolution during troubleshootingcat CORE-SW-01_interfaces_hosts.txt >> /etc/hosts# 2. Grep for a specific subnet to find all IPs in that rangegrep "#10.0.20" interfaces_hosts_20260304.txt# 3. Find all VIPs across the environmentgrep "_VIP" interfaces_hosts_20260304.txt# 4. Find all crypto peersgrep "_Peer_IP" interfaces_hosts_20260304.txt# 5. Diff two runs to see what changed between config backupsdiff interfaces_hosts_20260303.txt interfaces_hosts_20260304.txtThe Full Script
#!/usr/bin/env python3"""Cisco IOS / ASA / Nexus Config -> Hosts-style Interface ListSupports: dotted mask (IOS/ASA), CIDR (Nexus), HSRP/VRRP/GLBP VIPs, crypto map peers, no-IP VLAN SVIs, single file or folder batch."""import os, re, sysfrom datetime import datetimefrom ipaddress import ip_interface, ip_network, ip_addressfrom typing import List, Tuple, SetTABSTOP = 8CIDR_GAP_SPACES = 2EXTRA_HASH_TABS = 3MAX_DESC_LEN = 45SUPPORTED_EXTS = {".txt", ".cfg", ".conf", ".log", ".config", ".running-config"}INTERFACE_START_RE = re.compile(r"^\s*interface\s+(?P<ifname>\S+)\s*$", re.IGNORECASE)HOSTNAME_LINE_RE = re.compile(r"^\s*hostname\s+(?P<hostname>\S+)\s*$", re.IGNORECASE)DESC_RE = re.compile(r"^\s*description\s+(?P<desc>.+?)\s*$", re.IGNORECASE)NAMEIF_RE = re.compile(r"^\s*nameif\s+(?P<nameif>\S+)\s*$", re.IGNORECASE)VRF_MEMBER_RE = re.compile(r"^\s*vrf\s+member\s+(?P<vrf>\S+)\s*$", re.IGNORECASE)IPV4_MASK_RE = re.compile( r"^\s*ip\s+address\s+(?P<ip>\d+\.\d+\.\d+\.\d+)\s+(?P<mask>\d+\.\d+\.\d+\.\d+)" r"(?:\s+secondary)?\s*$", re.IGNORECASE)IPV4_CIDR_RE = re.compile( r"^\s*ip\s+address\s+(?P<ip>\d+\.\d+\.\d+\.\d+)\/(?P<pfx>\d+)(?:\s+tag\s+\S+)?\s*$", re.IGNORECASE)DHCP_RE = re.compile(r"^\s*ip\s+address\s+dhcp", re.IGNORECASE)BLOCK_END_RE = re.compile(r"^\s*!\s*$|^\s*end\s*$", re.IGNORECASE)HSRP_RE = re.compile(r"^\s*standby\s+\d+\s+ip\s+(?P<vip>\d+\.\d+\.\d+\.\d+)\s*$", re.IGNORECASE)VRRP_RE = re.compile(r"^\s*vrrp\s+\d+\s+(?:ip|address)\s+(?P<vip>\d+\.\d+\.\d+\.\d+)\s*$", re.IGNORECASE)GLBP_RE = re.compile(r"^\s*glbp\s+\d+\s+ip\s+(?P<vip>\d+\.\d+\.\d+\.\d+)\s*$", re.IGNORECASE)CRYPTO_PEER_LINE_RE = re.compile(r"^\s*crypto\s+map\s+\S+\s+\d+\s+set\s+peer\s+(?P<peers>.+?)\s*$", re.IGNORECASE)IPV4_TOKEN_RE = re.compile(r"\d+\.\d+\.\d+\.\d+")def abbreviate_iface_prefix(name): for long, short in [("TenGigabitEthernet","TenG"),("TenGigabit","TenG"),("GigabitEthernet","Gig")]: if re.match(f"^{long}", name, re.IGNORECASE): return re.sub(f"^{long}", short, name, flags=re.IGNORECASE) return namedef sanitize_iface(name): return abbreviate_iface_prefix(name or "").strip().replace("/", "-")def no_space(text): return re.sub(r"\s+", "", text or "")def mask_to_prefix(mask_str): return ip_interface(f"0.0.0.0/{mask_str}").network.prefixlendef parse_hostname(lines): for l in lines: m = HOSTNAME_LINE_RE.match(l) if m: return m.group("hostname").strip() return "device"def gather_config_files(path): path = os.path.expanduser(path) if os.path.isfile(path): return [path] if os.path.isdir(path): files = [] for root, _, fnames in os.walk(path): for fn in fnames: ext = os.path.splitext(fn)[1].lower() if ext in SUPPORTED_EXTS or not ext: files.append(os.path.join(root, fn)) return files raise FileNotFoundError(f"Path not found: {path}")def is_vlan(ifn): return re.match(r"^Vlan\d+$", ifn, re.IGNORECASE) is not Nonedef is_vlan1(ifn): return re.match(r"^Vlan1$", ifn, re.IGNORECASE) is not Nonedef build_alias(hostname, ifname, desc_compact): parts = [hostname, sanitize_iface(ifname)] if desc_compact: parts.append(desc_compact) return "_".join(parts)def parse_device(content): lines = content.splitlines() hostname = parse_hostname(lines) entries, no_ip_vlan_aliases, peer_ips = [], [], set() i, n = 0, len(lines) while i < n: m = INTERFACE_START_RE.match(lines[i]) if not m: i += 1; continue ifname = m.group("ifname").strip() if is_vlan1(ifname): i += 1 while i < n and not (INTERFACE_START_RE.match(lines[i]) or BLOCK_END_RE.match(lines[i])): i += 1 continue desc = asa_nameif = vrf_member = None ipv4s_mask, ipv4s_cidr, vips = [], [], [] has_dhcp = False i += 1 while i < n: l = lines[i] if INTERFACE_START_RE.match(l) or BLOCK_END_RE.match(l): break if (md := DESC_RE.match(l)): desc = md.group("desc").strip() if (mn := NAMEIF_RE.match(l)) and not asa_nameif: asa_nameif = mn.group("nameif").strip() if (mv := VRF_MEMBER_RE.match(l)) and not vrf_member: vrf_member = mv.group("vrf").strip() if (mm := IPV4_MASK_RE.match(l)): ipv4s_mask.append((mm.group("ip"), mm.group("mask"))) if (mc := IPV4_CIDR_RE.match(l)): ipv4s_cidr.append((mc.group("ip"), int(mc.group("pfx")))) elif DHCP_RE.match(l): has_dhcp = True for rex in (HSRP_RE, VRRP_RE, GLBP_RE): if (mv2 := rex.match(l)): vips.append(mv2.group("vip")) i += 1 desc_compact = no_space(desc if desc else (asa_nameif or ""))[:MAX_DESC_LEN] if is_vlan(ifname) and not (ipv4s_mask or ipv4s_cidr or has_dhcp): no_ip_vlan_aliases.append(build_alias(hostname, ifname, desc_compact)) for ip, mask in ipv4s_mask: pfx = mask_to_prefix(mask) net = ip_network(f"{ip}/{pfx}", strict=False) entries.append((ip, build_alias(hostname, ifname, desc_compact), f"{net.network_address}/{pfx}")) for ip, pfx in ipv4s_cidr: net = ip_network(f"{ip}/{pfx}", strict=False) entries.append((ip, build_alias(hostname, ifname, desc_compact), f"{net.network_address}/{pfx}")) inferred_cidr = "" if ipv4s_mask: pfx0 = mask_to_prefix(ipv4s_mask[0][1]) net0 = ip_network(f"{ipv4s_mask[0][0]}/{pfx0}", strict=False) inferred_cidr = f"{net0.network_address}/{pfx0}" elif ipv4s_cidr: net0 = ip_network(f"{ipv4s_cidr[0][0]}/{ipv4s_cidr[0][1]}", strict=False) inferred_cidr = f"{net0.network_address}/{ipv4s_cidr[0][1]}" for vip in vips: entries.append((vip, build_alias(hostname, ifname, desc_compact) + "_VIP", inferred_cidr)) for l in lines: if (mc := CRYPTO_PEER_LINE_RE.match(l)): for iptok in IPV4_TOKEN_RE.findall(mc.group("peers")): peer_ips.add(iptok) existing_ips = {ip for ip, _, _ in entries} peer_entries = [(pip, f"{hostname}_Peer_IP") for pip in sorted(peer_ips - existing_ips, key=lambda x: int(ip_address(x)._ip))] alias_start_col = max((len((ip+" ").expandtabs(TABSTOP)) for ip,_,_ in entries), default=TABSTOP*3) max_alias_len = max([len(a) for _,a,_ in entries] + [len(a) for a in no_ip_vlan_aliases], default=0) target_hash_col = alias_start_col + max_alias_len + CIDR_GAP_SPACES + (EXTRA_HASH_TABS * TABSTOP) return hostname, entries, no_ip_vlan_aliases, peer_entries, alias_start_col, target_hash_coldef write_output(out_path, entries, no_ip_vlan_aliases, peer_entries, alias_start_col, target_hash_col): tabs_for_alias_only = max(1, (alias_start_col + TABSTOP - 1) // TABSTOP) alias_only_prefix = "\t" * tabs_for_alias_only with open(out_path, "w", encoding="utf-8") as out: for ip, alias, cidr in entries: prefix = f"{ip}\t\t{alias}" pad = max(1, target_hash_col - len(prefix.expandtabs(TABSTOP))) out.write(f"{prefix}{' ' * pad}#{cidr}\n" if cidr else f"{prefix}\n") for alias in no_ip_vlan_aliases: out.write(f"{alias_only_prefix}{alias}\n") for pip, palias in peer_entries: out.write(f"{pip}\t\t{palias}\n") return out_pathdef main(): try: path = sys.argv[1] if len(sys.argv) > 1 else input("Config file or folder: ").strip().strip('"') if not path: sys.exit(1) files = gather_config_files(path) if not files: print("No config files found."); sys.exit(1) if len(files) == 1: with open(files[0], encoding="utf-8", errors="ignore") as fh: content = fh.read() _, entries, no_ip, peers, a_col, h_col = parse_device(content) base = os.path.splitext(files[0])[0] print("Wrote:", write_output(f"{base}_interfaces_hosts.txt", entries, no_ip, peers, a_col, h_col)) else: all_e, all_n, all_p, a_col, h_col = [], [], [], TABSTOP*3, TABSTOP*8 for f in files: try: with open(f, encoding="utf-8", errors="ignore") as fh: content = fh.read() _, e, n, p, ac, hc = parse_device(content) all_e += e; all_n += n; all_p += p a_col = max(a_col, ac); h_col = max(h_col, hc) except Exception as ex: print(f"! {f}: {ex}") all_e.sort(key=lambda t: int(ip_address(t[0])._ip)) stamp = datetime.now().strftime("%Y%m%d_%H%M%S") print("Wrote:", write_output( os.path.join(os.path.abspath(path), f"interfaces_hosts_{stamp}.txt"), all_e, all_n, all_p, a_col, h_col)) except KeyboardInterrupt: print("\nInterrupted.")if __name__ == "__main__": main()This script is the sole work and property of Emmanuel Bracuso. It is not affiliated with any company, organization, or institution. Free to use, copy, modify, and distribute for personal, educational, or commercial purposes โ provided that credit is given to the original author in any public use or redistribution. The software is provided as-is, without warranty of any kind.
View full license on GitHub โ