Skip to content

Commit 031aebf

Browse files
authored
Merge pull request #669 from ktbyers/develop
Release 2.0.1
2 parents cc08641 + 7b06c03 commit 031aebf

File tree

8 files changed

+1605
-6
lines changed

8 files changed

+1605
-6
lines changed

netmiko/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
NetmikoTimeoutError = NetMikoTimeoutException
2222
NetmikoAuthError = NetMikoAuthenticationException
2323

24-
__version__ = '2.0.0'
24+
__version__ = '2.0.1'
2525
__all__ = ('ConnectHandler', 'ssh_dispatcher', 'platforms', 'SCPConn', 'FileTransfer',
2626
'NetMikoTimeoutException', 'NetMikoAuthenticationException',
2727
'NetmikoTimeoutError', 'NetmikoAuthError', 'InLineTransfer', 'redispatch',

netmiko/_textfsm/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from netmiko._textfsm import _terminal
2+
from netmiko._textfsm import _texttable
3+
from netmiko._textfsm import _clitable
4+
5+
__all__ = ('_terminal', '_texttable', '_clitable')

netmiko/_textfsm/_clitable.py

Lines changed: 380 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,380 @@
1+
"""
2+
Google's clitable.py is inherently integrated to Linux:
3+
4+
This is a workaround for that (basically include modified clitable code without anything
5+
that is Linux-specific).
6+
7+
_clitable.py is identical to Google's as of 2017-12-17
8+
_texttable.py is identical to Google's as of 2017-12-17
9+
_terminal.py is a highly stripped down version of Google's such that clitable.py works
10+
11+
https://github.com/google/textfsm/blob/master/clitable.py
12+
"""
13+
14+
# Some of this code is from Google with the following license:
15+
#
16+
# Copyright 2012 Google Inc. All Rights Reserved.
17+
#
18+
# Licensed under the Apache License, Version 2.0 (the "License");
19+
# you may not use this file except in compliance with the License.
20+
# You may obtain a copy of the License at
21+
#
22+
# http://www.apache.org/licenses/LICENSE-2.0
23+
#
24+
# Unless required by applicable law or agreed to in writing, software
25+
# distributed under the License is distributed on an "AS IS" BASIS,
26+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
27+
# implied. See the License for the specific language governing
28+
# permissions and limitations under the License.
29+
30+
import copy
31+
import os
32+
import re
33+
import threading
34+
import copyable_regex_object
35+
import textfsm
36+
from netmiko._textfsm import _texttable as texttable
37+
38+
39+
class Error(Exception):
40+
"""Base class for errors."""
41+
42+
43+
class IndexTableError(Error):
44+
"""General INdexTable error."""
45+
46+
47+
class CliTableError(Error):
48+
"""General CliTable error."""
49+
50+
51+
class IndexTable(object):
52+
"""Class that reads and stores comma-separated values as a TextTable.
53+
Stores a compiled regexp of the value for efficient matching.
54+
Includes functions to preprocess Columns (both compiled and uncompiled).
55+
Attributes:
56+
index: TextTable, the index file parsed into a texttable.
57+
compiled: TextTable, the table but with compiled regexp for each field.
58+
"""
59+
60+
def __init__(self, preread=None, precompile=None, file_path=None):
61+
"""Create new IndexTable object.
62+
Args:
63+
preread: func, Pre-processing, applied to each field as it is read.
64+
precompile: func, Pre-compilation, applied to each field before compiling.
65+
file_path: String, Location of file to use as input.
66+
"""
67+
self.index = None
68+
self.compiled = None
69+
if file_path:
70+
self._index_file = file_path
71+
self._index_handle = open(self._index_file, 'r')
72+
self._ParseIndex(preread, precompile)
73+
74+
def __del__(self):
75+
"""Close index handle."""
76+
if hasattr(self, '_index_handle'):
77+
self._index_handle.close()
78+
79+
def __len__(self):
80+
"""Returns number of rows in table."""
81+
return self.index.size
82+
83+
def __copy__(self):
84+
"""Returns a copy of an IndexTable object."""
85+
clone = IndexTable()
86+
if hasattr(self, '_index_file'):
87+
# pylint: disable=protected-access
88+
clone._index_file = self._index_file
89+
clone._index_handle = self._index_handle
90+
91+
clone.index = self.index
92+
clone.compiled = self.compiled
93+
return clone
94+
95+
def __deepcopy__(self, memodict=None):
96+
"""Returns a deepcopy of an IndexTable object."""
97+
clone = IndexTable()
98+
if hasattr(self, '_index_file'):
99+
# pylint: disable=protected-access
100+
clone._index_file = copy.deepcopy(self._index_file)
101+
clone._index_handle = open(clone._index_file, 'r')
102+
103+
clone.index = copy.deepcopy(self.index)
104+
clone.compiled = copy.deepcopy(self.compiled)
105+
return clone
106+
107+
def _ParseIndex(self, preread, precompile):
108+
"""Reads index file and stores entries in TextTable.
109+
For optimisation reasons, a second table is created with compiled entries.
110+
Args:
111+
preread: func, Pre-processing, applied to each field as it is read.
112+
precompile: func, Pre-compilation, applied to each field before compiling.
113+
Raises:
114+
IndexTableError: If the column headers has illegal column labels.
115+
"""
116+
self.index = texttable.TextTable()
117+
self.index.CsvToTable(self._index_handle)
118+
119+
if preread:
120+
for row in self.index:
121+
for col in row.header:
122+
row[col] = preread(col, row[col])
123+
124+
self.compiled = copy.deepcopy(self.index)
125+
126+
for row in self.compiled:
127+
for col in row.header:
128+
if precompile:
129+
row[col] = precompile(col, row[col])
130+
if row[col]:
131+
row[col] = copyable_regex_object.CopyableRegexObject(row[col])
132+
133+
def GetRowMatch(self, attributes):
134+
"""Returns the row number that matches the supplied attributes."""
135+
for row in self.compiled:
136+
try:
137+
for key in attributes:
138+
# Silently skip attributes not present in the index file.
139+
# pylint: disable=E1103
140+
if key in row.header and row[key] and not row[key].match(attributes[key]):
141+
# This line does not match, so break and try next row.
142+
raise StopIteration()
143+
return row.row
144+
except StopIteration:
145+
pass
146+
return 0
147+
148+
149+
class CliTable(texttable.TextTable):
150+
"""Class that reads CLI output and parses into tabular format.
151+
Reads an index file and uses it to map command strings to templates. It then
152+
uses TextFSM to parse the command output (raw) into a tabular format.
153+
The superkey is the set of columns that contain data that uniquely defines the
154+
row, the key is the row number otherwise. This is typically gathered from the
155+
templates 'Key' value but is extensible.
156+
Attributes:
157+
raw: String, Unparsed command string from device/command.
158+
index_file: String, file where template/command mappings reside.
159+
template_dir: String, directory where index file and templates reside.
160+
"""
161+
162+
# Parse each template index only once across all instances.
163+
# Without this, the regexes are parsed at every call to CliTable().
164+
_lock = threading.Lock()
165+
INDEX = {}
166+
167+
# pylint: disable=C6409
168+
def synchronised(func):
169+
"""Synchronisation decorator."""
170+
171+
# pylint: disable=E0213
172+
def Wrapper(main_obj, *args, **kwargs):
173+
main_obj._lock.acquire() # pylint: disable=W0212
174+
try:
175+
return func(main_obj, *args, **kwargs) # pylint: disable=E1102
176+
finally:
177+
main_obj._lock.release() # pylint: disable=W0212
178+
return Wrapper
179+
# pylint: enable=C6409
180+
181+
@synchronised
182+
def __init__(self, index_file=None, template_dir=None):
183+
"""Create new CLiTable object.
184+
Args:
185+
index_file: String, file where template/command mappings reside.
186+
template_dir: String, directory where index file and templates reside.
187+
"""
188+
# pylint: disable=E1002
189+
super(CliTable, self).__init__()
190+
self._keys = set()
191+
self.raw = None
192+
self.index_file = index_file
193+
self.template_dir = template_dir
194+
if index_file:
195+
self.ReadIndex(index_file)
196+
197+
def ReadIndex(self, index_file=None):
198+
"""Reads the IndexTable index file of commands and templates.
199+
Args:
200+
index_file: String, file where template/command mappings reside.
201+
Raises:
202+
CliTableError: A template column was not found in the table.
203+
"""
204+
205+
self.index_file = index_file or self.index_file
206+
fullpath = os.path.join(self.template_dir, self.index_file)
207+
if self.index_file and fullpath not in self.INDEX:
208+
self.index = IndexTable(self._PreParse, self._PreCompile, fullpath)
209+
self.INDEX[fullpath] = self.index
210+
else:
211+
self.index = self.INDEX[fullpath]
212+
213+
# Does the IndexTable have the right columns.
214+
if 'Template' not in self.index.index.header: # pylint: disable=E1103
215+
raise CliTableError("Index file does not have 'Template' column.")
216+
217+
def _TemplateNamesToFiles(self, template_str):
218+
"""Parses a string of templates into a list of file handles."""
219+
template_list = template_str.split(':')
220+
template_files = []
221+
try:
222+
for tmplt in template_list:
223+
template_files.append(
224+
open(os.path.join(self.template_dir, tmplt), 'r'))
225+
except: # noqa
226+
for tmplt in template_files:
227+
tmplt.close()
228+
raise
229+
230+
return template_files
231+
232+
def ParseCmd(self, cmd_input, attributes=None, templates=None):
233+
"""Creates a TextTable table of values from cmd_input string.
234+
Parses command output with template/s. If more than one template is found
235+
subsequent tables are merged if keys match (dropped otherwise).
236+
Args:
237+
cmd_input: String, Device/command response.
238+
attributes: Dict, attribute that further refine matching template.
239+
templates: String list of templates to parse with. If None, uses index
240+
Raises:
241+
CliTableError: A template was not found for the given command.
242+
"""
243+
# Store raw command data within the object.
244+
self.raw = cmd_input
245+
246+
if not templates:
247+
# Find template in template index.
248+
row_idx = self.index.GetRowMatch(attributes)
249+
if row_idx:
250+
templates = self.index.index[row_idx]['Template']
251+
else:
252+
raise CliTableError('No template found for attributes: "%s"' %
253+
attributes)
254+
255+
template_files = self._TemplateNamesToFiles(templates)
256+
257+
try:
258+
# Re-initialise the table.
259+
self.Reset()
260+
self._keys = set()
261+
self.table = self._ParseCmdItem(self.raw, template_file=template_files[0])
262+
263+
# Add additional columns from any additional tables.
264+
for tmplt in template_files[1:]:
265+
self.extend(self._ParseCmdItem(self.raw, template_file=tmplt),
266+
set(self._keys))
267+
finally:
268+
for f in template_files:
269+
f.close()
270+
271+
def _ParseCmdItem(self, cmd_input, template_file=None):
272+
"""Creates Texttable with output of command.
273+
Args:
274+
cmd_input: String, Device response.
275+
template_file: File object, template to parse with.
276+
Returns:
277+
TextTable containing command output.
278+
Raises:
279+
CliTableError: A template was not found for the given command.
280+
"""
281+
# Build FSM machine from the template.
282+
fsm = textfsm.TextFSM(template_file)
283+
if not self._keys:
284+
self._keys = set(fsm.GetValuesByAttrib('Key'))
285+
286+
# Pass raw data through FSM.
287+
table = texttable.TextTable()
288+
table.header = fsm.header
289+
290+
# Fill TextTable from record entries.
291+
for record in fsm.ParseText(cmd_input):
292+
table.Append(record)
293+
return table
294+
295+
def _PreParse(self, key, value):
296+
"""Executed against each field of each row read from index table."""
297+
if key == 'Command':
298+
return re.sub(r'(\[\[.+?\]\])', self._Completion, value)
299+
else:
300+
return value
301+
302+
def _PreCompile(self, key, value):
303+
"""Executed against each field of each row before compiling as regexp."""
304+
if key == 'Template':
305+
return
306+
else:
307+
return value
308+
309+
def _Completion(self, match):
310+
# pylint: disable=C6114
311+
r"""Replaces double square brackets with variable length completion.
312+
Completion cannot be mixed with regexp matching or '\' characters
313+
i.e. '[[(\n)]] would become (\(n)?)?.'
314+
Args:
315+
match: A regex Match() object.
316+
Returns:
317+
String of the format '(a(b(c(d)?)?)?)?'.
318+
"""
319+
# Strip the outer '[[' & ']]' and replace with ()? regexp pattern.
320+
word = str(match.group())[2:-2]
321+
return '(' + ('(').join(word) + ')?' * len(word)
322+
323+
def LabelValueTable(self, keys=None):
324+
"""Return LabelValue with FSM derived keys."""
325+
keys = keys or self.superkey
326+
# pylint: disable=E1002
327+
return super(CliTable, self).LabelValueTable(keys)
328+
329+
# pylint: disable=W0622,C6409
330+
def sort(self, cmp=None, key=None, reverse=False):
331+
"""Overrides sort func to use the KeyValue for the key."""
332+
if not key and self._keys:
333+
key = self.KeyValue
334+
super(CliTable, self).sort(cmp=cmp, key=key, reverse=reverse)
335+
# pylint: enable=W0622
336+
337+
def AddKeys(self, key_list):
338+
"""Mark additional columns as being part of the superkey.
339+
Supplements the Keys already extracted from the FSM template.
340+
Useful when adding new columns to existing tables.
341+
Note: This will impact attempts to further 'extend' the table as the
342+
superkey must be common between tables for successful extension.
343+
Args:
344+
key_list: list of header entries to be included in the superkey.
345+
Raises:
346+
KeyError: If any entry in list is not a valid header entry.
347+
"""
348+
349+
for keyname in key_list:
350+
if keyname not in self.header:
351+
raise KeyError("'%s'" % keyname)
352+
353+
self._keys = self._keys.union(set(key_list))
354+
355+
@property
356+
def superkey(self):
357+
"""Returns a set of column names that together constitute the superkey."""
358+
sorted_list = []
359+
for header in self.header:
360+
if header in self._keys:
361+
sorted_list.append(header)
362+
return sorted_list
363+
364+
def KeyValue(self, row=None):
365+
"""Returns the super key value for the row."""
366+
if not row:
367+
if self._iterator:
368+
# If we are inside an iterator use current row iteration.
369+
row = self[self._iterator]
370+
else:
371+
row = self.row
372+
# If no superkey then use row number.
373+
if not self.superkey:
374+
return ['%s' % row.row]
375+
376+
sorted_list = []
377+
for header in self.header:
378+
if header in self.superkey:
379+
sorted_list.append(row[header])
380+
return sorted_list

0 commit comments

Comments
 (0)