TextFSM and ntc-templates: Turn show Output into Structured Data

Day 9 covered regex for pulling facts out of show output, with a promise: for full tables, there is a better tool. This is that tool. TextFSM is a template‑based parser that turns semi‑structured CLI text into a list of records — rows and columns — without writing a regex by hand for each command. Paired with ntc-templates, a community library of ready‑made templates for hundreds of show commands across many vendors, it converts show ip interface brief or show cdp neighbors into clean Python dictionaries in one call.

This is Day 19 of the 21‑post Python for Network Engineers series — the last building‑block day before the two mini‑projects that close the series.

The Problem TextFSM Solves

Regex works for one field but gets unwieldy for a whole table. Consider parsing every row of show ip interface brief: interface, IP, status, protocol — four columns, variable whitespace, a header line to skip. A hand‑written parser for that is fragile and command‑specific. TextFSM replaces it with a reusable template that describes the columns once, then applies to any output of that command.

ntc-templates: The Shortcut Worth Taking First

Most of the time, writing a TextFSM template by hand is unnecessary — someone already has. The ntc-templates project ships templates for hundreds of commands. Install it:

pip install ntc-templates
from ntc_templates.parse import parse_output

raw = """Interface              IP-Address      OK? Method Status                Protocol
GigabitEthernet0/0     10.0.0.1        YES NVRAM  up                    up
GigabitEthernet0/1     unassigned      YES unset  administratively down down
GigabitEthernet0/2     10.0.1.1        YES NVRAM  up                    up"""

records = parse_output(platform="cisco_ios",
                       command="show ip interface brief",
                       data=raw)

for r in records:
    print(r)
# {'interface': 'GigabitEthernet0/0', 'ipaddr': '10.0.0.1', 'status': 'up', 'proto': 'up'}
# {'interface': 'GigabitEthernet0/1', 'ipaddr': 'unassigned', 'status': 'administratively down', 'proto': 'down'}
# {'interface': 'GigabitEthernet0/2', 'ipaddr': '10.0.1.1', 'status': 'up', 'proto': 'up'}

One call, and the wall of text is a list of dicts. parse_output takes the platform (matching Netmiko’s device_type), the command string, and the raw text. It looks up the right template internally. This is exactly what send_command(use_textfsm=True) did on Day 13 — now visible as a standalone step usable on any captured text, live or from a file.

From Text to a Real Answer

Once the output is structured, questions that were awkward with regex become trivial list comprehensions — the Day 4 skills applied to live data:

from ntc_templates.parse import parse_output

records = parse_output(platform="cisco_ios",
                       command="show ip interface brief", data=raw)

# Which interfaces are down?
down = [r["interface"] for r in records if r["proto"] == "down"]
print("Down:", down)

# How many interfaces have an IP assigned?
addressed = [r for r in records if r["ipaddr"] != "unassigned"]
print(f"{len(addressed)} of {len(records)} interfaces are addressed")

Writing a TextFSM Template by Hand

When ntc-templates lacks a command — a vendor‑specific or niche show — a custom template fills the gap. A TextFSM template has two parts: Value definitions (the columns, each with a regex) and State rules (how to match lines). Here is a minimal template for show ip interface brief:

Value INTERFACE (\S+)
Value IPADDR (\S+)
Value STATUS (up|down|administratively down)
Value PROTO (up|down)

Start
  ^${INTERFACE}\s+${IPADDR}\s+\w+\s+\w+\s+${STATUS}\s+${PROTO} -> Record
  ^Interface\s+IP-Address -> Next

Each Value NAME (regex) declares a column and the pattern that captures it. The Start state lists line patterns; a line matching the first rule fills the values and -> Record saves a row. The header line matches the second rule and is skipped with -> Next. Driving it from Python:

import textfsm
import io

with open("show_ip_int_brief.textfsm") as tmpl_file:
    fsm = textfsm.TextFSM(tmpl_file)
    rows = fsm.ParseText(raw)          # list of lists
    # Pair the header (fsm.header) with each row to build dicts
    records = [dict(zip(fsm.header, row)) for row in rows]

for r in records:
    print(r)

