from __future__ import absolute_import from operator import methodcaller, attrgetter from colorama import Fore, Back, Style from difflib import SequenceMatcher import logging import time import sys import re import os if (sys.version_info>=(3, 0, 0,)): from collections.abc import MutableSequence, Iterator else: ## This syntax is not supported in Python 3... from collections import MutableSequence, Iterator from ciscoconfparse.models_cisco import IOSHostnameLine, IOSRouteLine from ciscoconfparse.models_cisco import IOSIntfLine from ciscoconfparse.models_cisco import IOSAccessLine, IOSIntfGlobal from ciscoconfparse.models_cisco import IOSAaaLoginAuthenticationLine from ciscoconfparse.models_cisco import IOSAaaEnableAuthenticationLine from ciscoconfparse.models_cisco import IOSAaaCommandsAuthorizationLine from ciscoconfparse.models_cisco import IOSAaaCommandsAccountingLine from ciscoconfparse.models_cisco import IOSAaaExecAccountingLine from ciscoconfparse.models_cisco import IOSAaaGroupServerLine from ciscoconfparse.models_cisco import IOSCfgLine from ciscoconfparse.models_nxos import NXOSHostnameLine, NXOSRouteLine, NXOSIntfLine from ciscoconfparse.models_nxos import NXOSAccessLine, NXOSIntfGlobal from ciscoconfparse.models_nxos import NXOSAaaLoginAuthenticationLine from ciscoconfparse.models_nxos import NXOSAaaEnableAuthenticationLine from ciscoconfparse.models_nxos import NXOSAaaCommandsAuthorizationLine from ciscoconfparse.models_nxos import NXOSAaaCommandsAccountingLine from ciscoconfparse.models_nxos import NXOSAaaExecAccountingLine from ciscoconfparse.models_nxos import NXOSAaaGroupServerLine from ciscoconfparse.models_nxos import NXOSvPCLine from ciscoconfparse.models_nxos import NXOSCfgLine from ciscoconfparse.models_asa import ASAObjGroupNetwork from ciscoconfparse.models_asa import ASAObjGroupService from ciscoconfparse.models_asa import ASAHostnameLine from ciscoconfparse.models_asa import ASAObjNetwork from ciscoconfparse.models_asa import ASAObjService from ciscoconfparse.models_asa import ASAIntfGlobal from ciscoconfparse.models_asa import ASAIntfLine from ciscoconfparse.models_asa import ASACfgLine from ciscoconfparse.models_asa import ASAName from ciscoconfparse.models_asa import ASAAclLine from ciscoconfparse.models_junos import JunosCfgLine r""" ciscoconfparse.py - Parse, Query, Build, and Modify IOS-style configs Copyright (C) 2007-2019 David Michael Pennington This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . If you need to contact the author, you can do so by emailing: mike [~at~] pennington [/dot\] net """ ## Docstring props: http://stackoverflow.com/a/1523456/667301 versionfilepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'version') # __version__ if-else below fixes Github issue #123 if os.path.isfile(versionfilepath): with open(versionfilepath) as vh: __version__ = vh.read().strip() else: # This case is required for importing from a zipfile... Github issue #123 __version__ = "0.0.0" # __version__ read failed __email__ = r"mike /at\ pennington [dot] net" __author__ = "David Michael Pennington <{0}>".format(__email__) __copyright__ = "2007-{0}, {1}".format(time.strftime('%Y'), __author__) __license__ = "GPLv3" __status__ = "Production" _log = logging.getLogger(__file__) _CCP_LOG_FORMAT_PREFIX_STR = ( Fore.WHITE + '[%(module)s %(funcName)s] [%(levelname)s] %(asctime)s ') _CCP_LOG_FORMAT_MSG_STR = (Fore.GREEN + '%(msg)s' + Fore.RESET) _CCP_LOG_FORMAT_STR = _CCP_LOG_FORMAT_PREFIX_STR + _CCP_LOG_FORMAT_MSG_STR _ccp_log_format = logging.Formatter(_CCP_LOG_FORMAT_STR, '%H:%M:%S') _log.setLevel(logging.DEBUG) _LOG_CHANNEL_STDOUT = logging.StreamHandler(sys.stdout) _LOG_CHANNEL_STDOUT.setFormatter(_ccp_log_format) _log.addHandler(_LOG_CHANNEL_STDOUT) class CiscoConfParse(object): """Parses Cisco IOS configurations and answers queries about the configs""" def __init__(self, config="", comment="!", debug=False, factory=False, linesplit_rgx=r"\r*\n+", ignore_blank_lines=True, syntax='ios'): """Initialize CiscoConfParse. Kwargs: - config (list or str): A list of configuration statements, or a configuration file path to be parsed - comment (str): A comment delimiter. This should only be changed when parsing non-Cisco IOS configurations, which do not use a ! as the comment delimiter. ``comment`` defaults to '!'. This value can hold multiple characters in case the config uses multiple characters for comment delimiters; however, the comment delimiters are always assumed to be one character wide - debug (bool): ``debug`` defaults to False, and should be kept that way unless you're working on a very tricky config parsing problem. Debug output is not particularly friendly - factory (bool): ``factory`` defaults to False; if set ``True``, it enables a beta-quality configuration line classifier. - linesplit_rgx (str): ``linesplit_rgx`` is used when parsing configuration files to find where new configuration lines are. It is best to leave this as the default, unless you're working on a system that uses unusual line terminations (for instance something besides Unix, OSX, or Windows) - ignore_blank_lines (bool): ``ignore_blank_lines`` defaults to True; when this is set True, ciscoconfparse ignores blank configuration lines. You might want to set ``ignore_blank_lines`` to False if you intentionally use blank lines in your configuration (ref: Github Issue #2), or you are parsing configurations which naturally have blank lines (such as Cisco Nexus configurations). - syntax (str): ``syntax`` defaults to 'ios'; You can choose from the following values: ios, nxos, asa, junos Attributes: - comment_delimiter (str): A string containing the comment-delimiter - ConfigObjs (:class:`~ciscoconfparse.IOSConfigList`) : A custom list, which contains all parsed :class:`~models_cisco.IOSCfgLine` instances. - all_parents (list) : A list of all parent :class:`~models_cisco.IOSCfgLine` instances. - last_index (int) : An integer with the last index in ``ConfigObjs`` Returns: - An instance of a :class:`~ciscoconfparse.CiscoConfParse` object This example illustrates how to parse a simple Cisco IOS configuration with :class:`~ciscoconfparse.CiscoConfParse` into a variable called ``parse``. This example also illustrates what the ``ConfigObjs`` and ``ioscfg`` attributes contain. .. code-block:: python :emphasize-lines: 5 >>> config = [ ... 'logging trap debugging', ... 'logging 172.28.26.15', ... ] >>> parse = CiscoConfParse(config) >>> parse >>> parse.ConfigObjs , ]> >>> parse.ioscfg ['logging trap debugging', 'logging 172.28.26.15'] >>> """ # all IOSCfgLine object instances... self.comment_delimiter = comment self.factory = factory self.ConfigObjs = None self.syntax = syntax self.debug = debug if isinstance(config, list) or isinstance(config, Iterator): if syntax == 'ios': # we already have a list object, simply call the parser if self.debug: _log.debug("parsing from a python list with ios syntax") self.ConfigObjs = IOSConfigList( data=config, comment_delimiter=comment, debug=debug, factory=factory, ignore_blank_lines=ignore_blank_lines, syntax='ios', CiscoConfParse=self) elif syntax == 'nxos': # we already have a list object, simply call the parser if self.debug: _log.debug("parsing from a python list with nxos syntax") self.ConfigObjs = NXOSConfigList( data=config, comment_delimiter=comment, debug=debug, factory=factory, ignore_blank_lines=False, # NXOS always has blank lines syntax='nxos', CiscoConfParse=self) elif syntax == 'asa': # we already have a list object, simply call the parser if self.debug: _log.debug("parsing from a python list with asa syntax") self.ConfigObjs = ASAConfigList( data=config, comment_delimiter=comment, debug=debug, factory=factory, ignore_blank_lines=ignore_blank_lines, syntax='asa', CiscoConfParse=self) elif syntax == 'junos': ## FIXME I am shamelessly abusing the IOSConfigList for now... # we already have a list object, simply call the parser error = 'junos parser factory is not yet enabled; use factory=False' assert factory is False, error config = self.convert_braces_to_ios(config) if self.debug: _log.debug("parsing from a python list with junos syntax") self.ConfigObjs = IOSConfigList( data=config, comment_delimiter=comment, debug=debug, factory=factory, ignore_blank_lines=ignore_blank_lines, syntax='junos', CiscoConfParse=self) else: raise ValueError("FATAL: '{}' is an unknown syntax".format( syntax)) ## Accept either a string, unicode, or a pathlib.Path instance... elif getattr(config, 'encode', False) or getattr(config, 'is_file'): # Try opening as a file try: if syntax == 'ios': # string - assume a filename... open file, split and parse if self.debug: _log.debug("parsing from '{0}' with ios syntax".format( config)) f = open(config, **self.openargs) text = f.read() f.close() rgx = re.compile(linesplit_rgx) self.ConfigObjs = IOSConfigList( rgx.split(text), comment_delimiter=comment, debug=debug, factory=factory, ignore_blank_lines=ignore_blank_lines, syntax='ios', CiscoConfParse=self) elif syntax == 'nxos': # string - assume a filename... open file, split and parse if self.debug: _log.debug("parsing from '{0}' with nxos syntax".format( config)) f = open(config, **self.openargs) text = f.read() f.close() rgx = re.compile(linesplit_rgx) self.ConfigObjs = NXOSConfigList( rgx.split(text), comment_delimiter=comment, debug=debug, factory=factory, ignore_blank_lines=False, syntax='nxos', CiscoConfParse=self) elif syntax == 'asa': # string - assume a filename... open file, split and parse if self.debug: _log.debug("parsing from '{0}' with asa syntax".format( config)) f = open(config, **self.openargs) text = f.read() f.close() rgx = re.compile(linesplit_rgx) self.ConfigObjs = ASAConfigList( rgx.split(text), comment_delimiter=comment, debug=debug, factory=factory, ignore_blank_lines=ignore_blank_lines, syntax='asa', CiscoConfParse=self) elif syntax == 'junos': # string - assume a filename... open file, split and parse if self.debug: _log.debug("parsing from '{0}' with junos syntax". format(config)) f = open(config, **self.openargs) text = f.read() f.close() rgx = re.compile(linesplit_rgx) config = self.convert_braces_to_ios(rgx.split(text)) ## FIXME I am shamelessly abusing the IOSConfigList for now... self.ConfigObjs = IOSConfigList( config, comment_delimiter=comment, debug=debug, factory=factory, ignore_blank_lines=ignore_blank_lines, syntax='junos', CiscoConfParse=self) else: raise ValueError("FATAL: '{}' is an unknown syntax".format( syntax)) except IOError: print("[FATAL] CiscoConfParse could not open '%s'" % config) raise RuntimeError else: raise RuntimeError("[FATAL] CiscoConfParse() received" + " an invalid argument\n") self.ConfigObjs.CiscoConfParse = self def __repr__(self): return "" % ( len(self.ConfigObjs), self.syntax, self.comment_delimiter, self.factory) @property def openargs(self): """Fix for Py3.5 deprecation of universal newlines - Ref Github #114 also see https://softwareengineering.stackexchange.com/q/298677/23144 """ if (sys.version_info>=(3, 5, 0,)): retval = {'mode': 'r', 'newline': None} else: retval = {'mode': 'rU'} return retval @property def ioscfg(self): """A list containing all text configuration statements""" ## I keep this here to emulate the legacy ciscoconfparse behavior return list(map(attrgetter('text'), self.ConfigObjs)) @property def objs(self): """An alias to the ``ConfigObjs`` attribute""" return self.ConfigObjs def atomic(self): """Call :func:`~ciscoconfparse.CiscoConfParse.atomic` to manually fix up ``ConfigObjs`` relationships after modifying a parsed configuration. This method is slow; try to batch calls to :func:`~ciscoconfparse.CiscoConfParse.atomic()` if possible. .. warning:: If you modify a configuration after parsing it with :class:`~ciscoconfparse.CiscoConfParse`, you *must* call :func:`~ciscoconfparse.CiscoConfParse.commit` or :func:`~ciscoconfparse.CiscoConfParse.atomic` before searching the configuration again with methods such as :func:`~ciscoconfparse.CiscoConfParse.find_objects` or :func:`~ciscoconfparse.CiscoConfParse.find_lines`. Failure to call :func:`~ciscoconfparse.CiscoConfParse.commit` or :func:`~ciscoconfparse.CiscoConfParse.atomic` on config modifications could lead to unexpected search results. """ self.ConfigObjs._bootstrap_from_text() def commit(self): """Alias for calling the :func:`~ciscoconfparse.CiscoConfParse.atomic` method. This method is slow; try to batch calls to :func:`~ciscoconfparse.CiscoConfParse.commit()` if possible. .. warning:: If you modify a configuration after parsing it with :class:`~ciscoconfparse.CiscoConfParse`, you *must* call :func:`~ciscoconfparse.CiscoConfParse.commit` or :func:`~ciscoconfparse.CiscoConfParse.atomic` before searching the configuration again with methods such as :func:`~ciscoconfparse.CiscoConfParse.find_objects` or :func:`~ciscoconfparse.CiscoConfParse.find_lines`. Failure to call :func:`~ciscoconfparse.CiscoConfParse.commit` or :func:`~ciscoconfparse.CiscoConfParse.atomic` on config modifications could lead to unexpected search results. """ self.atomic() def convert_braces_to_ios(self, input_list, stop_width=4): ## Note to self, I made this regex fairly junos-specific... assert '{' not in set(self.comment_delimiter) assert '}' not in set(self.comment_delimiter) JUNOS_RE_STR = r"""^ (?:\s* (?P\})*(?P.*?)(?P\{)*;* |(?P[^\{\}]*?)(?P\{)(?P.*?)(?P\});*\s* |(?P[^\{\}]*?);*\s* )$ """ LINE_RE = re.compile(JUNOS_RE_STR, re.VERBOSE) COMMENT_RE = re.compile(r'^\s*(?P[{0}]+)(?P[^{0}]*)$'.format(re.escape(self.comment_delimiter))) def parse_line_braces(input): assert input is not None indent_child = 0 indent_this_line = 0 mm = LINE_RE.search(input.strip()) nn = COMMENT_RE.search(input.strip()) if nn is not None: results = nn.groupdict() return (indent_this_line, indent_child, results.get('delimiter')+results.get('comment', '')) elif mm is not None: results = mm.groupdict() # } line1 { foo bar this } { braces_close_left = bool(results.get('braces_close_left', '')) braces_open_right = bool(results.get('braces_open_right', '')) # line2 braces_open_left = bool(results.get('braces_open_left', '')) braces_close_right = bool(results.get('braces_close_right', '')) # line3 line1_str = results.get('line1', '') line3_str = results.get('line3', '') if braces_close_left and braces_open_right: # Based off line1 # } elseif { bar baz } { indent_this_line -= 1 indent_child += 0 retval = results.get('line1', None) return (indent_this_line, indent_child, retval) elif bool(line1_str) and (braces_close_left is False) and (braces_open_right is False): # Based off line1: # address 1.1.1.1 indent_this_line -= 0 indent_child += 0 retval = results.get('line1', '').strip() # Strip empty braces here retval = re.sub(r'\s*\{\s*\}\s*', '', retval) return (indent_this_line, indent_child, retval) elif (line1_str == '') and (braces_close_left is False) and (braces_open_right is False): # Based off line1: # return empty string indent_this_line -= 0 indent_child += 0 return (indent_this_line, indent_child, '') elif braces_open_left and braces_close_right: # Based off line2 # this { bar baz } indent_this_line -= 0 indent_child += 0 line = results.get('line2', None) or '' condition = results.get('condition2', None) or '' if condition.strip() == '': retval = line else: retval = line + " {" + condition + " }" return (indent_this_line, indent_child, retval) elif braces_close_left: # Based off line1 # } indent_this_line -= 1 indent_child -= 1 return (indent_this_line, indent_child, '') elif braces_open_right: # Based off line1 # this that foo { indent_this_line -= 0 indent_child += 1 line = results.get('line1', None) or '' return (indent_this_line, indent_child, line) elif (line3_str != '') and (line3_str is not None): indent_this_line += 0 indent_child += 0 return (indent_this_line, indent_child, '') else: raise ValueError('Cannot parse junos match:"{0}"'.format(input)) else: raise ValueError('Cannot parse junos:"{0}"'.format(input)) lines = list() offset = 0 STOP_WIDTH = stop_width for idx, tmp in enumerate(input_list): if self.debug is True: _log.debug("Parse line {0}:'{1}'".format(idx+1, tmp.strip())) (indent_this_line, indent_child, line) = parse_line_braces( tmp.strip()) lines.append((" " * STOP_WIDTH * (offset + indent_this_line)) + line.strip()) offset += indent_child return lines def find_interface_objects(self, intfspec, exactmatch=True): """Find all :class:`~models_cisco.IOSCfgLine` or :class:`~models_cisco.NXOSCfgLine` objects whose text is an abbreviation for ``intfspec`` and return the objects in a python list. .. note:: The configuration *must* be parsed with ``factory=True`` to use this method Args: - intfspec (str): A string which is the abbreviation (or full name) of the interface Kwargs: - exactmatch (bool): Defaults to True; when True, this option requires ``intfspec`` match the whole interface name and number. Returns: - list. A list of matching :class:`~ciscoconfparse.IOSIntfLine` objects .. code-block:: python :emphasize-lines: 12 >>> config = [ ... '!', ... 'interface Serial1/0', ... ' ip address 1.1.1.1 255.255.255.252', ... '!', ... 'interface Serial1/1', ... ' ip address 1.1.1.5 255.255.255.252', ... '!', ... ] >>> parse = CiscoConfParse(config, factory=True) >>> >>> parse.find_interface_objects('Se 1/0') [] >>> """ if not (self.factory is True): raise ValueError( "FATAL: find_interface_objects() must be called with 'factory=True'" ) retval = list() if (self.syntax=='ios') or (self.syntax=='nxos'): if exactmatch: for obj in self.find_objects('^interface'): if intfspec.lower() in obj.abbvs: retval.append(obj) break # Only break if exactmatch is True else: raise NotImplementedError ## TODO: implement ASAConfigLine.abbvs and others else: raise NotImplementedError return retval def find_objects_dna(self, dnaspec, exactmatch=False): """Find all :class:`~models_cisco.IOSCfgLine` objects whose text matches ``dnaspec`` and return the :class:`~models_cisco.IOSCfgLine` objects in a python list. .. note:: :func:`~ciscoconfparse.CiscoConfParse.find_objects_dna` requires the configuration to be parsed with factory=True Args: - dnaspec (str): A string or python regular expression, which should be matched. This argument will be used to match dna attribute of the object Kwargs: - exactmatch (bool): Defaults to False. When set True, this option requires ``dnaspec`` match the whole configuration line, instead of a portion of the configuration line. Returns: - list. A list of matching :class:`~ciscoconfparse.IOSCfgLine` objects .. code-block:: python :emphasize-lines: 8 >>> config = [ ... '!', ... 'hostname MyRouterHostname', ... '!', ... ] >>> parse = CiscoConfParse(config, factory=True, syntax='ios') >>> >>> obj_list = parse.find_objects_dna(r'Hostname') >>> obj_list [] >>> >>> # The IOSHostnameLine object has a hostname attribute >>> obj_list[0].hostname 'MyRouterHostname' >>> """ if not self.factory: raise ValueError( "[FATAL] find_objects_dna() must be called in conjunction with the factory configuration parsing option" ) if not exactmatch: # Return objects whose text attribute matches linespec linespec_re = re.compile(dnaspec) elif exactmatch: # Return objects whose text attribute matches linespec exactly linespec_re = re.compile("^{0}$".format(dnaspec)) return list( filter(lambda obj: linespec_re.search(obj.dna), self.ConfigObjs)) def find_objects(self, linespec, exactmatch=False, ignore_ws=False): """Find all :class:`~models_cisco.IOSCfgLine` objects whose text matches ``linespec`` and return the :class:`~models_cisco.IOSCfgLine` objects in a python list. :func:`~ciscoconfparse.CiscoConfParse.find_objects` is similar to :func:`~ciscoconfparse.CiscoConfParse.find_lines`; however, the former returns a list of :class:`~models_cisco.IOSCfgLine` objects, while the latter returns a list of text configuration statements. Going forward, I strongly encourage people to start using :func:`~ciscoconfparse.CiscoConfParse.find_objects` instead of :func:`~ciscoconfparse.CiscoConfParse.find_lines`. Args: - linespec (str): A string or python regular expression, which should be matched Kwargs: - exactmatch (bool): Defaults to False. When set True, this option requires ``linespec`` match the whole configuration line, instead of a portion of the configuration line. - ignore_ws (bool): boolean that controls whether whitespace is ignored. Default is False. Returns: - list. A list of matching :class:`~ciscoconfparse.IOSCfgLine` objects This example illustrates the difference between :func:`~ciscoconfparse.CiscoConfParse.find_objects` and :func:`~ciscoconfparse.CiscoConfParse.find_lines`. .. code-block:: python :emphasize-lines: 12,15 >>> config = [ ... '!', ... 'interface Serial1/0', ... ' ip address 1.1.1.1 255.255.255.252', ... '!', ... 'interface Serial1/1', ... ' ip address 1.1.1.5 255.255.255.252', ... '!', ... ] >>> parse = CiscoConfParse(config) >>> >>> parse.find_objects(r'^interface') [, ] >>> >>> parse.find_lines(r'^interface') ['interface Serial1/0', 'interface Serial1/1'] >>> """ if ignore_ws: linespec = self._build_space_tolerant_regex(linespec) return self._find_line_OBJ(linespec, exactmatch) def find_lines(self, linespec, exactmatch=False, ignore_ws=False): """This method is the equivalent of a simple configuration grep (Case-sensitive). Args: - linespec (str): Text regular expression for the line to be matched Kwargs: - exactmatch (bool): Defaults to False. When set True, this option requires ``linespec`` match the whole configuration line, instead of a portion of the configuration line. - ignore_ws (bool): boolean that controls whether whitespace is ignored. Default is False. Returns: - list. A list of matching configuration lines """ if ignore_ws: linespec = self._build_space_tolerant_regex(linespec) if (exactmatch is False): # Return the lines in self.ioscfg, which match linespec return list(filter(re.compile(linespec).search, self.ioscfg)) else: # Return the lines in self.ioscfg, which match (exactly) linespec return list( filter(re.compile("^%s$" % linespec).search, self.ioscfg)) def find_children(self, linespec, exactmatch=False, ignore_ws=False): """Returns the parents matching the linespec, and their immediate children. This method is different than :meth:`find_all_children`, because :meth:`find_all_children` finds children of children. :meth:`find_children` only finds immediate children. Args: - linespec (str): Text regular expression for the line to be matched Kwargs: - exactmatch (bool): boolean that controls whether partial matches are valid - ignore_ws (bool): boolean that controls whether whitespace is ignored Returns: - list. A list of matching configuration lines Suppose you are interested in finding all immediate children of the `archive` statements in the following configuration... .. code:: username ddclient password 7 107D3D232342041E3A archive log config logging enable hidekeys path ftp://ns.foo.com//tftpboot/Foo-archive ! Using the config above, we expect to find the following config lines... .. code:: archive log config path ftp://ns.foo.com//tftpboot/Foo-archive We would accomplish this by querying `find_children('^archive')`... .. code-block:: python :emphasize-lines: 11 >>> from ciscoconfparse import CiscoConfParse >>> config = ['username ddclient password 7 107D3D232342041E3A', ... 'archive', ... ' log config', ... ' logging enable', ... ' hidekeys', ... ' path ftp://ns.foo.com//tftpboot/Foo-archive', ... '!', ... ] >>> p = CiscoConfParse(config) >>> p.find_children('^archive') ['archive', ' log config', ' path ftp://ns.foo.com//tftpboot/Foo-archive'] >>> """ if ignore_ws: linespec = self._build_space_tolerant_regex(linespec) if (exactmatch is False): parentobjs = self._find_line_OBJ(linespec) else: parentobjs = self._find_line_OBJ("^%s$" % linespec) allobjs = set([]) for parent in parentobjs: if (parent.has_children is True): allobjs.update(set(parent.children)) allobjs.add(parent) return list(map(attrgetter('text'), sorted(allobjs))) def find_all_children(self, linespec, exactmatch=False, ignore_ws=False): """Returns the parents matching the linespec, and all their children. This method is different than :meth:`find_children`, because :meth:`find_all_children` finds children of children. :meth:`find_children` only finds immediate children. Args: - linespec (str): Text regular expression for the line to be matched Kwargs: - exactmatch (bool): boolean that controls whether partial matches are valid - ignore_ws (bool): boolean that controls whether whitespace is ignored Returns: - list. A list of matching configuration lines Suppose you are interested in finding all `archive` statements in the following configuration... .. code:: username ddclient password 7 107D3D232342041E3A archive log config logging enable hidekeys path ftp://ns.foo.com//tftpboot/Foo-archive ! Using the config above, we expect to find the following config lines... .. code:: archive log config logging enable hidekeys path ftp://ns.foo.com//tftpboot/Foo-archive We would accomplish this by querying `find_all_children('^archive')`... .. code-block:: python :emphasize-lines: 11 >>> from ciscoconfparse import CiscoConfParse >>> config = ['username ddclient password 7 107D3D232342041E3A', ... 'archive', ... ' log config', ... ' logging enable', ... ' hidekeys', ... ' path ftp://ns.foo.com//tftpboot/Foo-archive', ... '!', ... ] >>> p = CiscoConfParse(config) >>> p.find_all_children('^archive') ['archive', ' log config', ' logging enable', ' hidekeys', ' path ftp://ns.foo.com//tftpboot/Foo-archive'] >>> """ if ignore_ws: linespec = self._build_space_tolerant_regex(linespec) if (exactmatch is False): parentobjs = self._find_line_OBJ(linespec) else: parentobjs = self._find_line_OBJ("^%s$" % linespec) allobjs = set([]) for parent in parentobjs: allobjs.add(parent) allobjs.update(set(parent.all_children)) return list(map(attrgetter('text'), sorted(allobjs))) def find_blocks(self, linespec, exactmatch=False, ignore_ws=False): """Find all siblings matching the linespec, then find all parents of those siblings. Return a list of config lines sorted by line number, lowest first. Note: any children of the siblings should NOT be returned. Args: - linespec (str): Text regular expression for the line to be matched Kwargs: - exactmatch (bool): boolean that controls whether partial matches are valid - ignore_ws (bool): boolean that controls whether whitespace is ignored Returns: - list. A list of matching configuration lines This example finds `bandwidth percent` statements in following config, the siblings of those `bandwidth percent` statements, as well as the parent configuration statements required to access them. .. code:: ! policy-map EXTERNAL_CBWFQ class IP_PREC_HIGH priority percent 10 police cir percent 10 conform-action transmit exceed-action drop class IP_PREC_MEDIUM bandwidth percent 50 queue-limit 100 class class-default bandwidth percent 40 queue-limit 100 policy-map SHAPE_HEIR class ALL shape average 630000 service-policy EXTERNAL_CBWFQ ! The following config lines should be returned: .. code:: policy-map EXTERNAL_CBWFQ class IP_PREC_MEDIUM bandwidth percent 50 queue-limit 100 class class-default bandwidth percent 40 queue-limit 100 We do this by quering `find_blocks('bandwidth percent')`... .. code-block:: python :emphasize-lines: 22,25 >>> from ciscoconfparse import CiscoConfParse >>> config = ['!', ... 'policy-map EXTERNAL_CBWFQ', ... ' class IP_PREC_HIGH', ... ' priority percent 10', ... ' police cir percent 10', ... ' conform-action transmit', ... ' exceed-action drop', ... ' class IP_PREC_MEDIUM', ... ' bandwidth percent 50', ... ' queue-limit 100', ... ' class class-default', ... ' bandwidth percent 40', ... ' queue-limit 100', ... 'policy-map SHAPE_HEIR', ... ' class ALL', ... ' shape average 630000', ... ' service-policy EXTERNAL_CBWFQ', ... '!', ... ] >>> p = CiscoConfParse(config) >>> p.find_blocks('bandwidth percent') ['policy-map EXTERNAL_CBWFQ', ' class IP_PREC_MEDIUM', ' bandwidth percent 50', ' queue-limit 100', ' class class-default', ' bandwidth percent 40', ' queue-limit 100'] >>> >>> p.find_blocks(' class class-default') ['policy-map EXTERNAL_CBWFQ', ' class IP_PREC_HIGH', ' class IP_PREC_MEDIUM', ' class class-default'] >>> """ tmp = set([]) if ignore_ws: linespec = self._build_space_tolerant_regex(linespec) # Find line objects maching the spec if (exactmatch is False): objs = self._find_line_OBJ(linespec) else: objs = self._find_line_OBJ("^%s$" % linespec) for obj in objs: tmp.add(obj) # Find the siblings of this line sib_objs = self._find_sibling_OBJ(obj) for sib_obj in sib_objs: tmp.add(sib_obj) # Find the parents for everything pobjs = set([]) for lineobject in tmp: for pobj in lineobject.all_parents: pobjs.add(pobj) tmp.update(pobjs) return list(map(attrgetter('text'), sorted(tmp))) def find_objects_w_child(self, parentspec, childspec, ignore_ws=False, recurse=False): """Return a list of parent :class:`~models_cisco.IOSCfgLine` objects, which matched the ``parentspec`` and whose children match ``childspec``. Only the parent :class:`~models_cisco.IOSCfgLine` objects will be returned. Args: - parentspec (str): Text regular expression for the :class:`~models_cisco.IOSCfgLine` object to be matched; this must match the parent's line - childspec (str): Text regular expression for the line to be matched; this must match the child's line Kwargs: - ignore_ws (bool): boolean that controls whether whitespace is ignored - recurse (bool): Set True if you want to search all children (children, grand children, great grand children, etc...) Returns: - list. A list of matching parent :class:`~models_cisco.IOSCfgLine` objects This example uses :func:`~ciscoconfparse.find_objects_w_child()` to find all ports that are members of access vlan 300 in following config... .. code:: ! interface FastEthernet0/1 switchport access vlan 532 spanning-tree vlan 532 cost 3 ! interface FastEthernet0/2 switchport access vlan 300 spanning-tree portfast ! interface FastEthernet0/3 duplex full speed 100 switchport access vlan 300 spanning-tree portfast ! The following interfaces should be returned: .. code:: interface FastEthernet0/2 interface FastEthernet0/3 We do this by quering `find_objects_w_child()`; we set our parent as `^interface` and set the child as `switchport access vlan 300`. .. code-block:: python :emphasize-lines: 18 >>> config = ['!', ... 'interface FastEthernet0/1', ... ' switchport access vlan 532', ... ' spanning-tree vlan 532 cost 3', ... '!', ... 'interface FastEthernet0/2', ... ' switchport access vlan 300', ... ' spanning-tree portfast', ... '!', ... 'interface FastEthernet0/3', ... ' duplex full', ... ' speed 100', ... ' switchport access vlan 300', ... ' spanning-tree portfast', ... '!', ... ] >>> p = CiscoConfParse(config) >>> p.find_objects_w_child('^interface', ... 'switchport access vlan 300') ... [, ] >>> """ if ignore_ws: parentspec = self._build_space_tolerant_regex(parentspec) childspec = self._build_space_tolerant_regex(childspec) return list( filter(lambda x: x.re_search_children(childspec, recurse=recurse), self.find_objects(parentspec))) def find_objects_w_all_children(self, parentspec, childspec, ignore_ws=False, recurse=False): """Return a list of parent :class:`~models_cisco.IOSCfgLine` objects, which matched the ``parentspec`` and whose children match all elements in ``childspec``. Only the parent :class:`~models_cisco.IOSCfgLine` objects will be returned. Args: - parentspec (str): Text regular expression for the :class:`~models_cisco.IOSCfgLine` object to be matched; this must match the parent's line - childspec (str): A list of text regular expressions to be matched among the children Kwargs: - ignore_ws (bool): boolean that controls whether whitespace is ignored - recurse (bool): Set True if you want to search all children (children, grand children, great grand children, etc...) Returns: - list. A list of matching parent :class:`~models_cisco.IOSCfgLine` objects This example uses :func:`~ciscoconfparse.find_objects_w_child()` to find all ports that are members of access vlan 300 in following config... .. code:: ! interface FastEthernet0/1 switchport access vlan 532 spanning-tree vlan 532 cost 3 ! interface FastEthernet0/2 switchport access vlan 300 spanning-tree portfast ! interface FastEthernet0/2 duplex full speed 100 switchport access vlan 300 spanning-tree portfast ! The following interfaces should be returned: .. code:: interface FastEthernet0/2 interface FastEthernet0/3 We do this by quering `find_objects_w_all_children()`; we set our parent as `^interface` and set the childspec as ['switchport access vlan 300', 'spanning-tree portfast']. .. code-block:: python :emphasize-lines: 18 >>> config = ['!', ... 'interface FastEthernet0/1', ... ' switchport access vlan 532', ... ' spanning-tree vlan 532 cost 3', ... '!', ... 'interface FastEthernet0/2', ... ' switchport access vlan 300', ... ' spanning-tree portfast', ... '!', ... 'interface FastEthernet0/3', ... ' duplex full', ... ' speed 100', ... ' switchport access vlan 300', ... ' spanning-tree portfast', ... '!', ... ] >>> p = CiscoConfParse(config) >>> p.find_objects_w_all_children('^interface', ... ['switchport access vlan 300', 'spanning-tree portfast']) ... [, ] >>> """ assert bool(getattr(childspec, 'append')) # Childspec must be a list retval = list() if ignore_ws: parentspec = self._build_space_tolerant_regex(parentspec) childspec = map(self._build_space_tolerant_regex, childspec) for parentobj in self.find_objects(parentspec): results = set([]) for child_cfg in childspec: results.add(bool(parentobj.re_search_children(child_cfg, recurse=recurse))) if False in results: continue else: retval.append(parentobj) return retval def find_objects_w_missing_children(self, parentspec, childspec, ignore_ws=False): """Return a list of parent :class:`~models_cisco.IOSCfgLine` objects, which matched the ``parentspec`` and whose children do not match all elements in ``childspec``. Only the parent :class:`~models_cisco.IOSCfgLine` objects will be returned. Args: - parentspec (str): Text regular expression for the :class:`~models_cisco.IOSCfgLine` object to be matched; this must match the parent's line - childspec (str): A list of text regular expressions to be matched among the children Kwargs: - ignore_ws (bool): boolean that controls whether whitespace is ignored Returns: - list. A list of matching parent :class:`~models_cisco.IOSCfgLine` objects""" assert bool(getattr(childspec, 'append')) # Childspec must be a list retval = list() if ignore_ws: parentspec = self._build_space_tolerant_regex(parentspec) childspec = map(self._build_space_tolerant_regex, childspec) for parentobj in self.find_objects(parentspec): results = set([]) for child_cfg in childspec: results.add(bool(parentobj.re_search_children(child_cfg))) if False in results: retval.append(parentobj) else: continue return retval def find_parents_w_child(self, parentspec, childspec, ignore_ws=False): """Parse through all children matching childspec, and return a list of parents that matched the parentspec. Only the parent lines will be returned. Args: - parentspec (str): Text regular expression for the line to be matched; this must match the parent's line - childspec (str): Text regular expression for the line to be matched; this must match the child's line Kwargs: - ignore_ws (bool): boolean that controls whether whitespace is ignored Returns: - list. A list of matching parent configuration lines This example finds all ports that are members of access vlan 300 in following config... .. code:: ! interface FastEthernet0/1 switchport access vlan 532 spanning-tree vlan 532 cost 3 ! interface FastEthernet0/2 switchport access vlan 300 spanning-tree portfast ! interface FastEthernet0/2 duplex full speed 100 switchport access vlan 300 spanning-tree portfast ! The following interfaces should be returned: .. code:: interface FastEthernet0/2 interface FastEthernet0/3 We do this by quering `find_parents_w_child()`; we set our parent as `^interface` and set the child as `switchport access vlan 300`. .. code-block:: python :emphasize-lines: 18 >>> config = ['!', ... 'interface FastEthernet0/1', ... ' switchport access vlan 532', ... ' spanning-tree vlan 532 cost 3', ... '!', ... 'interface FastEthernet0/2', ... ' switchport access vlan 300', ... ' spanning-tree portfast', ... '!', ... 'interface FastEthernet0/3', ... ' duplex full', ... ' speed 100', ... ' switchport access vlan 300', ... ' spanning-tree portfast', ... '!', ... ] >>> p = CiscoConfParse(config) >>> p.find_parents_w_child('^interface', 'switchport access vlan 300') ['interface FastEthernet0/2', 'interface FastEthernet0/3'] >>> """ tmp = self.find_objects_w_child( parentspec, childspec, ignore_ws=ignore_ws) return list(map(attrgetter('text'), tmp)) def find_objects_wo_child(self, parentspec, childspec, ignore_ws=False): r"""Return a list of parent :class:`~models_cisco.IOSCfgLine` objects, which matched the ``parentspec`` and whose children did not match ``childspec``. Only the parent :class:`~models_cisco.IOSCfgLine` objects will be returned. For simplicity, this method only finds oldest_ancestors without immediate children that match. Args: - parentspec (str): Text regular expression for the :class:`~models_cisco.IOSCfgLine` object to be matched; this must match the parent's line - childspec (str): Text regular expression for the line to be matched; this must match the child's line Kwargs: - ignore_ws (bool): boolean that controls whether whitespace is ignored Returns: - list. A list of matching parent configuration lines This example finds all ports that are autonegotiating in the following config... .. code:: ! interface FastEthernet0/1 switchport access vlan 532 spanning-tree vlan 532 cost 3 ! interface FastEthernet0/2 switchport access vlan 300 spanning-tree portfast ! interface FastEthernet0/2 duplex full speed 100 switchport access vlan 300 spanning-tree portfast ! The following interfaces should be returned: .. code:: interface FastEthernet0/1 interface FastEthernet0/2 We do this by quering `find_objects_wo_child()`; we set our parent as `^interface` and set the child as `speed\s\d+` (a regular-expression which matches the word 'speed' followed by an integer). .. code-block:: python :emphasize-lines: 18 >>> config = ['!', ... 'interface FastEthernet0/1', ... ' switchport access vlan 532', ... ' spanning-tree vlan 532 cost 3', ... '!', ... 'interface FastEthernet0/2', ... ' switchport access vlan 300', ... ' spanning-tree portfast', ... '!', ... 'interface FastEthernet0/3', ... ' duplex full', ... ' speed 100', ... ' switchport access vlan 300', ... ' spanning-tree portfast', ... '!', ... ] >>> p = CiscoConfParse(config) >>> p.find_objects_wo_child(r'^interface', r'speed\s\d+') [, ] >>> """ if ignore_ws: parentspec = self._build_space_tolerant_regex(parentspec) childspec = self._build_space_tolerant_regex(childspec) return [ obj for obj in self.find_objects(parentspec) if not obj.re_search_children(childspec) ] def find_parents_wo_child(self, parentspec, childspec, ignore_ws=False): r"""Parse through all parents matching parentspec, and return a list of parents that did NOT have children match the childspec. For simplicity, this method only finds oldest_ancestors without immediate children that match. Args: - parentspec (str): Text regular expression for the line to be matched; this must match the parent's line - childspec (str): Text regular expression for the line to be matched; this must match the child's line Kwargs: - ignore_ws (bool): boolean that controls whether whitespace is ignored Returns: - list. A list of matching parent configuration lines This example finds all ports that are autonegotiating in the following config... .. code:: ! interface FastEthernet0/1 switchport access vlan 532 spanning-tree vlan 532 cost 3 ! interface FastEthernet0/2 switchport access vlan 300 spanning-tree portfast ! interface FastEthernet0/2 duplex full speed 100 switchport access vlan 300 spanning-tree portfast ! The following interfaces should be returned: .. code:: interface FastEthernet0/1 interface FastEthernet0/2 We do this by quering `find_parents_wo_child()`; we set our parent as `^interface` and set the child as `speed\s\d+` (a regular-expression which matches the word 'speed' followed by an integer). .. code-block:: python :emphasize-lines: 18 >>> config = ['!', ... 'interface FastEthernet0/1', ... ' switchport access vlan 532', ... ' spanning-tree vlan 532 cost 3', ... '!', ... 'interface FastEthernet0/2', ... ' switchport access vlan 300', ... ' spanning-tree portfast', ... '!', ... 'interface FastEthernet0/3', ... ' duplex full', ... ' speed 100', ... ' switchport access vlan 300', ... ' spanning-tree portfast', ... '!', ... ] >>> p = CiscoConfParse(config) >>> p.find_parents_wo_child('^interface', 'speed\s\d+') ['interface FastEthernet0/1', 'interface FastEthernet0/2'] >>> """ tmp = self.find_objects_wo_child( parentspec, childspec, ignore_ws=ignore_ws) return list(map(attrgetter('text'), tmp)) def find_children_w_parents(self, parentspec, childspec, ignore_ws=False): r"""Parse through the children of all parents matching parentspec, and return a list of children that matched the childspec. Args: - parentspec (str): Text regular expression for the line to be matched; this must match the parent's line - childspec (str): Text regular expression for the line to be matched; this must match the child's line Kwargs: - ignore_ws (bool): boolean that controls whether whitespace is ignored Returns: - list. A list of matching child configuration lines This example finds the port-security lines on FastEthernet0/1 in following config... .. code:: ! interface FastEthernet0/1 switchport access vlan 532 switchport port-security switchport port-security violation protect switchport port-security aging time 5 switchport port-security aging type inactivity spanning-tree portfast spanning-tree bpduguard enable ! interface FastEthernet0/2 switchport access vlan 300 spanning-tree portfast spanning-tree bpduguard enable ! interface FastEthernet0/2 duplex full speed 100 switchport access vlan 300 spanning-tree portfast spanning-tree bpduguard enable ! The following lines should be returned: .. code:: switchport port-security switchport port-security violation protect switchport port-security aging time 5 switchport port-security aging type inactivity We do this by quering `find_children_w_parents()`; we set our parent as `^interface` and set the child as `switchport port-security`. .. code-block:: python :emphasize-lines: 25 >>> config = ['!', ... 'interface FastEthernet0/1', ... ' switchport access vlan 532', ... ' switchport port-security', ... ' switchport port-security violation protect', ... ' switchport port-security aging time 5', ... ' switchport port-security aging type inactivity', ... ' spanning-tree portfast', ... ' spanning-tree bpduguard enable', ... '!', ... 'interface FastEthernet0/2', ... ' switchport access vlan 300', ... ' spanning-tree portfast', ... ' spanning-tree bpduguard enable', ... '!', ... 'interface FastEthernet0/3', ... ' duplex full', ... ' speed 100', ... ' switchport access vlan 300', ... ' spanning-tree portfast', ... ' spanning-tree bpduguard enable', ... '!', ... ] >>> p = CiscoConfParse(config) >>> p.find_children_w_parents('^interface\sFastEthernet0/1', ... 'port-security') [' switchport port-security', ' switchport port-security violation protect', ' switchport port-security aging time 5', ' switchport port-security aging type inactivity'] >>> """ if ignore_ws: parentspec = self._build_space_tolerant_regex(parentspec) childspec = self._build_space_tolerant_regex(childspec) retval = set([]) childobjs = self._find_line_OBJ(childspec) for child in childobjs: parents = child.all_parents for parent in parents: if re.search(parentspec, parent.text): retval.add(child) return list(map(attrgetter('text'), sorted(retval))) def find_objects_w_parents(self, parentspec, childspec, ignore_ws=False): r"""Parse through the children of all parents matching parentspec, and return a list of child objects, which matched the childspec. Args: - parentspec (str): Text regular expression for the line to be matched; this must match the parent's line - childspec (str): Text regular expression for the line to be matched; this must match the child's line Kwargs: - ignore_ws (bool): boolean that controls whether whitespace is ignored Returns: - list. A list of matching child objects This example finds the object for "ge-0/0/0" under "interfaces" in the following config... .. code:: interfaces ge-0/0/0 unit 0 family ethernet-switching port-mode access vlan members VLAN_FOO ge-0/0/1 unit 0 family ethernet-switching port-mode trunk vlan members all native-vlan-id 1 vlan unit 0 family inet address 172.16.15.5/22 The following object should be returned: .. code:: We do this by quering `find_childobj_w_parents()`; we set our parent as `^\s*interface` and set the child as `^\s+ge-0/0/1`. .. code-block:: python :emphasize-lines: 21,22 >>> config = ['interfaces', ... ' ge-0/0/0', ... ' unit 0', ... ' family ethernet-switching', ... ' port-mode access', ... ' vlan', ... ' members VLAN_FOO', ... ' ge-0/0/1', ... ' unit 0', ... ' family ethernet-switching', ... ' port-mode trunk', ... ' vlan', ... ' members all', ... ' native-vlan-id 1', ... ' vlan', ... ' unit 0', ... ' family inet', ... ' address 172.16.15.5/22', ... ] >>> p = CiscoConfParse(config) >>> p.find_objects_w_parents('^\s*interfaces', ... r'\s+ge-0/0/1') [] >>> """ if ignore_ws: parentspec = self._build_space_tolerant_regex(parentspec) childspec = self._build_space_tolerant_regex(childspec) retval = set([]) childobjs = self._find_line_OBJ(childspec) for child in childobjs: parents = child.all_parents for parent in parents: if re.search(parentspec, parent.text): retval.add(child) return sorted(retval) def find_lineage(self, linespec, exactmatch=False): """Iterate through to the oldest ancestor of this object, and return a list of all ancestors / children in the direct line. Cousins or aunts / uncles are *not* returned. Note, all children of this object are returned.""" tmp = self.find_objects(linespec, exactmatch=exactmatch) if len(tmp) > 1: raise ValueError("linespec must be unique") return [obj.text for obj in tmp[0].lineage] def has_line_with(self, linespec): return self.ConfigObjs.has_line_with(linespec) def insert_before(self, linespec, insertstr="", exactmatch=False, ignore_ws=False, atomic=False): """Find all objects whose text matches linespec, and insert 'insertstr' before those line objects""" objs = self.find_objects(linespec, exactmatch, ignore_ws) last_idx = len(objs) - 1 local_atomic = False & atomic for idx, obj in enumerate(objs): if (idx == last_idx): local_atomic = True & atomic self.ConfigObjs.insert_before(obj, insertstr, atomic=local_atomic) ## Return the matching lines return list(map(attrgetter('text'), sorted(objs))) def insert_after(self, linespec, insertstr="", exactmatch=False, ignore_ws=False, atomic=False): """Find all :class:`~models_cisco.IOSCfgLine` objects whose text matches ``linespec``, and insert ``insertstr`` after those line objects""" objs = self.find_objects(linespec, exactmatch, ignore_ws) last_idx = len(objs) - 1 local_atomic = False & atomic for idx, obj in enumerate(objs): if idx == last_idx: local_atomic = True & atomic self.ConfigObjs.insert_after(obj, insertstr, atomic=local_atomic) ## Return the matching lines return list(map(attrgetter('text'), sorted(objs))) def insert_after_child(self, parentspec, childspec, insertstr="", exactmatch=False, excludespec=None, ignore_ws=False, atomic=False): """Find all :class:`~models_cisco.IOSCfgLine` objects whose text matches ``linespec`` and have a child matching ``childspec``, and insert an :class:`~models_cisco.IOSCfgLine` object for ``insertstr`` after those child objects.""" retval = list() modified = False for pobj in self._find_line_OBJ(parentspec, exactmatch=exactmatch): if excludespec and re.search(excludespec, pobj.text): # Exclude replacements on pobj lines which match excludespec continue for cobj in pobj.children: if excludespec and re.search(excludespec, cobj.text): # Exclude replacements on pobj lines which match excludespec continue elif re.search(childspec, cobj.text): modified = True retval.append( self.ConfigObjs.insert_after( cobj, insertstr, atomic=atomic)) else: pass return retval def delete_lines(self, linespec, exactmatch=False, ignore_ws=False): """Find all :class:`~models_cisco.IOSCfgLine` objects whose text matches linespec, and delete the object""" objs = self.find_objects(linespec, exactmatch, ignore_ws) for obj in reversed(objs): del self.ConfigObjs[obj.linenum] def prepend_line(self, linespec): """Unconditionally insert an :class:`~models_cisco.IOSCfgLine` object for ``linespec`` (a text line) at the top of the configuration""" self.ConfigObjs.insert(0, linespec) return self.ConfigObjs[0] def append_line(self, linespec): """Unconditionally insert ``linespec`` (a text line) at the end of the configuration Args: - linespec (str): Text IOS configuration line Returns: - The parsed :class:`~models_cisco.IOSCfgLine` instance """ self.ConfigObjs.append(linespec) return self.ConfigObjs[-1] def replace_lines(self, linespec, replacestr, excludespec=None, exactmatch=False, atomic=False): """This method is a text search and replace (Case-sensitive). You can optionally exclude lines from replacement by including a string (or compiled regular expression) in `excludespec`. Args: - linespec (str): Text regular expression for the line to be matched - replacestr (str): Text used to replace strings matching linespec Kwargs: - excludespec (str): Text regular expression used to reject lines, which would otherwise be replaced. Default value of ``excludespec`` is None, which means nothing is excluded - exactmatch (bool): boolean that controls whether partial matches are valid - atomic (bool): boolean that controls whether the config is reparsed after replacement (default True) Returns: - list. A list of changed configuration lines This example finds statements with `EXTERNAL_CBWFQ` in following config, and replaces all matching lines (in-place) with `EXTERNAL_QOS`. For the purposes of this example, let's assume that we do *not* want to make changes to any descriptions on the policy. .. code:: ! policy-map EXTERNAL_CBWFQ description implement an EXTERNAL_CBWFQ policy class IP_PREC_HIGH priority percent 10 police cir percent 10 conform-action transmit exceed-action drop class IP_PREC_MEDIUM bandwidth percent 50 queue-limit 100 class class-default bandwidth percent 40 queue-limit 100 policy-map SHAPE_HEIR class ALL shape average 630000 service-policy EXTERNAL_CBWFQ ! We do this by calling `replace_lines(linespec='EXTERNAL_CBWFQ', replacestr='EXTERNAL_QOS', excludespec='description')`... .. code-block:: python :emphasize-lines: 23 >>> from ciscoconfparse import CiscoConfParse >>> config = ['!', ... 'policy-map EXTERNAL_CBWFQ', ... ' description implement an EXTERNAL_CBWFQ policy', ... ' class IP_PREC_HIGH', ... ' priority percent 10', ... ' police cir percent 10', ... ' conform-action transmit', ... ' exceed-action drop', ... ' class IP_PREC_MEDIUM', ... ' bandwidth percent 50', ... ' queue-limit 100', ... ' class class-default', ... ' bandwidth percent 40', ... ' queue-limit 100', ... 'policy-map SHAPE_HEIR', ... ' class ALL', ... ' shape average 630000', ... ' service-policy EXTERNAL_CBWFQ', ... '!', ... ] >>> p = CiscoConfParse(config) >>> p.replace_lines('EXTERNAL_CBWFQ', 'EXTERNAL_QOS', 'description') ['policy-map EXTERNAL_QOS', ' service-policy EXTERNAL_QOS'] >>> Now when we call `p.find_blocks('policy-map EXTERNAL_QOS')`, we get the changed configuration, which has the replacements except on the policy-map's description. >>> p.find_blocks('EXTERNAL_QOS') ['policy-map EXTERNAL_QOS', ' description implement an EXTERNAL_CBWFQ policy', ' class IP_PREC_HIGH', ' class IP_PREC_MEDIUM', ' class class-default', 'policy-map SHAPE_HEIR', ' class ALL', ' shape average 630000', ' service-policy EXTERNAL_QOS'] >>> """ retval = list() ## Since we are replacing text, we *must* operate on ConfigObjs if excludespec: excludespec_re = re.compile(excludespec) for obj in self._find_line_OBJ(linespec, exactmatch=exactmatch): if excludespec and excludespec_re.search(obj.text): # Exclude replacements on lines which match excludespec continue retval.append(obj.re_sub(linespec, replacestr)) if self.factory and atomic: #self.ConfigObjs._reassign_linenums() self.ConfigObjs._bootstrap_from_text() return retval def replace_children(self, parentspec, childspec, replacestr, excludespec=None, exactmatch=False, atomic=False): r"""Replace lines matching `childspec` within the `parentspec`'s immediate children. Args: - parentspec (str): Text IOS configuration line - childspec (str): Text IOS configuration line, or regular expression - replacestr (str): Text IOS configuration, which should replace text matching ``childspec``. Kwargs: - excludespec (str): A regular expression, which indicates ``childspec`` lines which *must* be skipped. If ``excludespec`` is None, no lines will be excluded. - exactmatch (bool): Defaults to False. When set True, this option requires ``linespec`` match the whole configuration line, instead of a portion of the configuration line. Returns: - list. A list of changed :class:`~models_cisco.IOSCfgLine` instances. `replace_children()` just searches through a parent's child lines and replaces anything matching `childspec` with `replacestr`. This method is one of my favorites for quick and dirty standardization efforts if you *know* the commands are already there (just set inconsistently). One very common use case is rewriting all vlan access numbers in a configuration. The following example sets `storm-control broadcast level 0.5` on all GigabitEthernet ports. .. code-block:: python :emphasize-lines: 13 >>> from ciscoconfparse import CiscoConfParse >>> config = ['!', ... 'interface GigabitEthernet1/1', ... ' description {I have a broken storm-control config}', ... ' switchport', ... ' switchport mode access', ... ' switchport access vlan 50', ... ' switchport nonegotiate', ... ' storm-control broadcast level 0.2', ... '!' ... ] >>> p = CiscoConfParse(config) >>> p.replace_children(r'^interface\sGigabit', r'broadcast\slevel\s\S+', 'broadcast level 0.5') [' storm-control broadcast level 0.5'] >>> One thing to remember about the last example, you *cannot* use a regular expression in `replacestr`; just use a normal python string. """ retval = list() ## Since we are replacing text, we *must* operate on ConfigObjs childspec_re = re.compile(childspec) if excludespec: excludespec_re = re.compile(excludespec) for pobj in self._find_line_OBJ(parentspec, exactmatch=exactmatch): if excludespec and excludespec_re.search(pobj.text): # Exclude replacements on pobj lines which match excludespec continue for cobj in pobj.children: if excludespec and excludespec_re.search(cobj.text): # Exclude replacements on pobj lines which match excludespec continue elif childspec_re.search(cobj.text): retval.append(cobj.re_sub(childspec, replacestr)) else: pass if self.factory and atomic: #self.ConfigObjs._reassign_linenums() self.ConfigObjs._bootstrap_from_text() return retval def replace_all_children(self, parentspec, childspec, replacestr, excludespec=None, exactmatch=False, atomic=False): """Replace lines matching `childspec` within all children (recursive) of lines whilch match `parentspec`""" retval = list() ## Since we are replacing text, we *must* operate on ConfigObjs childspec_re = re.compile(childspec) if excludespec: excludespec_re = re.compile(excludespec) for pobj in self._find_line_OBJ(parentspec, exactmatch=exactmatch): if excludespec and excludespec_re.search(pobj.text): # Exclude replacements on pobj lines which match excludespec continue for cobj in self._find_all_child_OBJ(pobj): if excludespec and excludespec_re.search(cobj.text): # Exclude replacements on pobj lines which match excludespec continue elif childspec_re.search(cobj.text): retval.append(cobj.re_sub(childspec, replacestr)) else: pass if self.factory and atomic: #self.ConfigObjs._reassign_linenums() self.ConfigObjs._bootstrap_from_text() return retval def re_search_children(self, regex, recurse=False): """Use ``regex`` to search for root parents in the config with text matching regex. If recurse is False, only root parent objects are returned. Args: - regex (str): A string or python regular expression, which should be matched. - recurse (bool): Set True if you want to search all objects, and not just the root parents Returns: - list. A list of matching :class:`~models_cisco.IOSCfgLine` objects which matched. If there is no match, an empty :py:func:`list` is returned. """ ## I implemented this method in response to Github issue #156 if recurse is False: # Only return the matching oldest ancestor objects... return [obj for obj in self.find_objects(regex) \ if (obj.parent is obj)] else: # Return any matching object return [obj for obj in self.find_objects(regex)] def re_match_iter_typed(self, regex, group=1, result_type=str, default='', untyped_default=False): r"""Use ``regex`` to search the root parents in the config and return the contents of the regular expression group, at the integer ``group`` index, cast as ``result_type``; if there is no match, ``default`` is returned. Args: - regex (str): A string or python compiled regular expression, which should be matched. This regular expression should contain parenthesis, which bound a match group. Kwargs: - group (int): An integer which specifies the desired regex group to be returned. ``group`` defaults to 1. - result_type (type): A type (typically one of: ``str``, ``int``, ``float``, or :class:`~ccp_util.IPv4Obj`). All returned values are cast as ``result_type``, which defaults to ``str``. - default (any): The default value to be returned, if there is no match. - untyped_default (bool): Set True if you don't want the default value to be typed Returns: - ``result_type``. The text matched by the regular expression group; if there is no match, ``default`` is returned. All values are cast as ``result_type``. This example illustrates how you can use :func:`~ciscoconfparse.re_match_iter_typed` to get the first interface name listed in the config. >>> from ciscoconfparse import CiscoConfParse >>> config = [ ... '!', ... 'interface Serial1/0', ... ' ip address 1.1.1.1 255.255.255.252', ... '!', ... 'interface Serial2/0', ... ' ip address 1.1.1.5 255.255.255.252', ... '!', ... ] >>> parse = CiscoConfParse(config) >>> INTF_RE = re.compile(r'interface\s(\S+)') >>> parse.re_match_iter_typed(INTF_RE) Serial1/0 >>> """ ## iterate through root objects, and return the matching value ## (cast as result_type) from the first object.text that matches regex #if (default is True): ## Not using self.re_match_iter_typed(default=True), because I want ## to be sure I build the correct API for match=False ## ## Ref IOSIntfLine.has_dtp for an example of how to code around ## this while I build the API # raise NotImplementedError for cobj in self.ConfigObjs: # Only process parent objects at the root of the tree... if cobj.parent is not cobj: continue mm = re.search(regex, cobj.text) if not (mm is None): return result_type(mm.group(group)) ## Ref Github issue #121 if untyped_default: return default else: return result_type(default) def req_cfgspec_all_diff(self, cfgspec, ignore_ws=False): """ req_cfgspec_all_diff takes a list of required configuration lines, parses through the configuration, and ensures that none of cfgspec's lines are missing from the configuration. req_cfgspec_all_diff returns a list of missing lines from the config. One example use of this method is when you need to enforce routing protocol standards, or standards against interface configurations. **Example** .. doctest:: >>> config = [ ... 'logging trap debugging', ... 'logging 172.28.26.15', ... ] >>> p = CiscoConfParse(config) >>> required_lines = [ ... "logging 172.28.26.15", ... "logging 172.16.1.5", ... ] >>> diffs = p.req_cfgspec_all_diff(required_lines) >>> diffs ['logging 172.16.1.5'] >>> """ rgx = dict() if ignore_ws: for line in cfgspec: rgx[line] = self._build_space_tolerant_regex(line) skip_cfgspec = dict() retval = list() matches = self._find_line_OBJ("[a-zA-Z]") ## Make a list of unnecessary cfgspec lines for lineobj in matches: for reqline in cfgspec: if ignore_ws: if re.search(r'^'+rgx[reqline]+'$', lineobj.text.strip()): skip_cfgspec[reqline] = True else: if lineobj.text.strip() == reqline.strip(): skip_cfgspec[reqline] = True ## Add items to be configured ## TODO: Find a way to add the parent of the missing lines for line in cfgspec: if not skip_cfgspec.get(line, False): retval.append(line) return retval def req_cfgspec_excl_diff(self, linespec, uncfgspec, cfgspec): r""" req_cfgspec_excl_diff accepts a linespec, an unconfig spec, and a list of required configuration elements. Return a list of configuration diffs to make the configuration comply. **All** other config lines matching the linespec that are *not* listed in the cfgspec will be removed with the uncfgspec regex. Uses for this method include the need to enforce syslog, acl, or aaa standards. **Example** .. doctest:: >>> config = [ ... 'logging trap debugging', ... 'logging 172.28.26.15', ... ] >>> p = CiscoConfParse(config) >>> required_lines = [ ... "logging 172.16.1.5", ... "logging 1.10.20.30", ... "logging 192.168.1.1", ... ] >>> linespec = "logging\s+\d+\.\d+\.\d+\.\d+" >>> unconfspec = linespec >>> diffs = p.req_cfgspec_excl_diff(linespec, unconfspec, ... required_lines) >>> diffs ['no logging 172.28.26.15', 'logging 172.16.1.5', 'logging 1.10.20.30', 'logging 192.168.1.1'] >>> """ violate_objs = list() uncfg_objs = list() skip_cfgspec = dict() retval = list() matches = self._find_line_OBJ(linespec) ## Make a list of lineobject violations for lineobj in matches: # Look for config lines to unconfigure accept_lineobj = False for reqline in cfgspec: if (lineobj.text.strip() == reqline.strip()): accept_lineobj = True skip_cfgspec[reqline] = True if (accept_lineobj is False): # If a violation is found... violate_objs.append(lineobj) result = re.search(uncfgspec, lineobj.text) # add uncfgtext to the violator's lineobject lineobj.add_uncfgtext(result.group(0)) ## Make the list of unconfig objects, recurse through parents for vobj in violate_objs: parent_objs = vobj.all_parents for parent_obj in parent_objs: uncfg_objs.append(parent_obj) uncfg_objs.append(vobj) retval = self._objects_to_uncfg(uncfg_objs, violate_objs) ## Add missing lines... ## TODO: Find a way to add the parent of the missing lines for line in cfgspec: if not skip_cfgspec.get(line, False): retval.append(line) return retval def _sequence_nonparent_lines(self, a_nonparent_objs, b_nonparent_objs): """Assume a_nonparent_objs is the existing config sequence, and b_nonparent_objs is the *desired* config sequence This method walks b_nonparent_objs, and orders a_nonparent_objs the same way (as much as possible) This method returns: - The reordered list of a_nonparent_objs - The reordered list of a_nonparent_lines - The reordered list of a_nonparent_linenums """ a_parse = CiscoConfParse([]) # A *new* parse for reordered a lines a_lines = list() a_linenums = list() ## Mark all a objects as not done for aobj in a_nonparent_objs: aobj.done = False for bobj in b_nonparent_objs: for aobj in a_nonparent_objs: if aobj.text == bobj.text: aobj.done = True a_parse.append_line(aobj.text) # Add any missing a_parent_objs + their children... for aobj in a_nonparent_objs: if aobj.done is False: aobj.done = True a_parse.append_line(aobj.text) a_parse.commit() a_nonparents_reordered = a_parse.ConfigObjs for aobj in a_nonparents_reordered: a_lines.append(aobj.text) a_linenums.append(aobj.linenum) return a_parse, a_lines, a_linenums def _sequence_parent_lines(self, a_parent_objs, b_parent_objs): """Assume a_parent_objs is the existing config sequence, and b_parent_objs is the *desired* config sequence This method walks b_parent_objs, and orders a_parent_objs the same way (as much as possible) This method returns: - The reordered list of a_parent_objs - The reordered list of a_parent_lines - The reordered list of a_parent_linenums """ a_parse = CiscoConfParse([]) # A *new* parse for reordered a lines a_lines = list() a_linenums = list() ## Mark all a objects as not done for aobj in a_parent_objs: aobj.done = False for child in aobj.all_children: child.done = False ## Walk the b objects by parent, then child and reorder a objects for bobj in b_parent_objs: for aobj in a_parent_objs: if aobj.text == bobj.text: aobj.done = True a_parse.append_line(aobj.text) # Append *matching* children to this aobj in the same order for bchild in bobj.all_children: for achild in aobj.all_children: if achild.done: continue elif achild.geneology_text == bchild.geneology_text: achild.done = True a_parse.append_line(achild.text) # Append *missing* children to this aobj... for achild in aobj.all_children: if achild.done is False: achild.done = True a_parse.append_line(achild.text) # Add any missing a_parent_objs + their children... for aobj in a_parent_objs: if aobj.done is False: aobj.done = True a_parse.append_line(aobj.text) for achild in aobj.all_children: achild.done = True a_parse.append_line(achild.text) a_parse.commit() a_parents_reordered = a_parse.ConfigObjs for aobj in a_parents_reordered: a_lines.append(aobj.text) a_linenums.append(aobj.linenum) return a_parse, a_lines, a_linenums def sync_diff(self, cfgspec, linespec, uncfgspec=None, ignore_order=True, remove_lines=True, debug=False): r""" ``sync_diff()`` accepts a list of required configuration elements, a linespec, and an unconfig spec. This method return a list of configuration diffs to make the configuration comply with cfgspec. Args: - cfgspec (list): A list of required configuration lines - linespec (str): A regular expression, which filters lines to be diff'd Kwargs: - uncfgspec (str): A regular expression, which is used to unconfigure lines. When ciscoconfparse removes a line, it takes the entire portion of the line that matches ``uncfgspec``, and prepends "no" to it. - ignore_order (bool): Indicates whether the configuration should be reordered to minimize the number of diffs. Default: True (usually it's a good idea to leave ``ignore_order`` True, except for ACL comparisions) - remove_lines (bool): Indicates whether the lines which are *not* in ``cfgspec`` should be removed. Default: True. When ``remove_lines`` is True, all other config lines matching the linespec that are *not* listed in the cfgspec will be removed with the uncfgspec regex. - debug (bool): Miscellaneous debugging; Default: False Returns: - list. A list of string configuration diffs Uses for this method include the need to enforce syslog, acl, or aaa standards. **Example** >>> config = [ ... 'logging trap debugging', ... 'logging 172.28.26.15', ... ] >>> p = CiscoConfParse(config) >>> required_lines = [ ... "logging 172.16.1.5", ... "logging 1.10.20.30", ... "logging 192.168.1.1", ... ] >>> linespec = "logging\s+\d+\.\d+\.\d+\.\d+" >>> unconfspec = linespec >>> diffs = p.sync_diff(required_lines, ... linespec, unconfspec) # doctest: +SKIP >>> diffs # doctest: +SKIP ['no logging 172.28.26.15', 'logging 172.16.1.5', 'logging 1.10.20.30', 'logging 192.168.1.1'] >>> """ tmp = self._find_line_OBJ(linespec) if (uncfgspec is None): uncfgspec = linespec a_lines = map(lambda x: x.text, tmp) a = CiscoConfParse(a_lines) b = CiscoConfParse(cfgspec, factory=False) b_lines = b.ioscfg a_hierarchy = list() b_hierarchy = list() ## Build heirarchical, equal-length lists of parents / non-parents a_parents, a_nonparents = a.ConfigObjs.config_hierarchy() b_parents, b_nonparents = b.ConfigObjs.config_hierarchy() obj = DiffObject(0, a_nonparents, a_parents) a_hierarchy.append(obj) obj = DiffObject(0, b_nonparents, b_parents) b_hierarchy.append(obj) retval = list() ## Assign config_this and unconfig_this attributes by "diff level" for adiff_level, bdiff_level in zip(a_hierarchy, b_hierarchy): for attr in ['parents', 'nonparents']: if attr == 'parents': if ignore_order: a_parents = getattr(adiff_level, attr) b_parents = getattr(bdiff_level, attr) # Rewrite a, since we reordered everything a, a_lines, a_linenums = self._sequence_parent_lines( a_parents, b_parents) else: a_lines = list() a_linenums = list() for obj in adiff_level.parents: a_lines.append(obj.text) a_linenums.append(obj.linenum) a_lines.extend( map(lambda x: getattr(x, 'text'), obj.all_children)) a_linenums.extend( map(lambda x: getattr(x, 'linenum'), obj.all_children)) b_lines = list() b_linenums = list() for obj in bdiff_level.parents: b_lines.append(obj.text) b_linenums.append(obj.linenum) b_lines.extend( map(lambda x: getattr(x, 'text'), obj.all_children)) b_linenums.extend( map(lambda x: getattr(x, 'linenum'), obj.all_children)) else: if ignore_order: a_nonparents = getattr(adiff_level, attr) b_nonparents = getattr(bdiff_level, attr) # Rewrite a, since we reordered everything a, a_lines, a_linenums = self._sequence_nonparent_lines( a_nonparents, b_nonparents) else: a_lines = map(lambda x: getattr(x, 'text'), getattr(adiff_level, attr)) # Build a map from a_lines index to a.ConfigObjs index a_linenums = map(lambda x: getattr(x, 'linenum'), getattr(adiff_level, attr)) b_lines = map(lambda x: getattr(x, 'text'), getattr(bdiff_level, attr)) # Build a map from b_lines index to b.ConfigObjs index b_linenums = map(lambda x: getattr(x, 'linenum'), getattr(bdiff_level, attr)) ### ### Mark diffs here ### # Get a SequenceMatcher instance to calculate diffs at this level matcher = SequenceMatcher(isjunk=None, a=a_lines, b=b_lines) # Use the SequenceMatcher instance to label objects appropriately: # - tag is the diff evaluation: equal, replace, insert, or delete # - i1 and i2 are the begin and end points for arg a # - j1 and j2 are the begin and end points for arg b for tag, i1, i2, j1, j2 in matcher.get_opcodes(): #print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % (tag, i1, i2, a_lines[i1:i2], j1, j2, b_lines[j1:j2])) if debug or self.debug: _log.debug("TAG='{0}'".format(tag)) # if tag=='equal', check whether the parent objs are the same # if parent objects are the same, then do nothing # if parent objects are different, then delete a & config b # if tag=='replace' # delete a & config b # if tag=='insert', then configure b aobjs = list() # List of a IOSCfgLine objects at this level bobjs = list() # List of b IOSCfgLine objects at this level for num in range(i1, i2): aobj = a.ConfigObjs[a_linenums[num]] aobjs.append(aobj) for num in range(j1, j2): bobj = b.ConfigObjs[b_linenums[num]] bobjs.append(bobj) max_len = max(len(aobjs), len(bobjs)) for idx in range(0, max_len): try: aobj = aobjs[idx] # set aparent_text to all parents' text (joined) aparent_text = ' '.join( map(lambda x: x.text, aobj.all_parents)) except IndexError: # aobj doesn't exist, if we get an index error # fake some data... aobj = None aparent_text = '__ANOTHING__' if debug or self.debug: _log.debug(" aobj:'{0}'".format(aobj)) _log.debug(" aobj parents:'{0}'".format( aparent_text)) try: bobj = bobjs[idx] # set bparent_text to all parents' text (joined) bparent_text = ' '.join( map(lambda x: x.text, bobj.all_parents)) except IndexError: # bobj doesn't exist, if we get an index error # fake some data... bobj = None bparent_text = '__BNOTHING__' if debug or self.debug: _log.debug(" bobj:'{0}'".format(bobj)) _log.debug(" bobj parents:'{0}'".format( bparent_text)) if (tag == 'equal'): # If the diff claims that these lines are equal, they # aren't truly equal unless parents match if aparent_text != bparent_text: if debug or self.debug: _log.debug( " tagged 'equal', aparent_text!=bparent_text" ) # a & b parents are *not* the same # therefore a & b are not equal if aobj: # Only configure parent if it's not already # slated for removal if not getattr(aobj.parent, 'unconfig_this', False): aobj.parent.config_this = True aobj.unconfig_this = True if debug: _log.debug(" unconfigure aobj") if bobj: bobj.config_this = True bobj.parent.config_this = True if debug: _log.debug(" configure bobj") elif aparent_text == bparent_text: # Both a & b parents match, so these lines are equal aobj.unconfig_this = False bobj.config_this = False if debug: _log.debug( " tagged 'equal', aparent_text==bparent_text" ) _log.debug( " do nothing with aobj / bobj") elif (tag == 'replace'): # tag: replace, I'm not going to check parents for now if debug: _log.debug(" tagged 'replace'") if aobj: # Only configure parent if it's not already # slated for removal if not getattr(aobj.parent, 'unconfig_this', False): aobj.parent.config_this = True aobj.unconfig_this = True if debug: _log.debug(" unconfigure aobj") if bobj: bobj.config_this = True bobj.parent.config_this = True if debug: _log.debug(" configure bobj") elif (tag == 'insert'): if debug: _log.debug(" tagged 'insert'") # I don't think tag: insert ever applies to a objects... if aobj: # Only configure parent if it's not already # slated for removal if not getattr(aobj.parent, 'unconfig_this', False): aobj.parent.config_this = True aobj.unconfig_this = True if debug: _log.debug(" unconfigure aobj") # tag: insert certainly applies to b objects... if bobj: bobj.config_this = True bobj.parent.config_this = True if debug: _log.debug(" configure bobj") elif (tag == 'delete'): # NOTE: I'm not deleting b objects, for now if debug: _log.debug(" tagged 'delete'") if aobj: # Only configure parent if it's not already # slated for removal for pobj in aobj.all_parents: if not getattr(pobj, 'unconfig_this', False): pobj.config_this = True aobj.unconfig_this = True if debug: _log.debug(" unconfigure aobj") else: raise ValueError("Unknown action: {0}".format(tag)) ### ### Write a object diffs here ### ## Unconfigure A objects, at *each level*, as required for obj in a.ConfigObjs: if remove_lines and getattr(obj, 'unconfig_this', False): ## FIXME: This should only be applied to IOS and ASA configs if uncfgspec: mm = re.search(uncfgspec, obj.text) if not (mm is None): obj.add_uncfgtext(mm.group(0)) retval.append(obj.uncfgtext) else: retval.append(" " * obj.indent + "no " + obj.text.lstrip()) else: retval.append(" " * obj.indent + "no " + obj.text.lstrip()) elif remove_lines and getattr(obj, 'config_this', False): retval.append(obj.text) # Clean up the attributes we used temporarily in this method for attr in ['config_this', 'unconfig_this']: try: delattr(obj.text, attr) except: pass ### ### Write b object diffs here ### for obj in b.ConfigObjs: if getattr(obj, 'config_this', False): retval.append(obj.text) # Clean up the attributes we used temporarily in this method try: delattr(obj.text, 'config_this') except: pass ## Strip out 'double negatives' (i.e. 'no no ') for idx in range(0, len(retval)): retval[idx] = re.sub(r'(\s+)no\s+no\s+(\S+.+?)$', r'\g<1>\g<2>', retval[idx]) if debug: _log.debug("Completed diff:") for line in retval: _log.debug("'{0}'".format(line)) return retval def save_as(self, filepath): """Save a text copy of the configuration at ``filepath``; this method uses the OperatingSystem's native line separators (such as ``\\r\\n`` in Windows).""" try: with open(filepath, 'w') as newconf: for line in self.ioscfg: newconf.write(line + '\n') return True except Exception as e: raise e ### The methods below are marked SEMI-PRIVATE because they return an object ### or iterable of objects instead of the configuration text itself. def _build_space_tolerant_regex(self, linespec): r"""SEMI-PRIVATE: Accept a string, and return a string with all spaces replaced with '\s+'""" # Unicode below... backslash = '\x5c' # escaped_space = "\\s+" (not a raw string) if (sys.version_info>=(3, 0, 0,)): escaped_space = (backslash + backslash + "s+").translate('utf-8') else: escaped_space = backslash + backslash + "s+" LINESPEC_LIST_TYPE = bool(getattr(linespec, 'append', False)) if not LINESPEC_LIST_TYPE: assert bool(getattr(linespec, 'upper', False)) # Ensure it's a str linespec = re.sub(r'\s+', escaped_space, linespec) else: for idx in range(0, len(linespec)): ## Ensure this element is a string assert bool(getattr(linespec[idx], 'upper', False)) linespec[idx] = re.sub(r'\s+', escaped_space, linespec[idx]) return linespec def _find_line_OBJ(self, linespec, exactmatch=False): """SEMI-PRIVATE: Find objects whose text matches the linespec""" ## NOTE TO SELF: do not remove _find_line_OBJ(); used by Cisco employees if not exactmatch: # Return objects whose text attribute matches linespec linespec_re = re.compile(linespec) elif exactmatch: # Return objects whose text attribute matches linespec exactly linespec_re = re.compile("^%s$" % linespec) return list( filter(lambda obj: linespec_re.search(obj.text), self.ConfigObjs)) def _find_sibling_OBJ(self, lineobject): """SEMI-PRIVATE: Takes a singe object and returns a list of sibling objects""" siblings = lineobject.parent.children return siblings def _find_all_child_OBJ(self, lineobject): """SEMI-PRIVATE: Takes a single object and returns a list of decendants in all 'children' / 'grandchildren' / etc... after it. It should NOT return the children of siblings""" # sort the list, and get unique objects retval = set(lineobject.children) for candidate in lineobject.children: if candidate.has_children: for child in candidate.children: retval.add(child) retval = sorted(retval) return retval def _unique_OBJ(self, objectlist): """SEMI-PRIVATE: Returns a list of unique objects (i.e. with no duplicates). The returned value is sorted by configuration line number (lowest first)""" retval = set([]) for obj in objectlist: retval.add(obj) return sorted(retval) def _objects_to_uncfg(self, objectlist, unconflist): # Used by req_cfgspec_excl_diff() retval = list() unconfdict = dict() for unconf in unconflist: unconfdict[unconf] = "DEFINED" for obj in self._unique_OBJ(objectlist): if (unconfdict.get(obj, None) == "DEFINED"): retval.append(obj.uncfgtext) else: retval.append(obj.text) return retval #########################################################################3 class IOSConfigList(MutableSequence): """A custom list to hold :class:`~models_cisco.IOSCfgLine` objects. Most people will never need to use this class directly. """ def __init__(self, data=None, comment_delimiter='!', debug=False, factory=False, ignore_blank_lines=True, syntax='ios', CiscoConfParse=None): """Initialize the class. Kwargs: - data (list): A list of parsed :class:`~models_cisco.IOSCfgLine` objects - comment (str): A comment delimiter. This should only be changed when parsing non-Cisco IOS configurations, which do not use a ! as the comment delimiter. ``comment`` defaults to '!' - debug (bool): ``debug`` defaults to False, and should be kept that way unless you're working on a very tricky config parsing problem. Debug output is not particularly friendly - ignore_blank_lines (bool): ``ignore_blank_lines`` defaults to True; when this is set True, ciscoconfparse ignores blank configuration lines. You might want to set ``ignore_blank_lines`` to False if you intentionally use blank lines in your configuration (ref: Github Issue #2). Returns: - An instance of an :class:`~ciscoconfparse.IOSConfigList` object. """ #data = kwargs.get('data', None) #comment_delimiter = kwargs.get('comment_delimiter', '!') #debug = kwargs.get('debug', False) #factory = kwargs.get('factory', False) #ignore_blank_lines = kwargs.get('ignore_blank_lines', True) #syntax = kwargs.get('syntax', 'ios') #CiscoConfParse = kwargs.get('CiscoConfParse', None) super(IOSConfigList, self).__init__() self._list = list() self.CiscoConfParse = CiscoConfParse self.comment_delimiter = comment_delimiter self.factory = factory self.ignore_blank_lines = ignore_blank_lines self.syntax = syntax self.dna = 'IOSConfigList' self.debug = debug ## Support either a list or a generator instance if getattr(data, '__iter__', False): self._list = self._bootstrap_obj_init(data) else: self._list = list() def __len__(self): return len(self._list) def __getitem__(self, ii): return self._list[ii] def __delitem__(self, ii): del self._list[ii] self._bootstrap_from_text() def __setitem__(self, ii, val): return self._list[ii] def __str__(self): return self.__repr__() def __enter__(self): # Add support for with statements... # FIXME: *with* statements dont work for obj in self._list: yield obj def __exit__(self, *args, **kwargs): # FIXME: *with* statements dont work self._list[0].confobj.CiscoConfParse.atomic() def __repr__(self): return """""" % ( self.comment_delimiter, self._list) def _bootstrap_from_text(self): ## reparse all objects from their text attributes... this is *very* slow ## Ultimate goal: get rid of all reparsing from text... self._list = self._bootstrap_obj_init( list(map(attrgetter('text'), self._list))) if self.debug: _log.debug("self._list = {0}".format(self._list)) def has_line_with(self, linespec): return bool(filter(methodcaller('re_search', linespec), self._list)) def insert_before(self, robj, val, atomic=False): ## Insert something before robj if getattr(robj, 'capitalize', False): # robj must not be a string... raise ValueError if getattr(val, 'capitalize', False): if self.factory: obj = ConfigLineFactory( text=val, comment_delimiter=self.comment_delimiter, syntax=self.syntax) elif self.syntax == 'ios': obj = IOSCfgLine( text=val, comment_delimiter=self.comment_delimiter) ii = self._list.index(robj) if not (ii is None): ## Do insertion here self._list.insert(ii, obj) if atomic: # Reparse the whole config as a text list self._bootstrap_from_text() else: ## Just renumber lines... self._reassign_linenums() def insert_after(self, robj, val, atomic=False): ## Insert something after robj if getattr(robj, 'capitalize', False): raise ValueError ## If val is a string... if getattr(val, 'capitalize', False): if self.factory: obj = ConfigLineFactory( text=val, comment_delimiter=self.comment_delimiter, syntax=self.syntax) elif self.syntax == 'ios': obj = IOSCfgLine( text=val, comment_delimiter=self.comment_delimiter) ## FIXME: This shouldn't be required ## Removed 2015-01-24 during rewrite... #self._reassign_linenums() ii = self._list.index(robj) if not (ii is None): ## Do insertion here self._list.insert(ii + 1, obj) if atomic: # Reparse the whole config as a text list self._bootstrap_from_text() else: ## Just renumber lines... self._reassign_linenums() def insert(self, ii, val): if getattr(val, 'capitalize', False): if self.factory: obj = ConfigLineFactory( text=val, comment_delimiter=self.comment_delimiter, syntax=self.syntax) elif self.syntax == 'ios': obj = IOSCfgLine( text=val, comment_delimiter=self.comment_delimiter) else: raise ValueError( 'FATAL insert string - Cannot insert "{0}"'.format(val)) else: raise ValueError('FATAL insert - Cannot insert "{0}"'.format(val)) ## Insert something at index ii self._list.insert(ii, obj) ## Just renumber lines... self._reassign_linenums() def append(self, val): list_idx = len(self._list) self.insert(list_idx, val) def config_hierarchy(self): """Walk this configuration and return the following tuple at each parent 'level': (list_of_parent_sibling_objs, list_of_nonparent_sibling_objs) """ parent_siblings = list() nonparent_siblings = list() for obj in self.CiscoConfParse.find_objects(r'^\S+'): if obj.is_comment: continue elif len(obj.children) == 0: nonparent_siblings.append(obj) else: parent_siblings.append(obj) return parent_siblings, nonparent_siblings def _banner_mark_regex(self, REGEX): # Build a list of all leading banner lines banner_objs = list( filter(lambda obj: REGEX.search(obj.text), self._list)) BANNER_STR_RE = r'^(?:(?P(?:set\s+)*banner\s\w+\s+)(?P\S))' for parent in banner_objs: parent.oldest_ancestor = True ## Parse out the banner type and delimiting banner character mm = re.search(BANNER_STR_RE, parent.text) if not (mm is None): mm_results = mm.groupdict() (banner_lead, bannerdelimit) = (mm_results['btype'].rstrip(), mm_results['bchar']) else: (banner_lead, bannerdelimit) = ('', None) if self.debug: _log.debug("banner_lead = '{0}'".format(banner_lead)) _log.debug("bannerdelimit = '{0}'".format(bannerdelimit)) _log.debug("{0} starts at line {1}".format(banner_lead, parent.linenum)) idx = parent.linenum while not (bannerdelimit is None): ## Check whether the banner line has both begin and end delimter if idx == parent.linenum: parts = parent.text.split(bannerdelimit) if len(parts) > 2: ## banner has both begin and end delimiter on one line if self.debug: _log.debug("{0} ends at line" " {1}".format(banner_lead, parent.linenum)) break ## Use code below to identify children of the banner line idx += 1 try: obj = self._list[idx] if (obj.text is None): if self.debug: _log.warning( "found empty text while parsing '{0}' in the banner". format(obj)) pass elif bannerdelimit in obj.text.strip(): if self.debug: _log.debug("{0} ends at line" " {1}".format(banner_lead, obj.linenum)) parent.children.append(obj) parent.child_indent = 0 obj.parent = parent break # Commenting the following lines out; fix Github issue #115 #elif obj.is_comment and (obj.indent == 0): # break parent.children.append(obj) parent.child_indent = 0 obj.parent = parent except IndexError: break def _macro_mark_children(self, macro_parent_idx_list): # Mark macro children appropriately... for idx in macro_parent_idx_list: pobj = self._list[idx] pobj.child_indent = 0 # Walk the next configuration lines looking for the macro's children finished = False while not finished: idx += 1 cobj = self._list[idx] cobj.parent = pobj pobj.children.append(cobj) # If we hit the end of the macro, break out of the loop if cobj.text.rstrip()=='@': finished = True def _bootstrap_obj_init(self, text_list): """Accept a text list and format into proper IOSCfgLine() objects""" # Append text lines as IOSCfgLine objects... BANNER_STR = set([ 'login', 'motd', 'incoming', 'exec', 'telnet', 'lcd', ]) BANNER_ALL = [r'^(set\s+)*banner\s+{0}'.format(ii) for ii in BANNER_STR] BANNER_ALL.append('aaa authentication fail-message') # Github issue #76 BANNER_RE = re.compile('|'.join(BANNER_ALL)) retval = list() idx = 0 max_indent = 0 macro_parent_idx_list = list() parents = dict() for line in text_list: # Reject empty lines if ignore_blank_lines... if self.ignore_blank_lines and line.strip() == '': continue # if not self.factory: obj = IOSCfgLine(line, self.comment_delimiter) elif self.syntax == 'ios': obj = ConfigLineFactory( line, self.comment_delimiter, syntax='ios') else: raise ValueError obj.confobj = self obj.linenum = idx indent = len(line) - len(line.lstrip()) obj.indent = indent is_config_line = obj.is_config_line # list out macro parent line numbers... if obj.text[0:11]=='macro name ': macro_parent_idx_list.append(obj.linenum) ## Parent cache: ## Maintain indent vs max_indent in a family and ## cache the parent until indent= indent, sorted( parents.keys(), reverse=True)) for parent_idx in stale_parent_idxs: del parents[parent_idx] else: ## As long as the child indent hasn't gone backwards, ## we can use a cached parent parent = parents.get(indent, None) ## If indented, walk backwards and find the parent... ## 1. Assign parent to the child ## 2. Assign child to the parent ## 3. Assign parent's child_indent ## 4. Maintain oldest_ancestor if (indent > 0) and not (parent is None): ## Add the line as a child (parent was cached) self._add_child_to_parent(retval, idx, indent, parent, obj) elif (indent > 0) and (parent is None): ## Walk backwards to find parent, and add the line as a child candidate_parent_index = idx - 1 while candidate_parent_index >= 0: candidate_parent = retval[candidate_parent_index] if (candidate_parent.indent max_indent: max_indent = indent retval.append(obj) idx += 1 self._list = retval self._banner_mark_regex(BANNER_RE) # We need to use a different method for macros than banners because # macros don't specify a delimiter on their parent line, but # banners call out a delimiter. self._macro_mark_children(macro_parent_idx_list) # Process macros return retval def _add_child_to_parent(self, _list, idx, indent, parentobj, childobj): ## parentobj could be None when trying to add a child that should not ## have a parent if parentobj is None: if self.debug: _log.debug("parentobj is None") return if self.debug: #_log.debug("Adding child '{0}' to parent" # " '{1}'".format(childobj, parentobj)) #_log.debug("BEFORE parent.children - {0}" # .format(parentobj.children)) pass if childobj.is_comment and (_list[idx - 1].indent > indent): ## I *really* hate making this exception, but legacy ## ciscoconfparse never marked a comment as a child ## when the line immediately above it was indented more ## than the comment line pass elif (childobj.parent is childobj): # Child has not been assigned yet parentobj.children.append(childobj) childobj.parent = parentobj childobj.parent.child_indent = indent else: pass if self.debug: #_log.debug(" AFTER parent.children - {0}" # .format(parentobj.children)) pass def iter_with_comments(self, begin_index=0): for idx, obj in enumerate(self._list): if (idx >= begin_index): yield obj def iter_no_comments(self, begin_index=0): for idx, obj in enumerate(self._list): if (idx >= begin_index) and (not obj.is_comment): yield obj def _reassign_linenums(self): # Call this after any insertion or deletion for idx, obj in enumerate(self._list): obj.linenum = idx @property def all_parents(self): return [obj for obj in self._list if obj.has_children] @property def last_index(self): return (self.__len__() - 1) #########################################################################3 class NXOSConfigList(MutableSequence): """A custom list to hold :class:`~models_nxos.NXOSCfgLine` objects. Most people will never need to use this class directly. """ def __init__(self, data=None, comment_delimiter='!', debug=False, factory=False, ignore_blank_lines=True, syntax='nxos', CiscoConfParse=None): """Initialize the class. Kwargs: - data (list): A list of parsed :class:`~models_nxos.NXOSCfgLine` objects - comment (str): A comment delimiter. This should only be changed when parsing non-Cisco NXOS configurations, which do not use a ! as the comment delimiter. ``comment`` defaults to '!' - debug (bool): ``debug`` defaults to False, and should be kept that way unless you're working on a very tricky config parsing problem. Debug output is not particularly friendly - ignore_blank_lines (bool): ``ignore_blank_lines`` defaults to True; when this is set True, ciscoconfparse ignores blank configuration lines. You might want to set ``ignore_blank_lines`` to False if you intentionally use blank lines in your configuration (ref: Github Issue #2). Returns: - An instance of an :class:`~ciscoconfparse.NXOSConfigList` object. """ #data = kwargs.get('data', None) #comment_delimiter = kwargs.get('comment_delimiter', '!') #debug = kwargs.get('debug', False) #factory = kwargs.get('factory', False) #ignore_blank_lines = kwargs.get('ignore_blank_lines', True) #syntax = kwargs.get('syntax', 'nxos') #CiscoConfParse = kwargs.get('CiscoConfParse', None) super(NXOSConfigList, self).__init__() self._list = list() self.CiscoConfParse = CiscoConfParse self.comment_delimiter = comment_delimiter self.factory = factory self.ignore_blank_lines = ignore_blank_lines self.syntax = syntax self.dna = 'NXOSConfigList' self.debug = debug ## Support either a list or a generator instance if getattr(data, '__iter__', False): self._list = self._bootstrap_obj_init(data) else: self._list = list() def __len__(self): return len(self._list) def __getitem__(self, ii): return self._list[ii] def __delitem__(self, ii): del self._list[ii] self._bootstrap_from_text() def __setitem__(self, ii, val): return self._list[ii] def __str__(self): return self.__repr__() def __enter__(self): # Add support for with statements... # FIXME: *with* statements dont work for obj in self._list: yield obj def __exit__(self, *args, **kwargs): # FIXME: *with* statements dont work self._list[0].confobj.CiscoConfParse.atomic() def __repr__(self): return """""" % ( self.comment_delimiter, self._list) def _bootstrap_from_text(self): ## reparse all objects from their text attributes... this is *very* slow ## Ultimate goal: get rid of all reparsing from text... self._list = self._bootstrap_obj_init( list(map(attrgetter('text'), self._list))) if self.debug: _log.debug("self._list = {0}".format(self._list)) def has_line_with(self, linespec): return bool(filter(methodcaller('re_search', linespec), self._list)) def insert_before(self, robj, val, atomic=False): ## Insert something before robj if getattr(robj, 'capitalize', False): # robj must not be a string... raise ValueError if getattr(val, 'capitalize', False): if self.factory: obj = ConfigLineFactory( text=val, comment_delimiter=self.comment_delimiter, syntax=self.syntax) elif self.syntax == 'nxos': obj = NXOSCfgLine( text=val, comment_delimiter=self.comment_delimiter) ii = self._list.index(robj) if not (ii is None): ## Do insertion here self._list.insert(ii, obj) if atomic: # Reparse the whole config as a text list self._bootstrap_from_text() else: ## Just renumber lines... self._reassign_linenums() def insert_after(self, robj, val, atomic=False): ## Insert something after robj if getattr(robj, 'capitalize', False): raise ValueError ## If val is a string... if getattr(val, 'capitalize', False): if self.factory: obj = ConfigLineFactory( text=val, comment_delimiter=self.comment_delimiter, syntax=self.syntax) elif self.syntax == 'nxos': obj = NXOSCfgLine( text=val, comment_delimiter=self.comment_delimiter) ## FIXME: This shouldn't be required ## Removed 2015-01-24 during rewrite... #self._reassign_linenums() ii = self._list.index(robj) if not (ii is None): ## Do insertion here self._list.insert(ii + 1, obj) if atomic: # Reparse the whole config as a text list self._bootstrap_from_text() else: ## Just renumber lines... self._reassign_linenums() def insert(self, ii, val): if getattr(val, 'capitalize', False): if self.factory: obj = ConfigLineFactory( text=val, comment_delimiter=self.comment_delimiter, syntax=self.syntax) elif self.syntax == 'nxos': obj = NXOSCfgLine( text=val, comment_delimiter=self.comment_delimiter) else: raise ValueError( 'FATAL insert string - Cannot insert "{0}"'.format(val)) else: raise ValueError('FATAL insert - Cannot insert "{0}"'.format(val)) ## Insert something at index ii self._list.insert(ii, obj) ## Just renumber lines... self._reassign_linenums() def append(self, val): list_idx = len(self._list) self.insert(list_idx, val) def config_hierarchy(self): """Walk this configuration and return the following tuple at each parent 'level': (list_of_parent_sibling_objs, list_of_nonparent_sibling_objs) """ parent_siblings = list() nonparent_siblings = list() for obj in self.CiscoConfParse.find_objects(r'^\S+'): if obj.is_comment: continue elif len(obj.children) == 0: nonparent_siblings.append(obj) else: parent_siblings.append(obj) return parent_siblings, nonparent_siblings def _banner_mark_regex(self, REGEX): # Build a list of all leading banner lines banner_objs = list( filter(lambda obj: REGEX.search(obj.text), self._list)) BANNER_STR_RE = r'^(?:(?P(?:set\s+)*banner\s\w+\s+)(?P\S))' for parent in banner_objs: parent.oldest_ancestor = True ## Parse out the banner type and delimiting banner character mm = re.search(BANNER_STR_RE, parent.text) if not (mm is None): mm_results = mm.groupdict() (banner_lead, bannerdelimit) = (mm_results['btype'].rstrip(), mm_results['bchar']) else: (banner_lead, bannerdelimit) = ('', None) if self.debug: _log.debug("banner_lead = '{0}'".format(banner_lead)) _log.debug("bannerdelimit = '{0}'".format(bannerdelimit)) _log.debug("{0} starts at line {1}".format(banner_lead, parent.linenum)) idx = parent.linenum while not (bannerdelimit is None): ## Check whether the banner line has both begin and end delimter if idx == parent.linenum: parts = parent.text.split(bannerdelimit) if len(parts) > 2: ## banner has both begin and end delimiter on one line if self.debug: _log.debug("{0} ends at line" " {1}".format(banner_lead, parent.linenum)) break idx += 1 try: obj = self._list[idx] if (obj.text is None): if self.debug: _log.warning( "found empty text while parsing '{0}' in the banner". format(obj)) pass elif bannerdelimit in obj.text.strip(): if self.debug: _log.debug("{0} ends at line" " {1}".format(banner_lead, obj.linenum)) parent.children.append(obj) parent.child_indent = 0 obj.parent = parent break ## Fix Github issue #75 I don't think this case is reqd now #elif obj.is_comment and (obj.indent == 0): # break parent.children.append(obj) parent.child_indent = 0 obj.parent = parent except IndexError: break def _bootstrap_obj_init(self, text_list): """Accept a text list and format into proper objects""" # Append text lines as NXOSCfgLine objects... BANNER_STR = set([ 'login', 'motd', 'incoming', 'exec', 'telnet', 'lcd', ]) BANNER_RE = re.compile('|'.join( [r'^(set\s+)*banner\s+{0}'.format(ii) for ii in BANNER_STR])) retval = list() idx = 0 max_indent = 0 parents = dict() for line in text_list: # Reject empty lines if ignore_blank_lines... if self.ignore_blank_lines and line.strip() == '': continue # if not self.factory: obj = NXOSCfgLine(line, self.comment_delimiter) elif self.syntax == 'nxos': obj = ConfigLineFactory( line, self.comment_delimiter, syntax='nxos') else: raise ValueError obj.confobj = self obj.linenum = idx indent = len(line) - len(line.lstrip()) obj.indent = indent is_config_line = obj.is_config_line ## Parent cache: ## Maintain indent vs max_indent in a family and ## cache the parent until indent= indent, sorted( parents.keys(), reverse=True)) for parent_idx in stale_parent_idxs: del parents[parent_idx] else: ## As long as the child indent hasn't gone backwards, ## we can use a cached parent parent = parents.get(indent, None) ## If indented, walk backwards and find the parent... ## 1. Assign parent to the child ## 2. Assign child to the parent ## 3. Assign parent's child_indent ## 4. Maintain oldest_ancestor if (indent > 0) and not (parent is None): ## Add the line as a child (parent was cached) self._add_child_to_parent(retval, idx, indent, parent, obj) elif (indent > 0) and (parent is None): ## Walk backwards to find parent, and add the line as a child candidate_parent_index = idx - 1 while candidate_parent_index >= 0: candidate_parent = retval[candidate_parent_index] if (candidate_parent.indent max_indent: max_indent = indent retval.append(obj) idx += 1 self._list = retval self._banner_mark_regex(BANNER_RE) # Process IOS banners return retval def _add_child_to_parent(self, _list, idx, indent, parentobj, childobj): ## parentobj could be None when trying to add a child that should not ## have a parent if parentobj is None: if self.debug: _log.debug("parentobj is None") return if self.debug: #_log.debug("Adding child '{0}' to parent" # " '{1}'".format(childobj, parentobj)) #_log.debug("BEFORE parent.children - {0}" # .format(parentobj.children)) pass if childobj.is_comment and (_list[idx - 1].indent > indent): ## I *really* hate making this exception, but legacy ## ciscoconfparse never marked a comment as a child ## when the line immediately above it was indented more ## than the comment line pass elif (childobj.parent is childobj): # Child has not been assigned yet parentobj.children.append(childobj) childobj.parent = parentobj childobj.parent.child_indent = indent else: pass if self.debug: #_log.debug(" AFTER parent.children - {0}" # .format(parentobj.children)) pass def iter_with_comments(self, begin_index=0): for idx, obj in enumerate(self._list): if (idx >= begin_index): yield obj def iter_no_comments(self, begin_index=0): for idx, obj in enumerate(self._list): if (idx >= begin_index) and (not obj.is_comment): yield obj def _reassign_linenums(self): # Call this after any insertion or deletion for idx, obj in enumerate(self._list): obj.linenum = idx @property def all_parents(self): return [obj for obj in self._list if obj.has_children] @property def last_index(self): return (self.__len__() - 1) class ASAConfigList(MutableSequence): """A custom list to hold :class:`~models_asa.ASACfgLine` objects. Most people will never need to use this class directly. """ def __init__(self, data=None, comment_delimiter='!', debug=False, factory=False, ignore_blank_lines=True, syntax='asa', CiscoConfParse=None): """Initialize the class. Kwargs: - data (list): A list of parsed :class:`~models_asa.ASACfgLine` objects - comment (str): A comment delimiter. This should only be changed when parsing non-Cisco IOS configurations, which do not use a ! as the comment delimiter. ``comment`` defaults to '!' - debug (bool): ``debug`` defaults to False, and should be kept that way unless you're working on a very tricky config parsing problem. Debug output is not particularly friendly - ignore_blank_lines (bool): ``ignore_blank_lines`` defaults to True; when this is set True, ciscoconfparse ignores blank configuration lines. You might want to set ``ignore_blank_lines`` to False if you intentionally use blank lines in your configuration. Attributes: - names (dict): A Python dictionary, which maps a Cisco ASA name to a string representing the address - object_group_network (dict): A Python dictionary, which maps a Cisco ASA object-group network name to the :class:`~models_asa.ASAObjNetwork` object - object_group_service (dict): A Python dictionary, which maps a Cisco ASA object-group service name to the :class:`~models_asa.ASAObjService` object - access_list (dict): A Python dictionary, which maps a Cisco ASA access-list name to the list of ACEs for that ACL Returns: - An instance of an :class:`~ciscoconfparse.ASAConfigList` object. """ super(ASAConfigList, self).__init__() self._list = list() self.CiscoConfParse = CiscoConfParse self.comment_delimiter = comment_delimiter self.factory = factory self.ignore_blank_lines = ignore_blank_lines self.syntax = syntax self.dna = 'ASAConfigList' self.debug = debug ## Support either a list or a generator instance if getattr(data, '__iter__', False): self._bootstrap_obj_init(data) else: self._list = list() ### ### Internal structures self._RE_NAMES = re.compile(r'^\s*name\s+(\d+\.\d+\.\d+\.\d+)\s+(\S+)') self._RE_OBJNET = re.compile(r'^\s*object-group\s+network\s+(\S+)') self._RE_OBJSVC = re.compile(r'^\s*object-group\s+service\s+(\S+)') self._RE_OBJACL = re.compile(r'^\s*access-list\s+(\S+)') self._network_cache = dict() def __len__(self): return len(self._list) def __getitem__(self, ii): return self._list[ii] def __delitem__(self, ii): del self._list[ii] self._bootstrap_from_text() def __setitem__(self, ii, val): return self._list[ii] def __str__(self): return self.__repr__() def __enter__(self): # Add support for with statements... # FIXME: *with* statements dont work for obj in self._list: yield obj def __exit__(self, *args, **kwargs): # FIXME: *with* statements dont work self._list[0].confobj.CiscoConfParse.atomic() def __repr__(self): return """""" % ( self.comment_delimiter, self._list) def _bootstrap_from_text(self): ## reparse all objects from their text attributes... this is *very* slow ## Ultimate goal: get rid of all reparsing from text... self._list = self._bootstrap_obj_init( list(map(attrgetter('text'), self._list))) def has_line_with(self, linespec): return bool(filter(methodcaller('re_search', linespec), self._list)) def insert_before(self, robj, val, atomic=False): ## Insert something before robj if getattr(robj, 'capitalize', False): raise ValueError if getattr(val, 'capitalize', False): if self.factory: obj = ConfigLineFactory( text=val, comment_delimiter=self.comment_delimiter, syntax=self.syntax) elif self.syntax == 'asa': obj = ASACfgLine( text=val, comment_delimiter=self.comment_delimiter) ii = self._list.index(robj) if not (ii is None): ## Do insertion here self._list.insert(ii, obj) if atomic: # Reparse the whole config as a text list self._bootstrap_from_text() else: ## Just renumber lines... self._reassign_linenums() def insert_after(self, robj, val, atomic=False): ## Insert something after robj if getattr(robj, 'capitalize', False): raise ValueError if getattr(val, 'capitalize', False): if self.factory: obj = ConfigLineFactory( text=val, comment_delimiter=self.comment_delimiter, syntax=self.syntax) elif self.syntax == 'asa': obj = ASACfgLine( text=val, comment_delimiter=self.comment_delimiter) ## FIXME: This shouldn't be required self._reassign_linenums() ii = self._list.index(robj) if not (ii is None): ## Do insertion here self._list.insert(ii + 1, obj) if atomic: # Reparse the whole config as a text list self._bootstrap_from_text() else: ## Just renumber lines... self._reassign_linenums() def insert(self, ii, val): ## Insert something at index ii if getattr(val, 'capitalize', False): if self.factory: obj = ConfigLineFactory( text=val, comment_delimiter=self.comment_delimiter, syntax=self.syntax) elif self.syntax == 'asa': obj = ASACfgLine( text=val, comment_delimiter=self.comment_delimiter) self._list.insert(ii, obj) ## Just renumber lines... self._reassign_linenums() def append(self, val, atomic=False): list_idx = len(self._list) self.insert(list_idx, val) def config_hierarchy(self): """Walk this configuration and return the following tuple at each parent 'level': (list_of_parent_siblings, list_of_nonparent_siblings)""" parent_siblings = list() nonparent_siblings = list() for obj in self.CiscoConfParse.find_objects(r'^\S+'): if obj.is_comment: continue elif len(obj.children) == 0: nonparent_siblings.append(obj) else: parent_siblings.append(obj) return parent_siblings, nonparent_siblings def _bootstrap_obj_init(self, text_list): """Accept a text list and format into proper objects""" # Append text lines as IOSCfgLine objects... retval = list() idx = 0 max_indent = 0 parents = dict() for line in text_list: # Reject empty lines if ignore_blank_lines... if self.ignore_blank_lines and line.strip() == '': continue if self.syntax == 'asa' and self.factory: obj = ConfigLineFactory( line, self.comment_delimiter, syntax='asa') elif self.syntax == 'asa' and not self.factory: obj = ASACfgLine( text=line, comment_delimiter=self.comment_delimiter) else: raise ValueError obj.confobj = self obj.linenum = idx indent = len(line) - len(line.lstrip()) obj.indent = indent is_config_line = obj.is_config_line ## Parent cache: ## Maintain indent vs max_indent in a family and ## cache the parent until indent= indent, sorted( parents.keys(), reverse=True)) for parent_idx in stale_parent_idxs: del parents[parent_idx] else: ## As long as the child indent hasn't gone backwards, ## we can use a cached parent parent = parents.get(indent, None) ## If indented, walk backwards and find the parent... ## 1. Assign parent to the child ## 2. Assign child to the parent ## 3. Assign parent's child_indent ## 4. Maintain oldest_ancestor if (indent > 0) and not (parent is None): ## Add the line as a child (parent was cached) self._add_child_to_parent(retval, idx, indent, parent, obj) elif (indent > 0) and (parent is None): ## Walk backwards to find parent, and add the line as a child candidate_parent_index = idx - 1 while candidate_parent_index >= 0: candidate_parent = retval[candidate_parent_index] if (candidate_parent.indent max_indent: max_indent = indent retval.append(obj) idx += 1 self._list = retval ## Insert ASA-specific banner processing here, if required return retval def _add_child_to_parent(self, _list, idx, indent, parentobj, childobj): ## parentobj could be None when trying to add a child that should not ## have a parent if parentobj is None: if self.debug: _log.debug("parentobj is None") return if self.debug: _log.debug("Adding child '{0}' to parent" " '{1}'".format(childobj, parentobj)) _log.debug(" BEFORE parent.children - {0}" .format(parentobj.children)) if childobj.is_comment and (_list[idx - 1].indent > indent): ## I *really* hate making this exception, but legacy ## ciscoconfparse never marked a comment as a child ## when the line immediately above it was indented more ## than the comment line pass elif (childobj.parent is childobj): # Child has not been assigned yet parentobj.children.append(childobj) childobj.parent = parentobj childobj.parent.child_indent = indent else: pass if self.debug: _log.debug(" AFTER parent.children - {0}" .format(parentobj.children)) def iter_with_comments(self, begin_index=0): for idx, obj in enumerate(self._list): if (idx >= begin_index): yield obj def iter_no_comments(self, begin_index=0): for idx, obj in enumerate(self._list): if (idx >= begin_index) and (not obj.is_comment): yield obj def _reassign_linenums(self): # Call this after any insertion or deletion for idx, obj in enumerate(self._list): obj.linenum = idx @property def all_parents(self): return [obj for obj in self._list if obj.has_children] @property def last_index(self): return (self.__len__() - 1) ### ### ASA-specific stuff here... ### @property def names(self): """Return a dictionary of name to address mappings""" retval = dict() name_rgx = self._RE_NAMES for obj in self.CiscoConfParse.find_objects(name_rgx): addr = obj.re_match_typed(name_rgx, group=1, result_type=str) name = obj.re_match_typed(name_rgx, group=2, result_type=str) retval[name] = addr return retval @property def object_group_network(self): """Return a dictionary of name to object-group network mappings""" retval = dict() obj_rgx = self._RE_OBJNET for obj in self.CiscoConfParse.find_objects(obj_rgx): name = obj.re_match_typed(obj_rgx, group=1, result_type=str) retval[name] = obj return retval @property def access_list(self): """Return a dictionary of ACL name to ACE (list) mappings""" retval = dict() for obj in self.CiscoConfParse.find_objects(self._RE_OBJACL): name = obj.re_match_typed( self._RE_OBJACL, group=1, result_type=str) tmp = retval.get(name, []) tmp.append(obj) retval[name] = tmp return retval class DiffObject(object): """This object should be used at every level of hierarchy""" def __init__(self, level, nonparents, parents): self.level = level self.nonparents = nonparents self.parents = parents def __repr__(self): return "".format(self.level) class CiscoPassword(object): def __init__(self, ep=""): self.ep = ep def decrypt(self, ep=""): """Cisco Type 7 password decryption. Converted from perl code that was written by jbash [~at~] cisco.com; enhancements suggested by rucjain [~at~] cisco.com""" xlat = (0x64, 0x73, 0x66, 0x64, 0x3b, 0x6b, 0x66, 0x6f, 0x41, 0x2c, 0x2e, 0x69, 0x79, 0x65, 0x77, 0x72, 0x6b, 0x6c, 0x64, 0x4a, 0x4b, 0x44, 0x48, 0x53, 0x55, 0x42, 0x73, 0x67, 0x76, 0x63, 0x61, 0x36, 0x39, 0x38, 0x33, 0x34, 0x6e, 0x63, 0x78, 0x76, 0x39, 0x38, 0x37, 0x33, 0x32, 0x35, 0x34, 0x6b, 0x3b, 0x66, 0x67, 0x38, 0x37) dp = "" regex = re.compile("^(..)(.+)") ep = ep or self.ep if not (len(ep) & 1): result = regex.search(ep) try: s, e = int(result.group(1)), result.group(2) except ValueError: # typically get a ValueError for int( result.group(1))) because # the method was called with an unencrypted password. For now # SILENTLY bypass the error s, e = (0, "") for ii in range(0, len(e), 2): # int( blah, 16) assumes blah is base16... cool magic = int(re.search(".{%s}(..)" % ii, e).group(1), 16) # Wrap around after 53 chars... newchar = "%c" % (magic ^ int(xlat[int(s % 53)])) dp = dp + str(newchar) s = s + 1 #if s > 53: # _log.warning("password decryption failed.") return dp def ConfigLineFactory(text="", comment_delimiter="!", syntax='ios'): # Complicted & Buggy #classes = [j for (i,j) in globals().iteritems() if isinstance(j, TypeType) and issubclass(j, BaseCfgLine)] ## Manual and simple if syntax == 'ios': classes = [ IOSIntfLine, IOSRouteLine, IOSAccessLine, IOSAaaLoginAuthenticationLine, IOSAaaEnableAuthenticationLine, IOSAaaCommandsAuthorizationLine, IOSAaaCommandsAccountingLine, IOSAaaExecAccountingLine, IOSAaaGroupServerLine, IOSHostnameLine, IOSIntfGlobal, IOSCfgLine ] # This is simple elif syntax == 'nxos': classes = [ NXOSIntfLine, NXOSRouteLine, NXOSAccessLine, NXOSAaaLoginAuthenticationLine, NXOSAaaEnableAuthenticationLine, NXOSAaaCommandsAuthorizationLine, NXOSAaaCommandsAccountingLine, NXOSAaaExecAccountingLine, NXOSAaaGroupServerLine, NXOSvPCLine, NXOSHostnameLine, NXOSIntfGlobal, NXOSCfgLine ] # This is simple elif syntax == 'asa': classes = [ ASAName, ASAObjNetwork, ASAObjService, ASAObjGroupNetwork, ASAObjGroupService, ASAIntfLine, ASAIntfGlobal, ASAHostnameLine, ASAAclLine, ASACfgLine ] elif syntax == 'junos': classes = [ IOSConfigLine ] else: raise ValueError("'{0}' is an unknown syntax".format(syntax)) for cls in classes: if cls.is_object_for(text): inst = cls(text=text, comment_delimiter=comment_delimiter ) # instance of the proper subclass return inst raise ValueError("Could not find an object for '%s'" % line) ### TODO: Add unit tests below if __name__ == '__main__': import optparse pp = optparse.OptionParser() pp.add_option( "-c", dest="config", help="Config file to be parsed", metavar="FILENAME") pp.add_option( "-m", dest="method", help="Command for parsing", metavar="METHOD") pp.add_option( "--a1", dest="arg1", help="Command's first argument", metavar="ARG") pp.add_option( "--a2", dest="arg2", help="Command's second argument", metavar="ARG") pp.add_option( "--a3", dest="arg3", help="Command's third argument", metavar="ARG") (opts, args) = pp.parse_args() if opts.method == "find_lines": diff = CiscoConfParse(opts.config).find_lines(opts.arg1) elif opts.method == "find_children": diff = CiscoConfParse(opts.config).find_children(opts.arg1) elif opts.method == "find_all_children": diff = CiscoConfParse(opts.config).find_all_children(opts.arg1) elif opts.method == "find_blocks": diff = CiscoConfParse(opts.config).find_blocks(opts.arg1) elif opts.method == "find_parents_w_child": diff = CiscoConfParse(opts.config).find_parents_w_child(opts.arg1, opts.arg2) elif opts.method == "find_parents_wo_child": diff = CiscoConfParse(opts.config).find_parents_wo_child(opts.arg1, opts.arg2) elif opts.method == "req_cfgspec_excl_diff": diff = CiscoConfParse(opts.config).req_cfgspec_excl_diff( opts.arg1, opts.arg2, opts.arg3.split(",")) elif opts.method == "req_cfgspec_all_diff": diff = CiscoConfParse(opts.config).req_cfgspec_all_diff( opts.arg1.split(",")) elif opts.method == "decrypt": pp = CiscoPassword() print(pp.decrypt(opts.arg1)) exit(1) elif opts.method == "help": print("Valid methods and their arguments:") print(" find_lines: arg1=linespec") print(" find_children: arg1=linespec") print(" find_all_children: arg1=linespec") print(" find_blocks: arg1=linespec") print(" find_parents_w_child: arg1=parentspec arg2=childspec") print(" find_parents_wo_child: arg1=parentspec arg2=childspec") print(" req_cfgspec_excl_diff: arg1=linespec arg2=uncfgspec" + \ " arg3=cfgspec") print(" req_cfgspec_all_diff: arg1=cfgspec") print(" decrypt: arg1=encrypted_passwd") exit(1) else: import doctest doctest.testmod() exit(0) if len(diff) > 0: for line in diff: print(line) else: raise RuntimeError("FATAL: ciscoconfparse was called with unknown" + \ " parameters")