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
showcommand 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
- Warm-up. Use
parse_outputto parse theshow ip interface briefsample above and print how many records come back. - Filter. From those records, print only interfaces whose
statusis"up", formatted as<interface> -> <ipaddr>. - Different command. Find the ntc-templates entry for
show versiononcisco_ios, parse a sample, and print the extracted fields (the record’s keys reveal what the template captures). - Custom template. Write a TextFSM template that parses lines of the form
Vlan10 active Gi0/1, Gi0/2into columnsVLAN,STATUS, andPORTS. Drive it from Python and print the dicts. - 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 savedshow ip interface briefto 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.