TextFSM CLI Table#
With TextFSM it is possible to process output of commands and get a structured result. However, it is still necessary to manually specify which template will handle show commands each time TextFSM is used.
It would be much more convenient to have some mapping between command and template so that you can write a common script that performs connections to devices, sends commands, chooses template and parse output according to template.
TextFSM has such feature. To use it, you should create a file that describes
mapping between commands and templates. In TextFSM it is called index
.
This file should be in a directory with templates and should have this format:
first line - column names
every next line is a pattern match to a command
mandatory columns with fixed position (mandatory first and last, respectively):
first column - names of templates
last column - the corresponding command. This column uses a special format to describe that a command may not be fully written
other columns are optional
in example below there are columns Hostname, Vendor. They allow you to refine your device information to determine which template to use. For example, show version command may be in Cisco and HP devices. Of course, having only commands are not sufficient to determine which template to use. In this case, you can pass information about the type of equipment used with command and then you can define the correct template.
all columns except the first column support regular expressions. Regular expressions are not supported inside
[[]]
Example of index file:
Template, Hostname, Vendor, Command
sh_cdp_n_det.template, .*, Cisco, sh[[ow]] cdp ne[[ighbors]] de[[tail]]
sh_clock.template, .*, Cisco, sh[[ow]] clo[[ck]]
sh_ip_int_br.template, .*, Cisco, sh[[ow]] ip int[[erface]] br[[ief]]
sh_ip_route_ospf.template, .*, Cisco, sh[[ow]] ip rou[[te]] o[[spf]]
Note how commands are written: sh[[ow]] ip int[[erface]] br[[ief]]
.
Record will be converted to sh((ow)?)? ip int((erface)?)? br((ief)?)?
.
This means that TextFSM will be able to determine which template to use even
if command is not fully written. For example, such command variants will work:
sh ip int br
show ip inter bri
How to use CLI table#
Let’s see how to use clitable
class and index file.
templates
directory contains such templates and index file:
sh_cdp_n_det.template
sh_clock.template
sh_ip_int_br.template
sh_ip_route_ospf.template
index
First we try to work with CLI Table in ipython to see what features this class has and then we look at the final script.
First, we import clitable
class:
In [1]: from textfsm import clitable
Warning
There are different ways to import clitable
depending on textfsm version:
import clitable
for version <= 0.4.1from textfsm import clitable
for version >= 1.1.0
See textfsm version: pip show textfsm
.
We will check clitable
on the last example from previous
section - “show ip route ospf” command. Read the output that is stored
in output/sh_ip_route_ospf.txt file to string:
In [2]: with open('output/sh_ip_route_ospf.txt') as f:
...: output_sh_ip_route_ospf = f.read()
...:
First, you should initialize a class by giving it name of file in which mapping between templates and commands is stored, and specify name of directory in which templates are stored:
In [3]: cli_table = clitable.CliTable('index', 'templates')
Specify which command should be passed and specify additional attributes that will help to identify template. To do this, you should create a dictionary in which keys are names of columns that are defined in index file. In this case, it is not necessary to specify vendor name, since “sh ip route ospf” command corresponds to only one template.
In [4]: attributes = {'Command': 'show ip route ospf' , 'Vendor': 'Cisco'}
Command output and dictionary with parameters should be passed to ParseCmd method:
In [5]: cli_table.ParseCmd(output_sh_ip_route_ospf, attributes)
As a result we have processed output of “sh ip route ospf” command in cli_table object.
cli_table methods (to see all methods, call dir(cli_table)):
In [6]: cli_table.
cli_table.AddColumn cli_table.NewRow cli_table.index cli_table.size
cli_table.AddKeys cli_table.ParseCmd cli_table.index_file cli_table.sort
cli_table.Append cli_table.ReadIndex cli_table.next cli_table.superkey
cli_table.CsvToTable cli_table.Remove cli_table.raw cli_table.synchronised
cli_table.FormattedTable cli_table.Reset cli_table.row cli_table.table
cli_table.INDEX cli_table.RowWith cli_table.row_class cli_table.template_dir
cli_table.KeyValue cli_table.extend cli_table.row_index
cli_table.LabelValueTable cli_table.header cli_table.separator
For example, if you call print(cli_table)
you get this:
In [7]: print(cli_table)
Network, Mask, Distance, Metric, NextHop
10.0.24.0, /24, 110, 20, ['10.0.12.2']
10.0.34.0, /24, 110, 20, ['10.0.13.3']
10.2.2.2, /32, 110, 11, ['10.0.12.2']
10.3.3.3, /32, 110, 11, ['10.0.13.3']
10.4.4.4, /32, 110, 21, ['10.0.13.3', '10.0.12.2', '10.0.14.4']
10.5.35.0, /24, 110, 20, ['10.0.13.3']
FormattedTable method produces a table output:
In [8]: print(cli_table.FormattedTable())
Network Mask Distance Metric NextHop
====================================================================
10.0.24.0 /24 110 20 10.0.12.2
10.0.34.0 /24 110 20 10.0.13.3
10.2.2.2 /32 110 11 10.0.12.2
10.3.3.3 /32 110 11 10.0.13.3
10.4.4.4 /32 110 21 10.0.13.3, 10.0.12.2, 10.0.14.4
10.5.35.0 /24 110 20 10.0.13.3
This can be useful for displaying information.
To get a structured output from cli_table object, such as a list of lists, you have to refer to object in this way:
In [9]: data_rows = [list(row) for row in cli_table]
In [11]: data_rows
Out[11]:
[['10.0.24.0', '/24', '110', '20', ['10.0.12.2']],
['10.0.34.0', '/24', '110', '20', ['10.0.13.3']],
['10.2.2.2', '/32', '110', '11', ['10.0.12.2']],
['10.3.3.3', '/32', '110', '11', ['10.0.13.3']],
['10.4.4.4', '/32', '110', '21', ['10.0.13.3', '10.0.12.2', '10.0.14.4']],
['10.5.35.0', '/24', '110', '20', ['10.0.13.3']]]
You can get column names separately:
In [12]: header = list(cli_table.header)
In [14]: header
Out[14]: ['Network', 'Mask', 'Distance', 'Metric', 'NextHop']
The output is now similar to that of the previous section.
Assemble everything into one script (textfsm_clitable.py file):
import clitable
output_sh_ip_route_ospf = open('output/sh_ip_route_ospf.txt').read()
cli_table = clitable.CliTable('index', 'templates')
attributes = {'Command': 'show ip route ospf' , 'Vendor': 'Cisco'}
cli_table.ParseCmd(output_sh_ip_route_ospf, attributes)
print('CLI Table output:\n', cli_table)
print('Formatted Table:\n', cli_table.FormattedTable())
data_rows = [list(row) for row in cli_table]
header = list(cli_table.header)
print(header)
for row in data_rows:
print(row)
In exercises to this section there will be a task to combine described procedure into a function and task to get a list of dictionaries.
The output will be:
$ python textfsm_clitable.py
CLI Table output:
Network, Mask, Distance, Metric, NextHop
10.0.24.0, /24, 110, 20, ['10.0.12.2']
10.0.34.0, /24, 110, 20, ['10.0.13.3']
10.2.2.2, /32, 110, 11, ['10.0.12.2']
10.3.3.3, /32, 110, 11, ['10.0.13.3']
10.4.4.4, /32, 110, 21, ['10.0.13.3', '10.0.12.2', '10.0.14.4']
10.5.35.0, /24, 110, 20, ['10.0.13.3']
Formatted Table:
Network Mask Distance Metric NextHop
====================================================================
10.0.24.0 /24 110 20 10.0.12.2
10.0.34.0 /24 110 20 10.0.13.3
10.2.2.2 /32 110 11 10.0.12.2
10.3.3.3 /32 110 11 10.0.13.3
10.4.4.4 /32 110 21 10.0.13.3, 10.0.12.2, 10.0.14.4
10.5.35.0 /24 110 20 10.0.13.3
['Network', 'Mask', 'Distance', 'Metric', 'NextHop']
['10.0.24.0', '/24', '110', '20', ['10.0.12.2']]
['10.0.34.0', '/24', '110', '20', ['10.0.13.3']]
['10.2.2.2', '/32', '110', '11', ['10.0.12.2']]
['10.3.3.3', '/32', '110', '11', ['10.0.13.3']]
['10.4.4.4', '/32', '110', '21', ['10.0.13.3', '10.0.12.2', '10.0.14.4']]
['10.5.35.0', '/24', '110', '20', ['10.0.13.3']]
Now with TextFSM it is possible not only to get a structured output, but also to automatically determine which template to use by command and optional arguments.