1*9c5db199SXin Li#!/usr/bin/python3 2*9c5db199SXin Li# 3*9c5db199SXin Li# Copyright 2010 Google Inc. All Rights Reserved. 4*9c5db199SXin Li# 5*9c5db199SXin Li# Licensed under the Apache License, Version 2.0 (the "License"); 6*9c5db199SXin Li# you may not use this file except in compliance with the License. 7*9c5db199SXin Li# You may obtain a copy of the License at 8*9c5db199SXin Li# 9*9c5db199SXin Li# http://www.apache.org/licenses/LICENSE-2.0 10*9c5db199SXin Li# 11*9c5db199SXin Li# Unless required by applicable law or agreed to in writing, software 12*9c5db199SXin Li# distributed under the License is distributed on an "AS IS" BASIS, 13*9c5db199SXin Li# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 14*9c5db199SXin Li# implied. See the License for the specific language governing 15*9c5db199SXin Li# permissions and limitations under the License. 16*9c5db199SXin Li 17*9c5db199SXin Li"""Template based text parser. 18*9c5db199SXin Li 19*9c5db199SXin LiThis module implements a parser, intended to be used for converting 20*9c5db199SXin Lihuman readable text, such as command output from a router CLI, into 21*9c5db199SXin Lia list of records, containing values extracted from the input text. 22*9c5db199SXin Li 23*9c5db199SXin LiA simple template language is used to describe a state machine to 24*9c5db199SXin Liparse a specific type of text input, returning a record of values 25*9c5db199SXin Lifor each input entity. 26*9c5db199SXin Li 27*9c5db199SXin LiImport it to ~/file/client/common_lib/cros/. 28*9c5db199SXin Li""" 29*9c5db199SXin Lifrom __future__ import absolute_import 30*9c5db199SXin Lifrom __future__ import division 31*9c5db199SXin Lifrom __future__ import print_function 32*9c5db199SXin Li 33*9c5db199SXin Li__version__ = '0.3.2' 34*9c5db199SXin Li 35*9c5db199SXin Liimport getopt 36*9c5db199SXin Liimport inspect 37*9c5db199SXin Liimport re 38*9c5db199SXin Liimport string 39*9c5db199SXin Liimport sys 40*9c5db199SXin Li 41*9c5db199SXin Li 42*9c5db199SXin Liclass Error(Exception): 43*9c5db199SXin Li """Base class for errors.""" 44*9c5db199SXin Li 45*9c5db199SXin Li 46*9c5db199SXin Liclass Usage(Exception): 47*9c5db199SXin Li """Error in command line execution.""" 48*9c5db199SXin Li 49*9c5db199SXin Li 50*9c5db199SXin Liclass TextFSMError(Error): 51*9c5db199SXin Li """Error in the FSM state execution.""" 52*9c5db199SXin Li 53*9c5db199SXin Li 54*9c5db199SXin Liclass TextFSMTemplateError(Error): 55*9c5db199SXin Li """Errors while parsing templates.""" 56*9c5db199SXin Li 57*9c5db199SXin Li 58*9c5db199SXin Li# The below exceptions are internal state change triggers 59*9c5db199SXin Li# and not used as Errors. 60*9c5db199SXin Liclass FSMAction(Exception): 61*9c5db199SXin Li """Base class for actions raised with the FSM.""" 62*9c5db199SXin Li 63*9c5db199SXin Li 64*9c5db199SXin Liclass SkipRecord(FSMAction): 65*9c5db199SXin Li """Indicate a record is to be skipped.""" 66*9c5db199SXin Li 67*9c5db199SXin Li 68*9c5db199SXin Liclass SkipValue(FSMAction): 69*9c5db199SXin Li """Indicate a value is to be skipped.""" 70*9c5db199SXin Li 71*9c5db199SXin Li 72*9c5db199SXin Liclass TextFSMOptions(object): 73*9c5db199SXin Li """Class containing all valid TextFSMValue options. 74*9c5db199SXin Li 75*9c5db199SXin Li Each nested class here represents a TextFSM option. The format 76*9c5db199SXin Li is "option<name>". 77*9c5db199SXin Li Each class may override any of the methods inside the OptionBase class. 78*9c5db199SXin Li 79*9c5db199SXin Li A user of this module can extend options by subclassing 80*9c5db199SXin Li TextFSMOptionsBase, adding the new option class(es), then passing 81*9c5db199SXin Li that new class to the TextFSM constructor with the 'option_class' 82*9c5db199SXin Li argument. 83*9c5db199SXin Li """ 84*9c5db199SXin Li 85*9c5db199SXin Li class OptionBase(object): 86*9c5db199SXin Li """Factory methods for option class. 87*9c5db199SXin Li 88*9c5db199SXin Li Attributes: 89*9c5db199SXin Li value: A TextFSMValue, the parent Value. 90*9c5db199SXin Li """ 91*9c5db199SXin Li 92*9c5db199SXin Li def __init__(self, value): 93*9c5db199SXin Li self.value = value 94*9c5db199SXin Li 95*9c5db199SXin Li @property 96*9c5db199SXin Li def name(self): 97*9c5db199SXin Li return self.__class__.__name__.replace('option', '') 98*9c5db199SXin Li 99*9c5db199SXin Li def OnCreateOptions(self): 100*9c5db199SXin Li """Called after all options have been parsed for a Value.""" 101*9c5db199SXin Li 102*9c5db199SXin Li def OnClearVar(self): 103*9c5db199SXin Li """Called when value has been cleared.""" 104*9c5db199SXin Li 105*9c5db199SXin Li def OnClearAllVar(self): 106*9c5db199SXin Li """Called when a value has clearalled.""" 107*9c5db199SXin Li 108*9c5db199SXin Li def OnAssignVar(self): 109*9c5db199SXin Li """Called when a matched value is being assigned.""" 110*9c5db199SXin Li 111*9c5db199SXin Li def OnGetValue(self): 112*9c5db199SXin Li """Called when the value name is being requested.""" 113*9c5db199SXin Li 114*9c5db199SXin Li def OnSaveRecord(self): 115*9c5db199SXin Li """Called just prior to a record being committed.""" 116*9c5db199SXin Li 117*9c5db199SXin Li @classmethod 118*9c5db199SXin Li def ValidOptions(cls): 119*9c5db199SXin Li """Returns a list of valid option names.""" 120*9c5db199SXin Li valid_options = [] 121*9c5db199SXin Li for obj_name in dir(cls): 122*9c5db199SXin Li obj = getattr(cls, obj_name) 123*9c5db199SXin Li if inspect.isclass(obj) and issubclass(obj, cls.OptionBase): 124*9c5db199SXin Li valid_options.append(obj_name) 125*9c5db199SXin Li return valid_options 126*9c5db199SXin Li 127*9c5db199SXin Li @classmethod 128*9c5db199SXin Li def GetOption(cls, name): 129*9c5db199SXin Li """Returns the class of the requested option name.""" 130*9c5db199SXin Li return getattr(cls, name) 131*9c5db199SXin Li 132*9c5db199SXin Li class Required(OptionBase): 133*9c5db199SXin Li """The Value must be non-empty for the row to be recorded.""" 134*9c5db199SXin Li 135*9c5db199SXin Li def OnSaveRecord(self): 136*9c5db199SXin Li if not self.value.value: 137*9c5db199SXin Li raise SkipRecord 138*9c5db199SXin Li 139*9c5db199SXin Li class Filldown(OptionBase): 140*9c5db199SXin Li """Value defaults to the previous line's value.""" 141*9c5db199SXin Li 142*9c5db199SXin Li def OnCreateOptions(self): 143*9c5db199SXin Li self._myvar = None 144*9c5db199SXin Li 145*9c5db199SXin Li def OnAssignVar(self): 146*9c5db199SXin Li self._myvar = self.value.value 147*9c5db199SXin Li 148*9c5db199SXin Li def OnClearVar(self): 149*9c5db199SXin Li self.value.value = self._myvar 150*9c5db199SXin Li 151*9c5db199SXin Li def OnClearAllVar(self): 152*9c5db199SXin Li self._myvar = None 153*9c5db199SXin Li 154*9c5db199SXin Li class Fillup(OptionBase): 155*9c5db199SXin Li """Like Filldown, but upwards until it finds a non-empty entry.""" 156*9c5db199SXin Li 157*9c5db199SXin Li def OnAssignVar(self): 158*9c5db199SXin Li # If value is set, copy up the results table, until we 159*9c5db199SXin Li # see a set item. 160*9c5db199SXin Li if self.value.value: 161*9c5db199SXin Li # Get index of relevant result column. 162*9c5db199SXin Li value_idx = self.value.fsm.values.index(self.value) 163*9c5db199SXin Li # Go up the list from the end until we see a filled value. 164*9c5db199SXin Li # pylint: disable=protected-access 165*9c5db199SXin Li for result in reversed(self.value.fsm._result): 166*9c5db199SXin Li if result[value_idx]: 167*9c5db199SXin Li # Stop when a record has this column already. 168*9c5db199SXin Li break 169*9c5db199SXin Li # Otherwise set the column value. 170*9c5db199SXin Li result[value_idx] = self.value.value 171*9c5db199SXin Li 172*9c5db199SXin Li class Key(OptionBase): 173*9c5db199SXin Li """Value constitutes part of the Key of the record.""" 174*9c5db199SXin Li 175*9c5db199SXin Li class List(OptionBase): 176*9c5db199SXin Li """Value takes the form of a list.""" 177*9c5db199SXin Li 178*9c5db199SXin Li def OnCreateOptions(self): 179*9c5db199SXin Li self.OnClearAllVar() 180*9c5db199SXin Li 181*9c5db199SXin Li def OnAssignVar(self): 182*9c5db199SXin Li self._value.append(self.value.value) 183*9c5db199SXin Li 184*9c5db199SXin Li def OnClearVar(self): 185*9c5db199SXin Li if 'Filldown' not in self.value.OptionNames(): 186*9c5db199SXin Li self._value = [] 187*9c5db199SXin Li 188*9c5db199SXin Li def OnClearAllVar(self): 189*9c5db199SXin Li self._value = [] 190*9c5db199SXin Li 191*9c5db199SXin Li def OnSaveRecord(self): 192*9c5db199SXin Li self.value.value = list(self._value) 193*9c5db199SXin Li 194*9c5db199SXin Li 195*9c5db199SXin Liclass TextFSMValue(object): 196*9c5db199SXin Li """A TextFSM value. 197*9c5db199SXin Li 198*9c5db199SXin Li A value has syntax like: 199*9c5db199SXin Li 200*9c5db199SXin Li 'Value Filldown,Required helloworld (.*)' 201*9c5db199SXin Li 202*9c5db199SXin Li Where 'Value' is a keyword. 203*9c5db199SXin Li 'Filldown' and 'Required' are options. 204*9c5db199SXin Li 'helloworld' is the value name. 205*9c5db199SXin Li '(.*) is the regular expression to match in the input data. 206*9c5db199SXin Li 207*9c5db199SXin Li Attributes: 208*9c5db199SXin Li max_name_len: (int), maximum character length os a variable name. 209*9c5db199SXin Li name: (str), Name of the value. 210*9c5db199SXin Li options: (list), A list of current Value Options. 211*9c5db199SXin Li regex: (str), Regex which the value is matched on. 212*9c5db199SXin Li template: (str), regexp with named groups added. 213*9c5db199SXin Li fsm: A TextFSMBase(), the containing FSM. 214*9c5db199SXin Li value: (str), the current value. 215*9c5db199SXin Li """ 216*9c5db199SXin Li # The class which contains valid options. 217*9c5db199SXin Li 218*9c5db199SXin Li def __init__(self, fsm=None, max_name_len=48, options_class=None): 219*9c5db199SXin Li """Initialise a new TextFSMValue.""" 220*9c5db199SXin Li self.max_name_len = max_name_len 221*9c5db199SXin Li self.name = None 222*9c5db199SXin Li self.options = [] 223*9c5db199SXin Li self.regex = None 224*9c5db199SXin Li self.value = None 225*9c5db199SXin Li self.fsm = fsm 226*9c5db199SXin Li self._options_cls = options_class 227*9c5db199SXin Li 228*9c5db199SXin Li def AssignVar(self, value): 229*9c5db199SXin Li """Assign a value to this Value.""" 230*9c5db199SXin Li self.value = value 231*9c5db199SXin Li # Call OnAssignVar on options. 232*9c5db199SXin Li _ = [option.OnAssignVar() for option in self.options] 233*9c5db199SXin Li 234*9c5db199SXin Li def ClearVar(self): 235*9c5db199SXin Li """Clear this Value.""" 236*9c5db199SXin Li self.value = None 237*9c5db199SXin Li # Call OnClearVar on options. 238*9c5db199SXin Li _ = [option.OnClearVar() for option in self.options] 239*9c5db199SXin Li 240*9c5db199SXin Li def ClearAllVar(self): 241*9c5db199SXin Li """Clear this Value.""" 242*9c5db199SXin Li self.value = None 243*9c5db199SXin Li # Call OnClearAllVar on options. 244*9c5db199SXin Li _ = [option.OnClearAllVar() for option in self.options] 245*9c5db199SXin Li 246*9c5db199SXin Li def Header(self): 247*9c5db199SXin Li """Fetch the header name of this Value.""" 248*9c5db199SXin Li # Call OnGetValue on options. 249*9c5db199SXin Li _ = [option.OnGetValue() for option in self.options] 250*9c5db199SXin Li return self.name 251*9c5db199SXin Li 252*9c5db199SXin Li def OptionNames(self): 253*9c5db199SXin Li """Returns a list of option names for this Value.""" 254*9c5db199SXin Li return [option.name for option in self.options] 255*9c5db199SXin Li 256*9c5db199SXin Li def Parse(self, value): 257*9c5db199SXin Li """Parse a 'Value' declaration. 258*9c5db199SXin Li 259*9c5db199SXin Li Args: 260*9c5db199SXin Li value: String line from a template file, must begin with 'Value '. 261*9c5db199SXin Li 262*9c5db199SXin Li Raises: 263*9c5db199SXin Li TextFSMTemplateError: Value declaration contains an error. 264*9c5db199SXin Li 265*9c5db199SXin Li """ 266*9c5db199SXin Li 267*9c5db199SXin Li value_line = value.split(' ') 268*9c5db199SXin Li if len(value_line) < 3: 269*9c5db199SXin Li raise TextFSMTemplateError('Expect at least 3 tokens on line.') 270*9c5db199SXin Li 271*9c5db199SXin Li if not value_line[2].startswith('('): 272*9c5db199SXin Li # Options are present 273*9c5db199SXin Li options = value_line[1] 274*9c5db199SXin Li for option in options.split(','): 275*9c5db199SXin Li self._AddOption(option) 276*9c5db199SXin Li # Call option OnCreateOptions callbacks 277*9c5db199SXin Li _ = [option.OnCreateOptions() for option in self.options] 278*9c5db199SXin Li 279*9c5db199SXin Li self.name = value_line[2] 280*9c5db199SXin Li self.regex = ' '.join(value_line[3:]) 281*9c5db199SXin Li else: 282*9c5db199SXin Li # There were no valid options, so there are no options. 283*9c5db199SXin Li # Treat this argument as the name. 284*9c5db199SXin Li self.name = value_line[1] 285*9c5db199SXin Li self.regex = ' '.join(value_line[2:]) 286*9c5db199SXin Li 287*9c5db199SXin Li if len(self.name) > self.max_name_len: 288*9c5db199SXin Li raise TextFSMTemplateError( 289*9c5db199SXin Li "Invalid Value name '%s' or name too long." % self.name) 290*9c5db199SXin Li 291*9c5db199SXin Li if (not re.match(r'^\(.*\)$', self.regex) or 292*9c5db199SXin Li self.regex.count('(') != self.regex.count(')')): 293*9c5db199SXin Li raise TextFSMTemplateError( 294*9c5db199SXin Li "Value '%s' must be contained within a '()' pair." % self.regex) 295*9c5db199SXin Li 296*9c5db199SXin Li self.template = re.sub(r'^\(', '(?P<%s>' % self.name, self.regex) 297*9c5db199SXin Li 298*9c5db199SXin Li def _AddOption(self, name): 299*9c5db199SXin Li """Add an option to this Value. 300*9c5db199SXin Li 301*9c5db199SXin Li Args: 302*9c5db199SXin Li name: (str), the name of the Option to add. 303*9c5db199SXin Li 304*9c5db199SXin Li Raises: 305*9c5db199SXin Li TextFSMTemplateError: If option is already present or 306*9c5db199SXin Li the option does not exist. 307*9c5db199SXin Li """ 308*9c5db199SXin Li 309*9c5db199SXin Li # Check for duplicate option declaration 310*9c5db199SXin Li if name in [option.name for option in self.options]: 311*9c5db199SXin Li raise TextFSMTemplateError('Duplicate option "%s"' % name) 312*9c5db199SXin Li 313*9c5db199SXin Li # Create the option object 314*9c5db199SXin Li try: 315*9c5db199SXin Li option = self._options_cls.GetOption(name)(self) 316*9c5db199SXin Li except AttributeError: 317*9c5db199SXin Li raise TextFSMTemplateError('Unknown option "%s"' % name) 318*9c5db199SXin Li 319*9c5db199SXin Li self.options.append(option) 320*9c5db199SXin Li 321*9c5db199SXin Li def OnSaveRecord(self): 322*9c5db199SXin Li """Called just prior to a record being committed.""" 323*9c5db199SXin Li _ = [option.OnSaveRecord() for option in self.options] 324*9c5db199SXin Li 325*9c5db199SXin Li def __str__(self): 326*9c5db199SXin Li """Prints out the FSM Value, mimic the input file.""" 327*9c5db199SXin Li 328*9c5db199SXin Li if self.options: 329*9c5db199SXin Li return 'Value %s %s %s' % ( 330*9c5db199SXin Li ','.join(self.OptionNames()), 331*9c5db199SXin Li self.name, 332*9c5db199SXin Li self.regex) 333*9c5db199SXin Li else: 334*9c5db199SXin Li return 'Value %s %s' % (self.name, self.regex) 335*9c5db199SXin Li 336*9c5db199SXin Li 337*9c5db199SXin Liclass CopyableRegexObject(object): 338*9c5db199SXin Li """Like a re.RegexObject, but can be copied.""" 339*9c5db199SXin Li # pylint: disable=C6409 340*9c5db199SXin Li 341*9c5db199SXin Li def __init__(self, pattern): 342*9c5db199SXin Li self.pattern = pattern 343*9c5db199SXin Li self.regex = re.compile(pattern) 344*9c5db199SXin Li 345*9c5db199SXin Li def match(self, *args, **kwargs): 346*9c5db199SXin Li return self.regex.match(*args, **kwargs) 347*9c5db199SXin Li 348*9c5db199SXin Li def sub(self, *args, **kwargs): 349*9c5db199SXin Li return self.regex.sub(*args, **kwargs) 350*9c5db199SXin Li 351*9c5db199SXin Li def __copy__(self): 352*9c5db199SXin Li return CopyableRegexObject(self.pattern) 353*9c5db199SXin Li 354*9c5db199SXin Li def __deepcopy__(self, unused_memo): 355*9c5db199SXin Li return self.__copy__() 356*9c5db199SXin Li 357*9c5db199SXin Li 358*9c5db199SXin Liclass TextFSMRule(object): 359*9c5db199SXin Li """A rule in each FSM state. 360*9c5db199SXin Li 361*9c5db199SXin Li A value has syntax like: 362*9c5db199SXin Li 363*9c5db199SXin Li ^<regexp> -> Next.Record State2 364*9c5db199SXin Li 365*9c5db199SXin Li Where '<regexp>' is a regular expression. 366*9c5db199SXin Li 'Next' is a Line operator. 367*9c5db199SXin Li 'Record' is a Record operator. 368*9c5db199SXin Li 'State2' is the next State. 369*9c5db199SXin Li 370*9c5db199SXin Li Attributes: 371*9c5db199SXin Li match: Regex to match this rule. 372*9c5db199SXin Li regex: match after template substitution. 373*9c5db199SXin Li line_op: Operator on input line on match. 374*9c5db199SXin Li record_op: Operator on output record on match. 375*9c5db199SXin Li new_state: Label to jump to on action 376*9c5db199SXin Li regex_obj: Compiled regex for which the rule matches. 377*9c5db199SXin Li line_num: Integer row number of Value. 378*9c5db199SXin Li """ 379*9c5db199SXin Li # Implicit default is '(regexp) -> Next.NoRecord' 380*9c5db199SXin Li MATCH_ACTION = re.compile(r'(?P<match>.*)(\s->(?P<action>.*))') 381*9c5db199SXin Li 382*9c5db199SXin Li # The structure to the right of the '->'. 383*9c5db199SXin Li LINE_OP = ('Continue', 'Next', 'Error') 384*9c5db199SXin Li RECORD_OP = ('Clear', 'Clearall', 'Record', 'NoRecord') 385*9c5db199SXin Li 386*9c5db199SXin Li # Line operators. 387*9c5db199SXin Li LINE_OP_RE = '(?P<ln_op>%s)' % '|'.join(LINE_OP) 388*9c5db199SXin Li # Record operators. 389*9c5db199SXin Li RECORD_OP_RE = '(?P<rec_op>%s)' % '|'.join(RECORD_OP) 390*9c5db199SXin Li # Line operator with optional record operator. 391*9c5db199SXin Li OPERATOR_RE = r'(%s(\.%s)?)' % (LINE_OP_RE, RECORD_OP_RE) 392*9c5db199SXin Li # New State or 'Error' string. 393*9c5db199SXin Li NEWSTATE_RE = r'(?P<new_state>\w+|\".*\")' 394*9c5db199SXin Li 395*9c5db199SXin Li # Compound operator (line and record) with optional new state. 396*9c5db199SXin Li ACTION_RE = re.compile(r'\s+%s(\s+%s)?$' % (OPERATOR_RE, NEWSTATE_RE)) 397*9c5db199SXin Li # Record operator with optional new state. 398*9c5db199SXin Li ACTION2_RE = re.compile(r'\s+%s(\s+%s)?$' % (RECORD_OP_RE, NEWSTATE_RE)) 399*9c5db199SXin Li # Default operators with optional new state. 400*9c5db199SXin Li ACTION3_RE = re.compile(r'(\s+%s)?$' % (NEWSTATE_RE)) 401*9c5db199SXin Li 402*9c5db199SXin Li def __init__(self, line, line_num=-1, var_map=None): 403*9c5db199SXin Li """Initialise a new rule object. 404*9c5db199SXin Li 405*9c5db199SXin Li Args: 406*9c5db199SXin Li line: (str), a template rule line to parse. 407*9c5db199SXin Li line_num: (int), Optional line reference included in error reporting. 408*9c5db199SXin Li var_map: Map for template (${var}) substitutions. 409*9c5db199SXin Li 410*9c5db199SXin Li Raises: 411*9c5db199SXin Li TextFSMTemplateError: If 'line' is not a valid format for a Value entry. 412*9c5db199SXin Li """ 413*9c5db199SXin Li self.match = '' 414*9c5db199SXin Li self.regex = '' 415*9c5db199SXin Li self.regex_obj = None 416*9c5db199SXin Li self.line_op = '' # Equivalent to 'Next'. 417*9c5db199SXin Li self.record_op = '' # Equivalent to 'NoRecord'. 418*9c5db199SXin Li self.new_state = '' # Equivalent to current state. 419*9c5db199SXin Li self.line_num = line_num 420*9c5db199SXin Li 421*9c5db199SXin Li line = line.strip() 422*9c5db199SXin Li if not line: 423*9c5db199SXin Li raise TextFSMTemplateError('Null data in FSMRule. Line: %s' 424*9c5db199SXin Li % self.line_num) 425*9c5db199SXin Li 426*9c5db199SXin Li # Is there '->' action present. 427*9c5db199SXin Li match_action = self.MATCH_ACTION.match(line) 428*9c5db199SXin Li if match_action: 429*9c5db199SXin Li self.match = match_action.group('match') 430*9c5db199SXin Li else: 431*9c5db199SXin Li self.match = line 432*9c5db199SXin Li 433*9c5db199SXin Li # Replace ${varname} entries. 434*9c5db199SXin Li self.regex = self.match 435*9c5db199SXin Li if var_map: 436*9c5db199SXin Li try: 437*9c5db199SXin Li self.regex = string.Template(self.match).substitute(var_map) 438*9c5db199SXin Li except (ValueError, KeyError): 439*9c5db199SXin Li raise TextFSMTemplateError( 440*9c5db199SXin Li "Duplicate or invalid variable substitution: '%s'. Line: %s." % 441*9c5db199SXin Li (self.match, self.line_num)) 442*9c5db199SXin Li 443*9c5db199SXin Li try: 444*9c5db199SXin Li # Work around a regression in Python 2.6 that makes RE Objects uncopyable. 445*9c5db199SXin Li self.regex_obj = CopyableRegexObject(self.regex) 446*9c5db199SXin Li except re.error: 447*9c5db199SXin Li raise TextFSMTemplateError( 448*9c5db199SXin Li "Invalid regular expression: '%s'. Line: %s." % 449*9c5db199SXin Li (self.regex, self.line_num)) 450*9c5db199SXin Li 451*9c5db199SXin Li # No '->' present, so done. 452*9c5db199SXin Li if not match_action: 453*9c5db199SXin Li return 454*9c5db199SXin Li 455*9c5db199SXin Li # Attempt to match line.record operation. 456*9c5db199SXin Li action_re = self.ACTION_RE.match(match_action.group('action')) 457*9c5db199SXin Li if not action_re: 458*9c5db199SXin Li # Attempt to match record operation. 459*9c5db199SXin Li action_re = self.ACTION2_RE.match(match_action.group('action')) 460*9c5db199SXin Li if not action_re: 461*9c5db199SXin Li # Math implicit defaults with an optional new state. 462*9c5db199SXin Li action_re = self.ACTION3_RE.match(match_action.group('action')) 463*9c5db199SXin Li if not action_re: 464*9c5db199SXin Li # Last attempt, match an optional new state only. 465*9c5db199SXin Li raise TextFSMTemplateError("Badly formatted rule '%s'. Line: %s." % 466*9c5db199SXin Li (line, self.line_num)) 467*9c5db199SXin Li 468*9c5db199SXin Li # We have an Line operator. 469*9c5db199SXin Li if 'ln_op' in action_re.groupdict() and action_re.group('ln_op'): 470*9c5db199SXin Li self.line_op = action_re.group('ln_op') 471*9c5db199SXin Li 472*9c5db199SXin Li # We have a record operator. 473*9c5db199SXin Li if 'rec_op' in action_re.groupdict() and action_re.group('rec_op'): 474*9c5db199SXin Li self.record_op = action_re.group('rec_op') 475*9c5db199SXin Li 476*9c5db199SXin Li # A new state was specified. 477*9c5db199SXin Li if 'new_state' in action_re.groupdict() and action_re.group('new_state'): 478*9c5db199SXin Li self.new_state = action_re.group('new_state') 479*9c5db199SXin Li 480*9c5db199SXin Li # Only 'Next' (or implicit 'Next') line operator can have a new_state. 481*9c5db199SXin Li # But we allow error to have one as a warning message so we are left 482*9c5db199SXin Li # checking that Continue does not. 483*9c5db199SXin Li if self.line_op == 'Continue' and self.new_state: 484*9c5db199SXin Li raise TextFSMTemplateError( 485*9c5db199SXin Li "Action '%s' with new state %s specified. Line: %s." 486*9c5db199SXin Li % (self.line_op, self.new_state, self.line_num)) 487*9c5db199SXin Li 488*9c5db199SXin Li # Check that an error message is present only with the 'Error' operator. 489*9c5db199SXin Li if self.line_op != 'Error' and self.new_state: 490*9c5db199SXin Li if not re.match(r'\w+', self.new_state): 491*9c5db199SXin Li raise TextFSMTemplateError( 492*9c5db199SXin Li 'Alphanumeric characters only in state names. Line: %s.' 493*9c5db199SXin Li % (self.line_num)) 494*9c5db199SXin Li 495*9c5db199SXin Li def __str__(self): 496*9c5db199SXin Li """Prints out the FSM Rule, mimic the input file.""" 497*9c5db199SXin Li 498*9c5db199SXin Li operation = '' 499*9c5db199SXin Li if self.line_op and self.record_op: 500*9c5db199SXin Li operation = '.' 501*9c5db199SXin Li 502*9c5db199SXin Li operation = '%s%s%s' % (self.line_op, operation, self.record_op) 503*9c5db199SXin Li 504*9c5db199SXin Li if operation and self.new_state: 505*9c5db199SXin Li new_state = ' ' + self.new_state 506*9c5db199SXin Li else: 507*9c5db199SXin Li new_state = self.new_state 508*9c5db199SXin Li 509*9c5db199SXin Li # Print with implicit defaults. 510*9c5db199SXin Li if not (operation or new_state): 511*9c5db199SXin Li return ' %s' % self.match 512*9c5db199SXin Li 513*9c5db199SXin Li # Non defaults. 514*9c5db199SXin Li return ' %s -> %s%s' % (self.match, operation, new_state) 515*9c5db199SXin Li 516*9c5db199SXin Li 517*9c5db199SXin Liclass TextFSM(object): 518*9c5db199SXin Li """Parses template and creates Finite State Machine (FSM). 519*9c5db199SXin Li 520*9c5db199SXin Li Attributes: 521*9c5db199SXin Li states: (str), Dictionary of FSMState objects. 522*9c5db199SXin Li values: (str), List of FSMVariables. 523*9c5db199SXin Li value_map: (map), For substituting values for names in the expressions. 524*9c5db199SXin Li header: Ordered list of values. 525*9c5db199SXin Li state_list: Ordered list of valid states. 526*9c5db199SXin Li """ 527*9c5db199SXin Li # Variable and State name length. 528*9c5db199SXin Li MAX_NAME_LEN = 48 529*9c5db199SXin Li comment_regex = re.compile(r'^\s*#') 530*9c5db199SXin Li state_name_re = re.compile(r'^(\w+)$') 531*9c5db199SXin Li _DEFAULT_OPTIONS = TextFSMOptions 532*9c5db199SXin Li 533*9c5db199SXin Li def __init__(self, template, options_class=_DEFAULT_OPTIONS): 534*9c5db199SXin Li """Initialises and also parses the template file.""" 535*9c5db199SXin Li 536*9c5db199SXin Li self._options_cls = options_class 537*9c5db199SXin Li self.states = {} 538*9c5db199SXin Li # Track order of state definitions. 539*9c5db199SXin Li self.state_list = [] 540*9c5db199SXin Li self.values = [] 541*9c5db199SXin Li self.value_map = {} 542*9c5db199SXin Li # Track where we are for error reporting. 543*9c5db199SXin Li self._line_num = 0 544*9c5db199SXin Li # Run FSM in this state 545*9c5db199SXin Li self._cur_state = None 546*9c5db199SXin Li # Name of the current state. 547*9c5db199SXin Li self._cur_state_name = None 548*9c5db199SXin Li 549*9c5db199SXin Li # Read and parse FSM definition. 550*9c5db199SXin Li # Restore the file pointer once done. 551*9c5db199SXin Li try: 552*9c5db199SXin Li self._Parse(template) 553*9c5db199SXin Li finally: 554*9c5db199SXin Li template.seek(0) 555*9c5db199SXin Li 556*9c5db199SXin Li # Initialise starting data. 557*9c5db199SXin Li self.Reset() 558*9c5db199SXin Li 559*9c5db199SXin Li def __str__(self): 560*9c5db199SXin Li """Returns the FSM template, mimic the input file.""" 561*9c5db199SXin Li 562*9c5db199SXin Li result = '\n'.join([str(value) for value in self.values]) 563*9c5db199SXin Li result += '\n' 564*9c5db199SXin Li 565*9c5db199SXin Li for state in self.state_list: 566*9c5db199SXin Li result += '\n%s\n' % state 567*9c5db199SXin Li state_rules = '\n'.join([str(rule) for rule in self.states[state]]) 568*9c5db199SXin Li if state_rules: 569*9c5db199SXin Li result += state_rules + '\n' 570*9c5db199SXin Li 571*9c5db199SXin Li return result 572*9c5db199SXin Li 573*9c5db199SXin Li def Reset(self): 574*9c5db199SXin Li """Preserves FSM but resets starting state and current record.""" 575*9c5db199SXin Li 576*9c5db199SXin Li # Current state is Start state. 577*9c5db199SXin Li self._cur_state = self.states['Start'] 578*9c5db199SXin Li self._cur_state_name = 'Start' 579*9c5db199SXin Li 580*9c5db199SXin Li # Clear table of results and current record. 581*9c5db199SXin Li self._result = [] 582*9c5db199SXin Li self._ClearAllRecord() 583*9c5db199SXin Li 584*9c5db199SXin Li @property 585*9c5db199SXin Li def header(self): 586*9c5db199SXin Li """Returns header.""" 587*9c5db199SXin Li return self._GetHeader() 588*9c5db199SXin Li 589*9c5db199SXin Li def _GetHeader(self): 590*9c5db199SXin Li """Returns header.""" 591*9c5db199SXin Li header = [] 592*9c5db199SXin Li for value in self.values: 593*9c5db199SXin Li try: 594*9c5db199SXin Li header.append(value.Header()) 595*9c5db199SXin Li except SkipValue: 596*9c5db199SXin Li continue 597*9c5db199SXin Li return header 598*9c5db199SXin Li 599*9c5db199SXin Li def _GetValue(self, name): 600*9c5db199SXin Li """Returns the TextFSMValue object natching the requested name.""" 601*9c5db199SXin Li for value in self.values: 602*9c5db199SXin Li if value.name == name: 603*9c5db199SXin Li return value 604*9c5db199SXin Li 605*9c5db199SXin Li def _AppendRecord(self): 606*9c5db199SXin Li """Adds current record to result if well formed.""" 607*9c5db199SXin Li 608*9c5db199SXin Li # If no Values then don't output. 609*9c5db199SXin Li if not self.values: 610*9c5db199SXin Li return 611*9c5db199SXin Li 612*9c5db199SXin Li cur_record = [] 613*9c5db199SXin Li for value in self.values: 614*9c5db199SXin Li try: 615*9c5db199SXin Li value.OnSaveRecord() 616*9c5db199SXin Li except SkipRecord: 617*9c5db199SXin Li self._ClearRecord() 618*9c5db199SXin Li return 619*9c5db199SXin Li except SkipValue: 620*9c5db199SXin Li continue 621*9c5db199SXin Li 622*9c5db199SXin Li # Build current record into a list. 623*9c5db199SXin Li cur_record.append(value.value) 624*9c5db199SXin Li 625*9c5db199SXin Li # If no Values in template or whole record is empty then don't output. 626*9c5db199SXin Li if len(cur_record) == (cur_record.count(None) + cur_record.count([])): 627*9c5db199SXin Li return 628*9c5db199SXin Li 629*9c5db199SXin Li # Replace any 'None' entries with null string ''. 630*9c5db199SXin Li while None in cur_record: 631*9c5db199SXin Li cur_record[cur_record.index(None)] = '' 632*9c5db199SXin Li 633*9c5db199SXin Li self._result.append(cur_record) 634*9c5db199SXin Li self._ClearRecord() 635*9c5db199SXin Li 636*9c5db199SXin Li def _Parse(self, template): 637*9c5db199SXin Li """Parses template file for FSM structure. 638*9c5db199SXin Li 639*9c5db199SXin Li Args: 640*9c5db199SXin Li template: Valid template file. 641*9c5db199SXin Li 642*9c5db199SXin Li Raises: 643*9c5db199SXin Li TextFSMTemplateError: If template file syntax is invalid. 644*9c5db199SXin Li """ 645*9c5db199SXin Li 646*9c5db199SXin Li if not template: 647*9c5db199SXin Li raise TextFSMTemplateError('Null template.') 648*9c5db199SXin Li 649*9c5db199SXin Li # Parse header with Variables. 650*9c5db199SXin Li self._ParseFSMVariables(template) 651*9c5db199SXin Li 652*9c5db199SXin Li # Parse States. 653*9c5db199SXin Li while self._ParseFSMState(template): 654*9c5db199SXin Li pass 655*9c5db199SXin Li 656*9c5db199SXin Li # Validate destination states. 657*9c5db199SXin Li self._ValidateFSM() 658*9c5db199SXin Li 659*9c5db199SXin Li def _ParseFSMVariables(self, template): 660*9c5db199SXin Li """Extracts Variables from start of template file. 661*9c5db199SXin Li 662*9c5db199SXin Li Values are expected as a contiguous block at the head of the file. 663*9c5db199SXin Li These will be line separated from the State definitions that follow. 664*9c5db199SXin Li 665*9c5db199SXin Li Args: 666*9c5db199SXin Li template: Valid template file, with Value definitions at the top. 667*9c5db199SXin Li 668*9c5db199SXin Li Raises: 669*9c5db199SXin Li TextFSMTemplateError: If syntax or semantic errors are found. 670*9c5db199SXin Li """ 671*9c5db199SXin Li 672*9c5db199SXin Li self.values = [] 673*9c5db199SXin Li 674*9c5db199SXin Li for line in template: 675*9c5db199SXin Li self._line_num += 1 676*9c5db199SXin Li line = line.rstrip() 677*9c5db199SXin Li 678*9c5db199SXin Li # Blank line signifies end of Value definitions. 679*9c5db199SXin Li if not line: 680*9c5db199SXin Li return 681*9c5db199SXin Li 682*9c5db199SXin Li # Skip commented lines. 683*9c5db199SXin Li if self.comment_regex.match(line): 684*9c5db199SXin Li continue 685*9c5db199SXin Li 686*9c5db199SXin Li if line.startswith('Value '): 687*9c5db199SXin Li try: 688*9c5db199SXin Li value = TextFSMValue( 689*9c5db199SXin Li fsm=self, max_name_len=self.MAX_NAME_LEN, 690*9c5db199SXin Li options_class=self._options_cls) 691*9c5db199SXin Li value.Parse(line) 692*9c5db199SXin Li except TextFSMTemplateError as error: 693*9c5db199SXin Li raise TextFSMTemplateError('%s Line %s.' % (error, self._line_num)) 694*9c5db199SXin Li 695*9c5db199SXin Li if value.name in self.header: 696*9c5db199SXin Li raise TextFSMTemplateError( 697*9c5db199SXin Li "Duplicate declarations for Value '%s'. Line: %s." 698*9c5db199SXin Li % (value.name, self._line_num)) 699*9c5db199SXin Li 700*9c5db199SXin Li try: 701*9c5db199SXin Li self._ValidateOptions(value) 702*9c5db199SXin Li except TextFSMTemplateError as error: 703*9c5db199SXin Li raise TextFSMTemplateError('%s Line %s.' % (error, self._line_num)) 704*9c5db199SXin Li 705*9c5db199SXin Li self.values.append(value) 706*9c5db199SXin Li self.value_map[value.name] = value.template 707*9c5db199SXin Li # The line has text but without the 'Value ' prefix. 708*9c5db199SXin Li elif not self.values: 709*9c5db199SXin Li raise TextFSMTemplateError('No Value definitions found.') 710*9c5db199SXin Li else: 711*9c5db199SXin Li raise TextFSMTemplateError( 712*9c5db199SXin Li 'Expected blank line after last Value entry. Line: %s.' 713*9c5db199SXin Li % (self._line_num)) 714*9c5db199SXin Li 715*9c5db199SXin Li def _ValidateOptions(self, value): 716*9c5db199SXin Li """Checks that combination of Options is valid.""" 717*9c5db199SXin Li # Always passes in base class. 718*9c5db199SXin Li pass 719*9c5db199SXin Li 720*9c5db199SXin Li def _ParseFSMState(self, template): 721*9c5db199SXin Li """Extracts State and associated Rules from body of template file. 722*9c5db199SXin Li 723*9c5db199SXin Li After the Value definitions the remainder of the template is 724*9c5db199SXin Li state definitions. The routine is expected to be called iteratively 725*9c5db199SXin Li until no more states remain - indicated by returning None. 726*9c5db199SXin Li 727*9c5db199SXin Li The routine checks that the state names are a well formed string, do 728*9c5db199SXin Li not clash with reserved names and are unique. 729*9c5db199SXin Li 730*9c5db199SXin Li Args: 731*9c5db199SXin Li template: Valid template file after Value definitions 732*9c5db199SXin Li have already been read. 733*9c5db199SXin Li 734*9c5db199SXin Li Returns: 735*9c5db199SXin Li Name of the state parsed from file. None otherwise. 736*9c5db199SXin Li 737*9c5db199SXin Li Raises: 738*9c5db199SXin Li TextFSMTemplateError: If any state definitions are invalid. 739*9c5db199SXin Li """ 740*9c5db199SXin Li 741*9c5db199SXin Li if not template: 742*9c5db199SXin Li return 743*9c5db199SXin Li 744*9c5db199SXin Li state_name = '' 745*9c5db199SXin Li # Strip off extra white space lines (including comments). 746*9c5db199SXin Li for line in template: 747*9c5db199SXin Li self._line_num += 1 748*9c5db199SXin Li line = line.rstrip() 749*9c5db199SXin Li 750*9c5db199SXin Li # First line is state definition 751*9c5db199SXin Li if line and not self.comment_regex.match(line): 752*9c5db199SXin Li # Ensure statename has valid syntax and is not a reserved word. 753*9c5db199SXin Li if (not self.state_name_re.match(line) or 754*9c5db199SXin Li len(line) > self.MAX_NAME_LEN or 755*9c5db199SXin Li line in TextFSMRule.LINE_OP or 756*9c5db199SXin Li line in TextFSMRule.RECORD_OP): 757*9c5db199SXin Li raise TextFSMTemplateError("Invalid state name: '%s'. Line: %s" 758*9c5db199SXin Li % (line, self._line_num)) 759*9c5db199SXin Li 760*9c5db199SXin Li state_name = line 761*9c5db199SXin Li if state_name in self.states: 762*9c5db199SXin Li raise TextFSMTemplateError("Duplicate state name: '%s'. Line: %s" 763*9c5db199SXin Li % (line, self._line_num)) 764*9c5db199SXin Li self.states[state_name] = [] 765*9c5db199SXin Li self.state_list.append(state_name) 766*9c5db199SXin Li break 767*9c5db199SXin Li 768*9c5db199SXin Li # Parse each rule in the state. 769*9c5db199SXin Li for line in template: 770*9c5db199SXin Li self._line_num += 1 771*9c5db199SXin Li line = line.rstrip() 772*9c5db199SXin Li 773*9c5db199SXin Li # Finish rules processing on blank line. 774*9c5db199SXin Li if not line: 775*9c5db199SXin Li break 776*9c5db199SXin Li 777*9c5db199SXin Li if self.comment_regex.match(line): 778*9c5db199SXin Li continue 779*9c5db199SXin Li 780*9c5db199SXin Li # A rule within a state, starts with whitespace 781*9c5db199SXin Li if not (line.startswith(' ^') or line.startswith('\t^')): 782*9c5db199SXin Li raise TextFSMTemplateError( 783*9c5db199SXin Li "Missing white space or carat ('^') before rule. Line: %s" % 784*9c5db199SXin Li self._line_num) 785*9c5db199SXin Li 786*9c5db199SXin Li self.states[state_name].append( 787*9c5db199SXin Li TextFSMRule(line, self._line_num, self.value_map)) 788*9c5db199SXin Li 789*9c5db199SXin Li return state_name 790*9c5db199SXin Li 791*9c5db199SXin Li def _ValidateFSM(self): 792*9c5db199SXin Li """Checks state names and destinations for validity. 793*9c5db199SXin Li 794*9c5db199SXin Li Each destination state must exist, be a valid name and 795*9c5db199SXin Li not be a reserved name. 796*9c5db199SXin Li There must be a 'Start' state and if 'EOF' or 'End' states are specified, 797*9c5db199SXin Li they must be empty. 798*9c5db199SXin Li 799*9c5db199SXin Li Returns: 800*9c5db199SXin Li True if FSM is valid. 801*9c5db199SXin Li 802*9c5db199SXin Li Raises: 803*9c5db199SXin Li TextFSMTemplateError: If any state definitions are invalid. 804*9c5db199SXin Li """ 805*9c5db199SXin Li 806*9c5db199SXin Li # Must have 'Start' state. 807*9c5db199SXin Li if 'Start' not in self.states: 808*9c5db199SXin Li raise TextFSMTemplateError("Missing state 'Start'.") 809*9c5db199SXin Li 810*9c5db199SXin Li # 'End/EOF' state (if specified) must be empty. 811*9c5db199SXin Li if self.states.get('End'): 812*9c5db199SXin Li raise TextFSMTemplateError("Non-Empty 'End' state.") 813*9c5db199SXin Li 814*9c5db199SXin Li if self.states.get('EOF'): 815*9c5db199SXin Li raise TextFSMTemplateError("Non-Empty 'EOF' state.") 816*9c5db199SXin Li 817*9c5db199SXin Li # Remove 'End' state. 818*9c5db199SXin Li if 'End' in self.states: 819*9c5db199SXin Li del self.states['End'] 820*9c5db199SXin Li self.state_list.remove('End') 821*9c5db199SXin Li 822*9c5db199SXin Li # Ensure jump states are all valid. 823*9c5db199SXin Li for state in self.states: 824*9c5db199SXin Li for rule in self.states[state]: 825*9c5db199SXin Li if rule.line_op == 'Error': 826*9c5db199SXin Li continue 827*9c5db199SXin Li 828*9c5db199SXin Li if not rule.new_state or rule.new_state in ('End', 'EOF'): 829*9c5db199SXin Li continue 830*9c5db199SXin Li 831*9c5db199SXin Li if rule.new_state not in self.states: 832*9c5db199SXin Li raise TextFSMTemplateError( 833*9c5db199SXin Li "State '%s' not found, referenced in state '%s'" % 834*9c5db199SXin Li (rule.new_state, state)) 835*9c5db199SXin Li 836*9c5db199SXin Li return True 837*9c5db199SXin Li 838*9c5db199SXin Li def ParseText(self, text, eof=True): 839*9c5db199SXin Li """Passes CLI output through FSM and returns list of tuples. 840*9c5db199SXin Li 841*9c5db199SXin Li First tuple is the header, every subsequent tuple is a row. 842*9c5db199SXin Li 843*9c5db199SXin Li Args: 844*9c5db199SXin Li text: (str), Text to parse with embedded newlines. 845*9c5db199SXin Li eof: (boolean), Set to False if we are parsing only part of the file. 846*9c5db199SXin Li Suppresses triggering EOF state. 847*9c5db199SXin Li 848*9c5db199SXin Li Raises: 849*9c5db199SXin Li TextFSMError: An error occurred within the FSM. 850*9c5db199SXin Li 851*9c5db199SXin Li Returns: 852*9c5db199SXin Li List of Lists. 853*9c5db199SXin Li """ 854*9c5db199SXin Li 855*9c5db199SXin Li lines = [] 856*9c5db199SXin Li if text: 857*9c5db199SXin Li lines = text.splitlines() 858*9c5db199SXin Li 859*9c5db199SXin Li for line in lines: 860*9c5db199SXin Li self._CheckLine(line) 861*9c5db199SXin Li if self._cur_state_name in ('End', 'EOF'): 862*9c5db199SXin Li break 863*9c5db199SXin Li 864*9c5db199SXin Li if self._cur_state_name != 'End' and 'EOF' not in self.states and eof: 865*9c5db199SXin Li # Implicit EOF performs Next.Record operation. 866*9c5db199SXin Li # Suppressed if Null EOF state is instantiated. 867*9c5db199SXin Li self._AppendRecord() 868*9c5db199SXin Li 869*9c5db199SXin Li return self._result 870*9c5db199SXin Li 871*9c5db199SXin Li def _CheckLine(self, line): 872*9c5db199SXin Li """Passes the line through each rule until a match is made. 873*9c5db199SXin Li 874*9c5db199SXin Li Args: 875*9c5db199SXin Li line: A string, the current input line. 876*9c5db199SXin Li """ 877*9c5db199SXin Li for rule in self._cur_state: 878*9c5db199SXin Li matched = self._CheckRule(rule, line) 879*9c5db199SXin Li if matched: 880*9c5db199SXin Li for value in matched.groupdict(): 881*9c5db199SXin Li self._AssignVar(matched, value) 882*9c5db199SXin Li 883*9c5db199SXin Li if self._Operations(rule): 884*9c5db199SXin Li # Not a Continue so check for state transition. 885*9c5db199SXin Li if rule.new_state: 886*9c5db199SXin Li if rule.new_state not in ('End', 'EOF'): 887*9c5db199SXin Li self._cur_state = self.states[rule.new_state] 888*9c5db199SXin Li self._cur_state_name = rule.new_state 889*9c5db199SXin Li break 890*9c5db199SXin Li 891*9c5db199SXin Li def _CheckRule(self, rule, line): 892*9c5db199SXin Li """Check a line against the given rule. 893*9c5db199SXin Li 894*9c5db199SXin Li This is a separate method so that it can be overridden by 895*9c5db199SXin Li a debugging tool. 896*9c5db199SXin Li 897*9c5db199SXin Li Args: 898*9c5db199SXin Li rule: A TextFSMRule(), the rule to check. 899*9c5db199SXin Li line: A str, the line to check. 900*9c5db199SXin Li 901*9c5db199SXin Li Returns: 902*9c5db199SXin Li A regex match object. 903*9c5db199SXin Li """ 904*9c5db199SXin Li return rule.regex_obj.match(line) 905*9c5db199SXin Li 906*9c5db199SXin Li def _AssignVar(self, matched, value): 907*9c5db199SXin Li """Assigns variable into current record from a matched rule. 908*9c5db199SXin Li 909*9c5db199SXin Li If a record entry is a list then append, otherwise values are replaced. 910*9c5db199SXin Li 911*9c5db199SXin Li Args: 912*9c5db199SXin Li matched: (regexp.match) Named group for each matched value. 913*9c5db199SXin Li value: (str) The matched value. 914*9c5db199SXin Li """ 915*9c5db199SXin Li self._GetValue(value).AssignVar(matched.group(value)) 916*9c5db199SXin Li 917*9c5db199SXin Li def _Operations(self, rule): 918*9c5db199SXin Li """Operators on the data record. 919*9c5db199SXin Li 920*9c5db199SXin Li Operators come in two parts and are a '.' separated pair: 921*9c5db199SXin Li 922*9c5db199SXin Li Operators that effect the input line or the current state (line_op). 923*9c5db199SXin Li 'Next' Get next input line and restart parsing (default). 924*9c5db199SXin Li 'Continue' Keep current input line and continue resume parsing. 925*9c5db199SXin Li 'Error' Unrecoverable input discard result and raise Error. 926*9c5db199SXin Li 927*9c5db199SXin Li Operators that affect the record being built for output (record_op). 928*9c5db199SXin Li 'NoRecord' Does nothing (default) 929*9c5db199SXin Li 'Record' Adds the current record to the result. 930*9c5db199SXin Li 'Clear' Clears non-Filldown data from the record. 931*9c5db199SXin Li 'Clearall' Clears all data from the record. 932*9c5db199SXin Li 933*9c5db199SXin Li Args: 934*9c5db199SXin Li rule: FSMRule object. 935*9c5db199SXin Li 936*9c5db199SXin Li Returns: 937*9c5db199SXin Li True if state machine should restart state with new line. 938*9c5db199SXin Li 939*9c5db199SXin Li Raises: 940*9c5db199SXin Li TextFSMError: If Error state is encountered. 941*9c5db199SXin Li """ 942*9c5db199SXin Li # First process the Record operators. 943*9c5db199SXin Li if rule.record_op == 'Record': 944*9c5db199SXin Li self._AppendRecord() 945*9c5db199SXin Li 946*9c5db199SXin Li elif rule.record_op == 'Clear': 947*9c5db199SXin Li # Clear record. 948*9c5db199SXin Li self._ClearRecord() 949*9c5db199SXin Li 950*9c5db199SXin Li elif rule.record_op == 'Clearall': 951*9c5db199SXin Li # Clear all record entries. 952*9c5db199SXin Li self._ClearAllRecord() 953*9c5db199SXin Li 954*9c5db199SXin Li # Lastly process line operators. 955*9c5db199SXin Li if rule.line_op == 'Error': 956*9c5db199SXin Li if rule.new_state: 957*9c5db199SXin Li raise TextFSMError('Error: %s. Line: %s.' 958*9c5db199SXin Li % (rule.new_state, rule.line_num)) 959*9c5db199SXin Li 960*9c5db199SXin Li raise TextFSMError('State Error raised. Line: %s.' 961*9c5db199SXin Li % (rule.line_num)) 962*9c5db199SXin Li 963*9c5db199SXin Li elif rule.line_op == 'Continue': 964*9c5db199SXin Li # Continue with current line without returning to the start of the state. 965*9c5db199SXin Li return False 966*9c5db199SXin Li 967*9c5db199SXin Li # Back to start of current state with a new line. 968*9c5db199SXin Li return True 969*9c5db199SXin Li 970*9c5db199SXin Li def _ClearRecord(self): 971*9c5db199SXin Li """Remove non 'Filldown' record entries.""" 972*9c5db199SXin Li _ = [value.ClearVar() for value in self.values] 973*9c5db199SXin Li 974*9c5db199SXin Li def _ClearAllRecord(self): 975*9c5db199SXin Li """Remove all record entries.""" 976*9c5db199SXin Li _ = [value.ClearAllVar() for value in self.values] 977*9c5db199SXin Li 978*9c5db199SXin Li def GetValuesByAttrib(self, attribute): 979*9c5db199SXin Li """Returns the list of values that have a particular attribute.""" 980*9c5db199SXin Li 981*9c5db199SXin Li if attribute not in self._options_cls.ValidOptions(): 982*9c5db199SXin Li raise ValueError("'%s': Not a valid attribute." % attribute) 983*9c5db199SXin Li 984*9c5db199SXin Li result = [] 985*9c5db199SXin Li for value in self.values: 986*9c5db199SXin Li if attribute in value.OptionNames(): 987*9c5db199SXin Li result.append(value.name) 988*9c5db199SXin Li 989*9c5db199SXin Li return result 990*9c5db199SXin Li 991*9c5db199SXin Li 992*9c5db199SXin Lidef main(argv=None): 993*9c5db199SXin Li """Validate text parsed with FSM or validate an FSM via command line.""" 994*9c5db199SXin Li 995*9c5db199SXin Li if argv is None: 996*9c5db199SXin Li argv = sys.argv 997*9c5db199SXin Li 998*9c5db199SXin Li try: 999*9c5db199SXin Li opts, args = getopt.getopt(argv[1:], 'h', ['help']) 1000*9c5db199SXin Li except getopt.error as msg: 1001*9c5db199SXin Li raise Usage(msg) 1002*9c5db199SXin Li 1003*9c5db199SXin Li for opt, _ in opts: 1004*9c5db199SXin Li if opt in ('-h', '--help'): 1005*9c5db199SXin Li print(__doc__) 1006*9c5db199SXin Li print(help_msg) 1007*9c5db199SXin Li return 0 1008*9c5db199SXin Li 1009*9c5db199SXin Li if not args or len(args) > 4: 1010*9c5db199SXin Li raise Usage('Invalid arguments.') 1011*9c5db199SXin Li 1012*9c5db199SXin Li # If we have an argument, parse content of file and display as a template. 1013*9c5db199SXin Li # Template displayed will match input template, minus any comment lines. 1014*9c5db199SXin Li with open(args[0], 'r') as template: 1015*9c5db199SXin Li fsm = TextFSM(template) 1016*9c5db199SXin Li print('FSM Template:\n%s\n' % fsm) 1017*9c5db199SXin Li 1018*9c5db199SXin Li if len(args) > 1: 1019*9c5db199SXin Li # Second argument is file with example cli input. 1020*9c5db199SXin Li # Prints parsed tabular result. 1021*9c5db199SXin Li with open(args[1], 'r') as f: 1022*9c5db199SXin Li cli_input = f.read() 1023*9c5db199SXin Li 1024*9c5db199SXin Li table = fsm.ParseText(cli_input) 1025*9c5db199SXin Li print('FSM Table:') 1026*9c5db199SXin Li result = str(fsm.header) + '\n' 1027*9c5db199SXin Li for line in table: 1028*9c5db199SXin Li result += str(line) + '\n' 1029*9c5db199SXin Li print(result, end='') 1030*9c5db199SXin Li 1031*9c5db199SXin Li if len(args) > 2: 1032*9c5db199SXin Li # Compare tabular result with data in third file argument. 1033*9c5db199SXin Li # Exit value indicates if processed data matched expected result. 1034*9c5db199SXin Li with open(args[2], 'r') as f: 1035*9c5db199SXin Li ref_table = f.read() 1036*9c5db199SXin Li 1037*9c5db199SXin Li if ref_table != result: 1038*9c5db199SXin Li print('Data mis-match!') 1039*9c5db199SXin Li return 1 1040*9c5db199SXin Li else: 1041*9c5db199SXin Li print('Data match!') 1042*9c5db199SXin Li 1043*9c5db199SXin Li 1044*9c5db199SXin Liif __name__ == '__main__': 1045*9c5db199SXin Li help_msg = '%s [--help] template [input_file [output_file]]\n' % sys.argv[0] 1046*9c5db199SXin Li try: 1047*9c5db199SXin Li sys.exit(main()) 1048*9c5db199SXin Li except Usage as err: 1049*9c5db199SXin Li print(err, file=sys.stderr) 1050*9c5db199SXin Li print('For help use --help', file=sys.stderr) 1051*9c5db199SXin Li sys.exit(2) 1052*9c5db199SXin Li except (IOError, TextFSMError, TextFSMTemplateError) as err: 1053*9c5db199SXin Li print(err, file=sys.stderr) 1054*9c5db199SXin Li sys.exit(2) 1055