Back to Blog
โ˜…โ˜…โ˜†Intermediate๐Ÿค– Network Automation
PythonAutomationCiscoNexusNetwork DocumentationScripting

Automating Interface Documentation: Parsing Cisco IOS, ASA & Nexus Configs into a Hosts File

March 4, 2026ยท12 min read

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:

bash
# Single device output: <hostname>_interfaces_hosts.txt# Folder of devices:    interfaces_hosts_<timestamp>.txt

Each line follows a consistent pattern:

bash
<ip-address>    <hostname>_<interface>_<description>    #<network>/<prefix>

A real output looks like this:

bash
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_IP

Notice:

  • 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 #CIDR column is perfectly aligned across all rows

Supported Platforms and Config Formats

The script handles three config styles automatically:

cisco
! 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 UNDERLAY

All 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:

python
alias = hostname + "_" + sanitized_interface + "_" + compact_description# Example: CORE-SW-01_Vlan10_Management# Example: BORDER-01_TenG1-1_uplink-to-ISP-A

Interface names are abbreviated to keep aliases readable:

python
GigabitEthernet     ->  GigTenGigabitEthernet  ->  TenG

Forward slashes in interface numbers are replaced with dashes so the alias is filesystem-safe:

python
Ethernet1/1    ->  Ethernet1-1GigabitEthernet0/0  ->  Gig0-0

Pass 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:

cisco
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_IP

VIP 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:

cisco
! Configinterface Vlan20 description ServerFarm ip address 10.0.20.1 255.255.255.0 standby 1 ip 10.0.20.254

Output:

bash
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/24

The 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:

cisco
! Configinterface Vlan99 description Quarantine no ip address

Output (alias-only, indented under the IP column):

bash
                CORE-SW-01_Vlan99_Quarantine

Vlan1 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 nameif for 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:

bash
chmod +x convert_hostfile.py./convert_hostfile.py CORE-SW-01.txt

Or run it with python3 explicitly if you prefer not to chmod:

bash
python3 convert_hostfile.py CORE-SW-01.txt

The general invocation pattern is:

bash
./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

bash
python3 convert_hostfile.py CORE-SW-01.cfg# Output: CORE-SW-01_interfaces_hosts.txt

Entire folder of configs

bash
python3 convert_hostfile.py /backups/configs/# Output: interfaces_hosts_20260304_142301.txt

Supported 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:

bash
# 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.txt

The Full Script

python
#!/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()

convert_hostfile.py

Python 3 ยท No external dependencies ยท ~150 lines

Download .py
Personal Attribution License

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 โ†’