fsm.header holds the value names in order; zip‑ing it with each row (the Day 5 zip pattern) turns the list‑of‑lists into a list‑of‑dicts. That dict form is what makes the data pleasant to work with.

Cisco Context: A CDP Neighbor Map

A common real task — build a topology map from show cdp neighbors detail across the fleet — is a few lines once the output is structured. ntc-templates already has the template:

from ntc_templates.parse import parse_output
# from netmiko import ConnectHandler   # Day 13, for live capture

cdp_raw = open("cdp_output.txt").read()   # or conn.send_command(...)
neighbors = parse_output(platform="cisco_ios",
                         command="show cdp neighbors detail",
                         data=cdp_raw)

for n in neighbors:
    print(f"{n['local_interface']:12} -> {n['destination_host']} "
          f"({n['management_ip']}) on {n['remote_interface']}")

The combination that closes the loop: Netmiko captures the text (Day 13), TextFSM structures it (today), and a comprehension answers the question (Day 4). Three days of skills composing into a working tool.

When to Use Which

  • ntc-templates — the default. A standard show command on a common platform almost certainly has a template already.
  • Hand-written TextFSM — for a command ntc-templates does not cover, or output from a custom script.
  • Regex (Day 9) — for extracting a single value, or a quick one‑off where a full template is overkill.

Exercises

  1. Warm-up. Use parse_output to parse the show ip interface brief sample above and print how many records come back.
  2. Filter. From those records, print only interfaces whose status is "up", formatted as <interface> -> <ipaddr>.
  3. Different command. Find the ntc-templates entry for show version on cisco_ios, parse a sample, and print the extracted fields (the record’s keys reveal what the template captures).
  4. Custom template. Write a TextFSM template that parses lines of the form Vlan10 active Gi0/1, Gi0/2 into columns VLAN, STATUS, and PORTS. Drive it from Python and print the dicts.
  5. Challenge. Write parse_and_report(platform, command, path) that reads captured CLI output from a file, parses it with ntc-templates, and returns the list of dicts — then use it on a saved show ip interface brief to print a count of up vs. down interfaces.

Answers

Show answers

1. Warm-up

from ntc_templates.parse import parse_output
records = parse_output(platform="cisco_ios",
                       command="show ip interface brief", data=raw)
print(len(records))   # 3

2. Filter

for r in records:
    if r["status"] == "up":
        print(f"{r['interface']} -> {r['ipaddr']}")

3. Different command

version_raw = "..."   # paste real 'show version' output
recs = parse_output(platform="cisco_ios", command="show version",
                    data=version_raw)
print(recs[0].keys())   # e.g. version, hostname, uptime, serial, ...

The template determines the keys; inspecting recs[0].keys() is the quickest way to learn what a given template extracts.

4. Custom template

Value VLAN (\S+)
Value STATUS (active|suspended)
Value PORTS (.+)

Start
  ^${VLAN}\s+${STATUS}\s+${PORTS} -> Record
import textfsm
data = "Vlan10  active  Gi0/1, Gi0/2\nVlan20  active  Gi0/3"
with open("vlans.textfsm") as f:
    fsm = textfsm.TextFSM(f)
    rows = fsm.ParseText(data)
records = [dict(zip(fsm.header, r)) for r in rows]
print(records)
# [{'VLAN': 'Vlan10', 'STATUS': 'active', 'PORTS': 'Gi0/1, Gi0/2'}, ...]

5. Challenge

from ntc_templates.parse import parse_output

def parse_and_report(platform, command, path):
    with open(path) as f:
        data = f.read()
    return parse_output(platform=platform, command=command, data=data)

recs = parse_and_report("cisco_ios", "show ip interface brief", "sib.txt")
up = sum(1 for r in recs if r["proto"] == "up")
print(f"{up} up, {len(recs) - up} down")

Wrapping the parse in a function that takes a file path makes it reusable across every saved capture — the same shape the config‑backup project uses tomorrow.


Previously: NETCONF with ncclient. Coming tomorrow — Project 1: a complete tool that backs up running‑configs from a device list into a Git repository.

Leave a Reply