1 2from __future__ import absolute_import 3 4from loguru import logger 5 6from ciscoconfparse.models_cisco import IOSHostnameLine, IOSRouteLine 7from ciscoconfparse.models_cisco import IOSIntfLine 8from ciscoconfparse.models_cisco import IOSAccessLine, IOSIntfGlobal 9from ciscoconfparse.models_cisco import IOSAaaLoginAuthenticationLine 10from ciscoconfparse.models_cisco import IOSAaaEnableAuthenticationLine 11from ciscoconfparse.models_cisco import IOSAaaCommandsAuthorizationLine 12from ciscoconfparse.models_cisco import IOSAaaCommandsAccountingLine 13from ciscoconfparse.models_cisco import IOSAaaExecAccountingLine 14from ciscoconfparse.models_cisco import IOSAaaGroupServerLine 15from ciscoconfparse.models_cisco import IOSCfgLine 16 17from ciscoconfparse.models_nxos import NXOSHostnameLine, NXOSRouteLine, NXOSIntfLine 18from ciscoconfparse.models_nxos import NXOSAccessLine, NXOSIntfGlobal 19from ciscoconfparse.models_nxos import NXOSAaaLoginAuthenticationLine 20from ciscoconfparse.models_nxos import NXOSAaaEnableAuthenticationLine 21from ciscoconfparse.models_nxos import NXOSAaaCommandsAuthorizationLine 22from ciscoconfparse.models_nxos import NXOSAaaCommandsAccountingLine 23from ciscoconfparse.models_nxos import NXOSAaaExecAccountingLine 24from ciscoconfparse.models_nxos import NXOSAaaGroupServerLine 25from ciscoconfparse.models_nxos import NXOSvPCLine 26from ciscoconfparse.models_nxos import NXOSCfgLine 27 28from ciscoconfparse.models_asa import ASAObjGroupNetwork 29from ciscoconfparse.models_asa import ASAObjGroupService 30from ciscoconfparse.models_asa import ASAHostnameLine 31from ciscoconfparse.models_asa import ASAObjNetwork 32from ciscoconfparse.models_asa import ASAObjService 33from ciscoconfparse.models_asa import ASAIntfGlobal 34from ciscoconfparse.models_asa import ASAIntfLine 35from ciscoconfparse.models_asa import ASACfgLine 36from ciscoconfparse.models_asa import ASAName 37from ciscoconfparse.models_asa import ASAAclLine 38 39from ciscoconfparse.models_junos import JunosCfgLine 40 41from ciscoconfparse.ccp_util import junos_unsupported, UnsupportedFeatureWarning 42 43from operator import methodcaller, attrgetter 44from colorama import Fore, Back, Style 45from difflib import SequenceMatcher 46import inspect 47import json 48import time 49import copy 50import sys 51import re 52import os 53 54if sys.version_info >= ( 55 3, 56 0, 57 0, 58): 59 from collections.abc import MutableSequence, Iterator 60else: 61 ## This syntax is not supported in Python 3... 62 from collections import MutableSequence, Iterator 63 64 65 66 67r""" ciscoconfparse.py - Parse, Query, Build, and Modify IOS-style configs 68 69 Copyright (C) 2020-2021 David Michael Pennington at Cisco Systems 70 Copyright (C) 2019 David Michael Pennington at ThousandEyes 71 Copyright (C) 2012-2019 David Michael Pennington at Samsung Data Services 72 Copyright (C) 2011-2012 David Michael Pennington at Dell Computer Corp 73 Copyright (C) 2007-2011 David Michael Pennington 74 75 This program is free software: you can redistribute it and/or modify 76 it under the terms of the GNU General Public License as published by 77 the Free Software Foundation, either version 3 of the License, or 78 (at your option) any later version. 79 80 This program is distributed in the hope that it will be useful, 81 but WITHOUT ANY WARRANTY; without even the implied warranty of 82 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 83 GNU General Public License for more details. 84 85 You should have received a copy of the GNU General Public License 86 along with this program. If not, see <http://www.gnu.org/licenses/>. 87 88 If you need to contact the author, you can do so by emailing: 89 mike [~at~] pennington [/dot\] net 90""" 91 92 93## Docstring props: http://stackoverflow.com/a/1523456/667301 94# __version__ if-else below fixes Github issue #123 95metadata_json_path = os.path.join( 96 os.path.dirname(os.path.abspath(__file__)), "metadata.json" 97) 98if os.path.isfile(metadata_json_path): 99 ## Retrieve the version number from json... 100 with open(metadata_json_path) as mh: 101 metadata_dict = json.load(mh) 102 __author__ = metadata_dict.get("author") 103 __author_email__ = metadata_dict.get("author_email") 104 __version__ = metadata_dict.get("version") 105else: 106 # This case is required for importing from a zipfile... Github issue #123 107 __version__ = "0.0.0" # __version__ read failed 108__author_email__ = r"mike /at\ pennington [dot] net" 109__author__ = "David Michael Pennington <{0}>".format(__author_email__) 110__copyright__ = "2007-{0}, {1}".format(time.strftime("%Y"), __author__) 111__license__ = "GPLv3" 112__status__ = "Production" 113 114 115logger.remove() 116# Send logs to sys.stderr by default 117logger.add( 118 sink=sys.stderr, 119 colorize=True, 120 diagnose=True, 121 backtrace=True, 122 enqueue=True, 123 serialize=False, 124 catch=True, 125 level="DEBUG", 126 #format='<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>' 127) 128 129class CiscoConfParse(object): 130 """Parses Cisco IOS configurations and answers queries about the configs.""" 131 132 def __init__( 133 self, 134 config="", 135 comment="!", 136 debug=0, 137 factory=False, 138 linesplit_rgx=r"\r*\n+", 139 ignore_blank_lines=True, 140 syntax="ios", 141 ): 142 """ 143 Initialize CiscoConfParse. 144 145 Parameters 146 ---------- 147 config : list or str 148 A list of configuration statements, or a configuration file path to be parsed 149 comment : str 150 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 151 debug : int 152 ``debug`` defaults to 0, and should be kept that way unless you're working on a very tricky config parsing problem. Debug range goes from 0 (no debugging) to 5 (max debugging). Debug output is not particularly friendly. 153 factory : bool 154 ``factory`` defaults to False; if set ``True``, it enables a beta-quality configuration line classifier. 155 linesplit_rgx : str 156 ``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) 157 ignore_blank_lines : bool 158 ``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). 159 syntax : str 160 A string holding the configuration type. Default: 'ios'. Must be one of: 'ios', 'nxos', 'asa', 'junos'. Use 'junos' for any brace-delimited configuration (including F5, Palo Alto, etc...). 161 162 Returns 163 ------- 164 :class:`~ciscoconfparse.CiscoConfParse` 165 166 Examples 167 -------- 168 This example illustrates how to parse a simple Cisco IOS configuration 169 with :class:`~ciscoconfparse.CiscoConfParse` into a variable called 170 ``parse``. This example also illustrates what the ``ConfigObjs`` 171 and ``ioscfg`` attributes contain. 172 173 >>> from ciscoconfparse import CiscoConfParse 174 >>> config = [ 175 ... 'logging trap debugging', 176 ... 'logging 172.28.26.15', 177 ... ] 178 >>> parse = CiscoConfParse(config) 179 >>> parse 180 <CiscoConfParse: 2 lines / syntax: ios / comment delimiter: '!' / factory: False> 181 >>> parse.ConfigObjs 182 <IOSConfigList, comment='!', conf=[<IOSCfgLine # 0 'logging trap debugging'>, <IOSCfgLine # 1 'logging 172.28.26.15'>]> 183 >>> parse.ioscfg 184 ['logging trap debugging', 'logging 172.28.26.15'] 185 >>> 186 187 Attributes 188 ---------- 189 comment_delimiter : str 190 A string containing the comment-delimiter. Default: "!" 191 ConfigObjs : :class:`~ciscoconfparse.IOSConfigList` 192 A custom list, which contains all parsed :class:`~models_cisco.IOSCfgLine` instances. 193 debug : int 194 An int to enable verbose config parsing debugs. Default 0. 195 ioscfg : list 196 A list of text configuration strings 197 objs 198 An alias for `ConfigObjs` 199 openargs : dict 200 Returns a dictionary of valid arguments for `open()` (these change based on the running python version). 201 syntax : str 202 A string holding the configuration type. Default: 'ios'. Must be one of: 'ios', 'nxos', 'asa', 'junos'. Use 'junos' for any brace-delimited configuration (including F5, Palo Alto, etc...). 203 204 205 """ 206 207 # all IOSCfgLine object instances... 208 self.comment_delimiter = comment 209 self.factory = factory 210 self.ConfigObjs = None 211 self.syntax = syntax 212 self.debug = debug 213 214 if isinstance(config, list) or isinstance(config, Iterator): 215 if syntax == "ios": 216 # we already have a list object, simply call the parser 217 if self.debug > 0: 218 logger.debug("parsing from a python list with ios syntax") 219 self.ConfigObjs = IOSConfigList( 220 data=config, 221 comment_delimiter=comment, 222 debug=debug, 223 factory=factory, 224 ignore_blank_lines=ignore_blank_lines, 225 syntax="ios", 226 CiscoConfParse=self, 227 ) 228 elif syntax == "nxos": 229 # we already have a list object, simply call the parser 230 if self.debug > 0: 231 logger.debug("parsing from a python list with nxos syntax") 232 self.ConfigObjs = NXOSConfigList( 233 data=config, 234 comment_delimiter=comment, 235 debug=debug, 236 factory=factory, 237 ignore_blank_lines=False, # NXOS always has blank lines 238 syntax="nxos", 239 CiscoConfParse=self, 240 ) 241 elif syntax == "asa": 242 # we already have a list object, simply call the parser 243 if self.debug > 0: 244 logger.debug("parsing from a python list with asa syntax") 245 self.ConfigObjs = ASAConfigList( 246 data=config, 247 comment_delimiter=comment, 248 debug=debug, 249 factory=factory, 250 ignore_blank_lines=ignore_blank_lines, 251 syntax="asa", 252 CiscoConfParse=self, 253 ) 254 elif syntax == "junos": 255 ## FIXME I am shamelessly abusing the IOSConfigList for now... 256 # we already have a list object, simply call the parser 257 error = "junos parser factory is not yet enabled; use factory=False" 258 assert factory is False, error 259 config = self.convert_braces_to_ios(config) 260 if self.debug > 0: 261 logger.debug("parsing from a python list with junos syntax") 262 self.ConfigObjs = IOSConfigList( 263 data=config, 264 comment_delimiter=comment, 265 debug=debug, 266 factory=factory, 267 ignore_blank_lines=ignore_blank_lines, 268 syntax="junos", 269 CiscoConfParse=self, 270 ) 271 else: 272 error = "'{}' is an unknown syntax".format(syntax) 273 logger.critical(error) 274 raise ValueError(error) 275 276 ## Accept either a string, unicode, or a pathlib.Path instance... 277 elif getattr(config, "encode", False) or getattr(config, "is_file"): 278 # Try opening as a file 279 try: 280 if syntax == "ios": 281 # string - assume a filename... open file, split and parse 282 if self.debug > 0: 283 logger.debug( 284 "parsing from '{0}' with ios syntax".format(config) 285 ) 286 with open(config, **self.openargs) as fh: 287 text = fh.read() 288 rgx = re.compile(linesplit_rgx) 289 self.ConfigObjs = IOSConfigList( 290 rgx.split(text), 291 comment_delimiter=comment, 292 debug=debug, 293 factory=factory, 294 ignore_blank_lines=ignore_blank_lines, 295 syntax="ios", 296 CiscoConfParse=self, 297 ) 298 elif syntax == "nxos": 299 # string - assume a filename... open file, split and parse 300 if self.debug > 0: 301 logger.debug( 302 "parsing from '{0}' with nxos syntax".format(config) 303 ) 304 with open(config, **self.openargs) as fh: 305 text = fh.read() 306 rgx = re.compile(linesplit_rgx) 307 self.ConfigObjs = NXOSConfigList( 308 rgx.split(text), 309 comment_delimiter=comment, 310 debug=debug, 311 factory=factory, 312 ignore_blank_lines=False, 313 syntax="nxos", 314 CiscoConfParse=self, 315 ) 316 elif syntax == "asa": 317 # string - assume a filename... open file, split and parse 318 if self.debug > 0: 319 logger.debug( 320 "parsing from '{0}' with asa syntax".format(config) 321 ) 322 with open(config, **self.openargs) as fh: 323 text = fh.read() 324 rgx = re.compile(linesplit_rgx) 325 self.ConfigObjs = ASAConfigList( 326 rgx.split(text), 327 comment_delimiter=comment, 328 debug=debug, 329 factory=factory, 330 ignore_blank_lines=ignore_blank_lines, 331 syntax="asa", 332 CiscoConfParse=self, 333 ) 334 335 elif syntax == "junos": 336 # string - assume a filename... open file, split and parse 337 if self.debug > 0: 338 logger.debug( 339 "parsing from '{0}' with junos syntax".format(config) 340 ) 341 with open(config, **self.openargs) as fh: 342 text = fh.read() 343 rgx = re.compile(linesplit_rgx) 344 345 config = self.convert_braces_to_ios(rgx.split(text)) 346 ## FIXME I am shamelessly abusing the IOSConfigList for now... 347 self.ConfigObjs = IOSConfigList( 348 config, 349 comment_delimiter=comment, 350 debug=debug, 351 factory=factory, 352 ignore_blank_lines=ignore_blank_lines, 353 syntax="junos", 354 CiscoConfParse=self, 355 ) 356 else: 357 error = "'{}' is an unknown syntax".format(syntax) 358 logger.critical(error) 359 raise ValueError(error) 360 361 except (IOError or FileNotFoundError): 362 error = "CiscoConfParse could not open() the filepath '%s'" % config 363 logger.critical(error) 364 raise RuntimeError 365 else: 366 error = "CiscoConfParse() received an invalid argument\n" 367 logger.critical(error) 368 raise RuntimeError(error) 369 370 self.ConfigObjs.CiscoConfParse = self 371 372 def __repr__(self): 373 return ( 374 "<CiscoConfParse: %s lines / syntax: %s / comment delimiter: '%s' / factory: %s>" 375 % (len(self.ConfigObjs), self.syntax, self.comment_delimiter, self.factory) 376 ) 377 378 @property 379 def openargs(self): 380 """Fix for Py3.5 deprecation of universal newlines - Ref Github #114 381 also see https://softwareengineering.stackexchange.com/q/298677/23144 382 """ 383 if sys.version_info >= ( 384 3, 385 5, 386 0, 387 ): 388 retval = {"mode": "r", "newline": None} 389 else: 390 retval = {"mode": "rU"} 391 return retval 392 393 @property 394 def ioscfg(self): 395 """A list containing all text configuration statements""" 396 ## I keep this here to emulate the legacy ciscoconfparse behavior 397 return list(map(attrgetter("text"), self.ConfigObjs)) 398 399 @property 400 def objs(self): 401 """An alias to the ``ConfigObjs`` attribute""" 402 return self.ConfigObjs 403 404 def atomic(self): 405 """Call :func:`~ciscoconfparse.CiscoConfParse.atomic` to manually fix 406 up ``ConfigObjs`` relationships 407 after modifying a parsed configuration. This method is slow; try to 408 batch calls to :func:`~ciscoconfparse.CiscoConfParse.atomic()` if 409 possible. 410 411 Warnings 412 -------- 413 If you modify a configuration after parsing it with 414 :class:`~ciscoconfparse.CiscoConfParse`, you *must* call 415 :func:`~ciscoconfparse.CiscoConfParse.commit` or 416 :func:`~ciscoconfparse.CiscoConfParse.atomic` before searching 417 the configuration again with methods such as 418 :func:`~ciscoconfparse.CiscoConfParse.find_objects` or 419 :func:`~ciscoconfparse.CiscoConfParse.find_lines`. Failure to 420 call :func:`~ciscoconfparse.CiscoConfParse.commit` or 421 :func:`~ciscoconfparse.CiscoConfParse.atomic` on config 422 modifications could lead to unexpected search results. 423 424 See Also 425 -------- 426 :func:`~ciscoconfparse.CiscoConfParse.commit` 427 428 """ 429 self.ConfigObjs._bootstrap_from_text() 430 431 def commit(self): 432 """Alias for calling the :func:`~ciscoconfparse.CiscoConfParse.atomic` 433 method. This method is slow; try to batch calls to 434 :func:`~ciscoconfparse.CiscoConfParse.commit()` if possible. 435 436 Warnings 437 -------- 438 If you modify a configuration after parsing it with 439 :class:`~ciscoconfparse.CiscoConfParse`, you *must* call 440 :func:`~ciscoconfparse.CiscoConfParse.commit` or 441 :func:`~ciscoconfparse.CiscoConfParse.atomic` before searching 442 the configuration again with methods such as 443 :func:`~ciscoconfparse.CiscoConfParse.find_objects` or 444 :func:`~ciscoconfparse.CiscoConfParse.find_lines`. Failure to 445 call :func:`~ciscoconfparse.CiscoConfParse.commit` or 446 :func:`~ciscoconfparse.CiscoConfParse.atomic` on config 447 modifications could lead to unexpected search results. 448 449 See Also 450 -------- 451 :func:`~ciscoconfparse.CiscoConfParse.atomic` 452 """ 453 self.atomic() 454 455 def convert_braces_to_ios(self, input_list, stop_width=4): 456 """ 457 Parameters 458 ---------- 459 input_list : list 460 A list of plain-text brace-delimited configuration lines 461 stop_width: int 462 An integer used to mark how many spaces each config level is indented. 463 464 Returns 465 ------- 466 list 467 An ios-style configuration list (indented by stop_width for each configuration level). 468 """ 469 ## Note to self, I made this regex fairly junos-specific... 470 assert "{" not in set(self.comment_delimiter) 471 assert "}" not in set(self.comment_delimiter) 472 473 JUNOS_RE_STR = r"""^ 474 (?:\s* 475 (?P<braces_close_left>\})*(?P<line1>.*?)(?P<braces_open_right>\{)*;* 476 |(?P<line2>[^\{\}]*?)(?P<braces_open_left>\{)(?P<condition2>.*?)(?P<braces_close_right>\});*\s* 477 |(?P<line3>[^\{\}]*?);*\s* 478 )$ 479 """ 480 LINE_RE = re.compile(JUNOS_RE_STR, re.VERBOSE) 481 482 COMMENT_RE = re.compile( 483 r"^\s*(?P<delimiter>[{0}]+)(?P<comment>[^{0}]*)$".format( 484 re.escape(self.comment_delimiter) 485 ) 486 ) 487 488 def parse_line_braces(input_str): 489 assert input_str is not None 490 indent_child = 0 491 indent_this_line = 0 492 493 mm = LINE_RE.search(input_str.strip()) 494 nn = COMMENT_RE.search(input_str.strip()) 495 496 if nn is not None: 497 results = nn.groupdict() 498 return ( 499 indent_this_line, 500 indent_child, 501 results.get("delimiter") + results.get("comment", ""), 502 ) 503 504 elif mm is not None: 505 results = mm.groupdict() 506 507 # } line1 { foo bar this } { 508 braces_close_left = bool(results.get("braces_close_left", "")) 509 braces_open_right = bool(results.get("braces_open_right", "")) 510 511 # line2 512 braces_open_left = bool(results.get("braces_open_left", "")) 513 braces_close_right = bool(results.get("braces_close_right", "")) 514 515 # line3 516 line1_str = results.get("line1", "") 517 line3_str = results.get("line3", "") 518 519 if braces_close_left and braces_open_right: 520 # Based off line1 521 # } elseif { bar baz } { 522 indent_this_line -= 1 523 indent_child += 0 524 retval = results.get("line1", None) 525 return (indent_this_line, indent_child, retval) 526 527 elif ( 528 bool(line1_str) 529 and (braces_close_left is False) 530 and (braces_open_right is False) 531 ): 532 # Based off line1: 533 # address 1.1.1.1 534 indent_this_line -= 0 535 indent_child += 0 536 retval = results.get("line1", "").strip() 537 # Strip empty braces here 538 retval = re.sub(r"\s*\{\s*\}\s*", "", retval) 539 return (indent_this_line, indent_child, retval) 540 541 elif ( 542 (line1_str == "") 543 and (braces_close_left is False) 544 and (braces_open_right is False) 545 ): 546 # Based off line1: 547 # return empty string 548 indent_this_line -= 0 549 indent_child += 0 550 return (indent_this_line, indent_child, "") 551 552 elif braces_open_left and braces_close_right: 553 # Based off line2 554 # this { bar baz } 555 indent_this_line -= 0 556 indent_child += 0 557 line = results.get("line2", None) or "" 558 condition = results.get("condition2", None) or "" 559 if condition.strip() == "": 560 retval = line 561 else: 562 retval = line + " {" + condition + " }" 563 return (indent_this_line, indent_child, retval) 564 565 elif braces_close_left: 566 # Based off line1 567 # } 568 indent_this_line -= 1 569 indent_child -= 1 570 return (indent_this_line, indent_child, "") 571 572 elif braces_open_right: 573 # Based off line1 574 # this that foo { 575 indent_this_line -= 0 576 indent_child += 1 577 line = results.get("line1", None) or "" 578 return (indent_this_line, indent_child, line) 579 580 elif (line3_str != "") and (line3_str is not None): 581 indent_this_line += 0 582 indent_child += 0 583 return (indent_this_line, indent_child, "") 584 585 else: 586 error = 'Cannot parse junos match:"{0}"'.format(input_str) 587 logger.critical(error) 588 raise ValueError(error) 589 590 else: 591 error = 'Cannot parse junos:"{0}"'.format(input_str) 592 logger.critical(error) 593 raise ValueError(error) 594 595 lines = list() 596 offset = 0 597 STOP_WIDTH = stop_width 598 for idx, tmp in enumerate(input_list): 599 if self.debug > 0: 600 logger.debug("Parse line {0}:'{1}'".format(idx + 1, tmp.strip())) 601 (indent_this_line, indent_child, line) = parse_line_braces(tmp.strip()) 602 lines.append( 603 (" " * STOP_WIDTH * (offset + indent_this_line)) + line.strip() 604 ) 605 offset += indent_child 606 return lines 607 608 def find_object_branches(self, branchspec=(), regex_flags=0, allow_none=True): 609 r"""This method iterates over a tuple of regular expressions in `branchspec` and returns the matching objects in a list of lists (consider it similar to a table of matching config objects). `branchspec` expects to start at some ancestor and walk through the nested object hierarchy (with no limit on depth). 610 611 Previous CiscoConfParse() methods only handled a single parent regex and single child regex (such as :func:`~ciscoconfparse.CiscoConfParse.find_parents_w_child`). 612 613 This method dives beyond a simple parent-child relationship to include entire family 'branches' (i.e. parents, children, grand-children, great-grand-children, etc). The result of handling longer regex chains is that it flattens what would otherwise be nested loops in your scripts; this makes parsing heavily-nested configuratations like Palo-Alto and F5 much simpler. Of course, there are plenty of applications for "flatter" config formats like IOS. 614 615 This method returns a list of lists (of object 'branches') which are nested to the same depth required in `branchspec`. However, unlike most other CiscoConfParse() methods, it returns an explicit `None` if there is no object match. Returning `None` allows a single search over configs that may not be uniformly nested in every branch. 616 617 Parameters 618 ---------- 619 branchspec : tuple 620 A tuple of python regular expressions to be matched. 621 regex_flags : 622 Chained regular expression flags, such as `re.IGNORECASE|re.MULTILINE` 623 allow_none : 624 Set False if you don't want an explicit `None` for missing branch elements (default is allow_none=True) 625 626 Returns 627 ------- 628 list 629 A list of lists of matching :class:`~ciscoconfparse.IOSCfgLine` objects 630 631 Examples 632 -------- 633 634 >>> from operator import attrgetter 635 >>> from ciscoconfparse import CiscoConfParse 636 >>> config = [ 637 ... 'ltm pool FOO {', 638 ... ' members {', 639 ... ' k8s-05.localdomain:8443 {', 640 ... ' address 192.0.2.5', 641 ... ' session monitor-enabled', 642 ... ' state up', 643 ... ' }', 644 ... ' k8s-06.localdomain:8443 {', 645 ... ' address 192.0.2.6', 646 ... ' session monitor-enabled', 647 ... ' state down', 648 ... ' }', 649 ... ' }', 650 ... '}', 651 ... 'ltm pool BAR {', 652 ... ' members {', 653 ... ' k8s-07.localdomain:8443 {', 654 ... ' address 192.0.2.7', 655 ... ' session monitor-enabled', 656 ... ' state down', 657 ... ' }', 658 ... ' }', 659 ... '}', 660 ... ] 661 >>> parse = CiscoConfParse(config, syntax='junos', comment='#') 662 >>> 663 >>> branchspec = (r'ltm\spool', r'members', r'\S+?:\d+', r'state\sup') 664 >>> branches = parse.find_object_branches(branchspec=branchspec) 665 >>> 666 >>> # We found three branches 667 >>> len(branches) 668 3 669 >>> # Each branch must match the length of branchspec 670 >>> len(branches[0]) 671 4 672 >>> # Print out one object 'branch' 673 >>> branches[0] 674 [<IOSCfgLine # 0 'ltm pool FOO'>, <IOSCfgLine # 1 ' members' (parent is # 0)>, <IOSCfgLine # 2 ' k8s-05.localdomain:8443' (parent is # 1)>, <IOSCfgLine # 5 ' state up' (parent is # 2)>] 675 >>> 676 >>> # Get the a list of text lines for this branch... 677 >>> list(map(attrgetter('text'), branches[0])) 678 ['ltm pool FOO', ' members', ' k8s-05.localdomain:8443', ' state up'] 679 >>> 680 >>> # Get the config text of the root object of the branch... 681 >>> branches[0][0].text 682 'ltm pool FOO' 683 >>> 684 >>> # Note: `None` in branches[1][-1] because of no regex match 685 >>> branches[1] 686 [<IOSCfgLine # 0 'ltm pool FOO'>, <IOSCfgLine # 1 ' members' (parent is # 0)>, <IOSCfgLine # 6 ' k8s-06.localdomain:8443' (parent is # 1)>, None] 687 >>> 688 >>> branches[2] 689 [<IOSCfgLine # 10 'ltm pool BAR'>, <IOSCfgLine # 11 ' members' (parent is # 10)>, <IOSCfgLine # 12 ' k8s-07.localdomain:8443' (parent is # 11)>, None] 690 """ 691 assert isinstance( 692 branchspec, tuple 693 ), "find_object_branches(): Please enclose the regular expressions in a Python tuple" 694 assert branchspec != (), "find_object_branches(): branchspec must not be empty" 695 696 def list_matching_children(parent_obj, childspec, regex_flags, allow_none=True): 697 ## I'm not using parent_obj.re_search_children() because 698 ## re_search_children() doesn't return None for no match... 699 700 # FIXME: Insert debugging here... 701 # print("PARENT "+str(parent_obj)) 702 703 # Get the child objects from parent objects 704 if parent_obj is None: 705 children = self._find_line_OBJ(linespec=childspec, exactmatch=False) 706 else: 707 children = parent_obj.children 708 709 # Find all child objects which match childspec... 710 segment_list = [ 711 cobj 712 for cobj in children 713 if re.search(childspec, cobj.text, regex_flags) 714 ] 715 # Return [None] if no children matched... 716 if (allow_none is True) and len(segment_list) == 0: 717 segment_list = [None] 718 719 # FIXME: Insert debugging here... 720 # print(" SEGMENTS "+str(segment_list)) 721 return segment_list 722 723 branches = list() 724 # iterate over the regular expressions in branchspec 725 for idx, childspec in enumerate(branchspec): 726 # FIXME: Insert debugging here... 727 # print("CHILDSPEC "+childspec) 728 if idx == 0: 729 # Get matching 'root' objects from the config 730 next_kids = list_matching_children( 731 parent_obj=None, 732 childspec=childspec, 733 regex_flags=regex_flags, 734 allow_none=allow_none, 735 ) 736 if allow_none is True: 737 # Start growing branches from the segments we received... 738 branches = [[kid] for kid in next_kids] 739 else: 740 branches = [[kid] for kid in next_kids if kid is not None] 741 742 else: 743 new_branches = list() 744 for branch in branches: 745 # Extend existing branches into the new_branches 746 if branch[-1] is not None: 747 # Find children to extend the family branch... 748 next_kids = list_matching_children( 749 parent_obj=branch[-1], 750 childspec=childspec, 751 regex_flags=regex_flags, 752 allow_none=allow_none, 753 ) 754 755 for kid in next_kids: 756 # Fork off a new branch and add each matching kid... 757 # Use copy.copy() for a "shallow copy" of branch: 758 # https://realpython.com/copying-python-objects/ 759 tmp = copy.copy(branch) 760 tmp.append(kid) 761 new_branches.append(tmp) 762 elif allow_none is True: 763 branch.append(None) 764 new_branches.append(branch) 765 766 # Ensure we have the most recent branches... 767 branches = new_branches 768 769 return branches 770 771 def find_interface_objects(self, intfspec, exactmatch=True): 772 """Find all :class:`~models_cisco.IOSCfgLine` or 773 :class:`~models_cisco.NXOSCfgLine` objects whose text 774 is an abbreviation for ``intfspec`` and return the 775 objects in a python list. 776 777 Notes 778 ----- 779 The configuration *must* be parsed with ``factory=True`` to use this method 780 781 Parameters 782 ---------- 783 intfspec : str 784 A string which is the abbreviation (or full name) of the interface 785 exactmatch : bool 786 Defaults to True; when True, this option requires ``intfspec`` match the whole interface name and number. 787 788 Returns 789 ------- 790 list 791 A list of matching :class:`~ciscoconfparse.IOSIntfLine` objects 792 793 Examples 794 -------- 795 796 >>> from ciscoconfparse import CiscoConfParse 797 >>> config = [ 798 ... '!', 799 ... 'interface Serial1/0', 800 ... ' ip address 1.1.1.1 255.255.255.252', 801 ... '!', 802 ... 'interface Serial1/1', 803 ... ' ip address 1.1.1.5 255.255.255.252', 804 ... '!', 805 ... ] 806 >>> parse = CiscoConfParse(config, factory=True) 807 >>> 808 >>> parse.find_interface_objects('Se 1/0') 809 [<IOSIntfLine # 1 'Serial1/0' info: '1.1.1.1/30'>] 810 >>> 811 812 """ 813 if (self.factory is not True): 814 error = "find_interface_objects() must be called with 'factory=True'" 815 logger.error(error) 816 raise ValueError(error) 817 818 retval = list() 819 if (self.syntax == "ios") or (self.syntax == "nxos"): 820 if exactmatch: 821 for obj in self.find_objects("^interface"): 822 if intfspec.lower() in obj.abbvs: 823 retval.append(obj) 824 break # Only break if exactmatch is True 825 else: 826 error = "This method requires exactmatch set True" 827 logger.error(error) 828 raise NotImplementedError(error) 829 ## TODO: implement ASAConfigLine.abbvs and others 830 else: 831 error = "This method requires exactmatch set True" 832 logger.error(error) 833 raise NotImplementedError(error) 834 835 return retval 836 837 def find_objects_dna(self, dnaspec, exactmatch=False): 838 """Find all :class:`~models_cisco.IOSCfgLine` objects whose text 839 matches ``dnaspec`` and return the :class:`~models_cisco.IOSCfgLine` 840 objects in a python list. 841 842 Notes 843 ----- 844 :func:`~ciscoconfparse.CiscoConfParse.find_objects_dna` requires the configuration to be parsed with factory=True 845 846 847 Parameters 848 ---------- 849 dnaspec : str 850 A string or python regular expression, which should be matched. This argument will be used to match dna attribute of the object 851 exactmatch : bool 852 Defaults to False. When set True, this option requires ``dnaspec`` match the whole configuration line, instead of a portion of the configuration line. 853 854 Returns 855 ------- 856 list 857 A list of matching :class:`~ciscoconfparse.IOSCfgLine` objects 858 859 Examples 860 -------- 861 862 >>> from ciscoconfparse import CiscoConfParse 863 >>> config = [ 864 ... '!', 865 ... 'hostname MyRouterHostname', 866 ... '!', 867 ... ] 868 >>> parse = CiscoConfParse(config, factory=True, syntax='ios') 869 >>> 870 >>> obj_list = parse.find_objects_dna(r'Hostname') 871 >>> obj_list 872 [<IOSHostnameLine # 1 'MyRouterHostname'>] 873 >>> 874 >>> # The IOSHostnameLine object has a hostname attribute 875 >>> obj_list[0].hostname 876 'MyRouterHostname' 877 """ 878 if not self.factory: 879 error = "find_objects_dna() must be called with 'factory=True'" 880 logger.error(error) 881 raise ValueError(error) 882 883 if not exactmatch: 884 # Return objects whose text attribute matches linespec 885 linespec_re = re.compile(dnaspec) 886 elif exactmatch: 887 # Return objects whose text attribute matches linespec exactly 888 linespec_re = re.compile("^{0}$".format(dnaspec)) 889 return list(filter(lambda obj: linespec_re.search(obj.dna), self.ConfigObjs)) 890 891 def find_objects(self, linespec, exactmatch=False, ignore_ws=False): 892 """Find all :class:`~models_cisco.IOSCfgLine` objects whose text 893 matches ``linespec`` and return the :class:`~models_cisco.IOSCfgLine` 894 objects in a python list. 895 :func:`~ciscoconfparse.CiscoConfParse.find_objects` is similar to 896 :func:`~ciscoconfparse.CiscoConfParse.find_lines`; however, the former 897 returns a list of :class:`~models_cisco.IOSCfgLine` objects, while the 898 latter returns a list of text configuration statements. Going 899 forward, I strongly encourage people to start using 900 :func:`~ciscoconfparse.CiscoConfParse.find_objects` instead of 901 :func:`~ciscoconfparse.CiscoConfParse.find_lines`. 902 903 Parameters 904 ---------- 905 linespec : str 906 A string or python regular expression, which should be matched 907 exactmatch : bool 908 Defaults to False. When set True, this option requires ``linespec`` match the whole configuration line, instead of a portion of the configuration line. 909 ignore_ws : bool 910 boolean that controls whether whitespace is ignored. Default is False. 911 912 Returns 913 ------- 914 list 915 A list of matching :class:`~ciscoconfparse.IOSCfgLine` objects 916 917 Examples 918 -------- 919 This example illustrates the difference between 920 :func:`~ciscoconfparse.CiscoConfParse.find_objects` and 921 :func:`~ciscoconfparse.CiscoConfParse.find_lines`. 922 923 >>> from ciscoconfparse import CiscoConfParse 924 >>> config = [ 925 ... '!', 926 ... 'interface Serial1/0', 927 ... ' ip address 1.1.1.1 255.255.255.252', 928 ... '!', 929 ... 'interface Serial1/1', 930 ... ' ip address 1.1.1.5 255.255.255.252', 931 ... '!', 932 ... ] 933 >>> parse = CiscoConfParse(config) 934 >>> 935 >>> parse.find_objects(r'^interface') 936 [<IOSCfgLine # 1 'interface Serial1/0'>, <IOSCfgLine # 4 'interface Serial1/1'>] 937 >>> 938 >>> parse.find_lines(r'^interface') 939 ['interface Serial1/0', 'interface Serial1/1'] 940 >>> 941 942 """ 943 if ignore_ws: 944 linespec = self._build_space_tolerant_regex(linespec) 945 return self._find_line_OBJ(linespec, exactmatch) 946 947 def find_lines(self, linespec, exactmatch=False, ignore_ws=False): 948 """This method is the equivalent of a simple configuration grep 949 (Case-sensitive). 950 951 Parameters 952 ---------- 953 linespec : str 954 Text regular expression for the line to be matched 955 exactmatch : bool 956 Defaults to False. When set True, this option requires ``linespec`` match the whole configuration line, instead of a portion of the configuration line. 957 ignore_ws : bool 958 boolean that controls whether whitespace is ignored. Default is False. 959 960 Returns 961 ------- 962 list 963 A list of matching configuration lines 964 """ 965 if ignore_ws: 966 linespec = self._build_space_tolerant_regex(linespec) 967 968 if exactmatch is False: 969 # Return the lines in self.ioscfg, which match linespec 970 return list(filter(re.compile(linespec).search, self.ioscfg)) 971 else: 972 # Return the lines in self.ioscfg, which match (exactly) linespec 973 return list(filter(re.compile("^%s$" % linespec).search, self.ioscfg)) 974 975 def find_children(self, linespec, exactmatch=False, ignore_ws=False): 976 """Returns the parents matching the linespec, and their immediate 977 children. This method is different than :meth:`find_all_children`, 978 because :meth:`find_all_children` finds children of children. 979 :meth:`find_children` only finds immediate children. 980 981 Parameters 982 ---------- 983 linespec : str 984 Text regular expression for the line to be matched 985 exactmatch : bool 986 boolean that controls whether partial matches are valid 987 ignore_ws : bool 988 boolean that controls whether whitespace is ignored 989 990 Returns 991 ------- 992 list 993 A list of matching configuration lines 994 995 Examples 996 -------- 997 998 >>> from ciscoconfparse import CiscoConfParse 999 >>> config = ['username ddclient password 7 107D3D232342041E3A', 1000 ... 'archive', 1001 ... ' log config', 1002 ... ' logging enable', 1003 ... ' hidekeys', 1004 ... ' path ftp://ns.foo.com//tftpboot/Foo-archive', 1005 ... '!', 1006 ... ] 1007 >>> p = CiscoConfParse(config) 1008 >>> p.find_children('^archive') 1009 ['archive', ' log config', ' path ftp://ns.foo.com//tftpboot/Foo-archive'] 1010 >>> 1011 """ 1012 if ignore_ws: 1013 linespec = self._build_space_tolerant_regex(linespec) 1014 1015 if exactmatch is False: 1016 parentobjs = self._find_line_OBJ(linespec) 1017 else: 1018 parentobjs = self._find_line_OBJ("^%s$" % linespec) 1019 1020 allobjs = set([]) 1021 for parent in parentobjs: 1022 if parent.has_children is True: 1023 allobjs.update(set(parent.children)) 1024 allobjs.add(parent) 1025 1026 return list(map(attrgetter("text"), sorted(allobjs))) 1027 1028 def find_all_children(self, linespec, exactmatch=False, ignore_ws=False): 1029 """Returns the parents matching the linespec, and all their children. 1030 This method is different than :meth:`find_children`, because 1031 :meth:`find_all_children` finds children of children. 1032 :meth:`find_children` only finds immediate children. 1033 1034 Parameters 1035 ---------- 1036 linespec : str 1037 Text regular expression for the line to be matched 1038 exactmatch : bool 1039 boolean that controls whether partial matches are valid 1040 ignore_ws : bool 1041 boolean that controls whether whitespace is ignored 1042 1043 Returns 1044 ------- 1045 list 1046 A list of matching configuration lines 1047 1048 Examples 1049 -------- 1050 Suppose you are interested in finding all `archive` statements in 1051 the following configuration... 1052 1053 .. code:: 1054 1055 username ddclient password 7 107D3D232342041E3A 1056 archive 1057 log config 1058 logging enable 1059 hidekeys 1060 path ftp://ns.foo.com//tftpboot/Foo-archive 1061 ! 1062 1063 Using the config above, we expect to find the following config lines... 1064 1065 .. code:: 1066 1067 archive 1068 log config 1069 logging enable 1070 hidekeys 1071 path ftp://ns.foo.com//tftpboot/Foo-archive 1072 1073 We would accomplish this by querying `find_all_children('^archive')`... 1074 1075 >>> from ciscoconfparse import CiscoConfParse 1076 >>> config = ['username ddclient password 7 107D3D232342041E3A', 1077 ... 'archive', 1078 ... ' log config', 1079 ... ' logging enable', 1080 ... ' hidekeys', 1081 ... ' path ftp://ns.foo.com//tftpboot/Foo-archive', 1082 ... '!', 1083 ... ] 1084 >>> p = CiscoConfParse(config) 1085 >>> p.find_all_children('^archive') 1086 ['archive', ' log config', ' logging enable', ' hidekeys', ' path ftp://ns.foo.com//tftpboot/Foo-archive'] 1087 >>> 1088 """ 1089 1090 if ignore_ws: 1091 linespec = self._build_space_tolerant_regex(linespec) 1092 1093 if exactmatch is False: 1094 parentobjs = self._find_line_OBJ(linespec) 1095 else: 1096 parentobjs = self._find_line_OBJ("^%s$" % linespec) 1097 1098 allobjs = set([]) 1099 for parent in parentobjs: 1100 allobjs.add(parent) 1101 allobjs.update(set(parent.all_children)) 1102 return list(map(attrgetter("text"), sorted(allobjs))) 1103 1104 def find_blocks(self, linespec, exactmatch=False, ignore_ws=False): 1105 """Find all siblings matching the linespec, then find all parents of 1106 those siblings. Return a list of config lines sorted by line number, 1107 lowest first. Note: any children of the siblings should NOT be 1108 returned. 1109 1110 Parameters 1111 ---------- 1112 linespec : str 1113 Text regular expression for the line to be matched 1114 exactmatch : bool 1115 boolean that controls whether partial matches are valid 1116 ignore_ws : bool 1117 boolean that controls whether whitespace is ignored 1118 1119 Returns 1120 ------- 1121 list 1122 A list of matching configuration lines 1123 1124 1125 Examples 1126 -------- 1127 This example finds `bandwidth percent` statements in following config, 1128 the siblings of those `bandwidth percent` statements, as well 1129 as the parent configuration statements required to access them. 1130 1131 .. code:: 1132 1133 ! 1134 policy-map EXTERNAL_CBWFQ 1135 class IP_PREC_HIGH 1136 priority percent 10 1137 police cir percent 10 1138 conform-action transmit 1139 exceed-action drop 1140 class IP_PREC_MEDIUM 1141 bandwidth percent 50 1142 queue-limit 100 1143 class class-default 1144 bandwidth percent 40 1145 queue-limit 100 1146 policy-map SHAPE_HEIR 1147 class ALL 1148 shape average 630000 1149 service-policy EXTERNAL_CBWFQ 1150 ! 1151 1152 The following config lines should be returned: 1153 1154 .. code:: 1155 1156 policy-map EXTERNAL_CBWFQ 1157 class IP_PREC_MEDIUM 1158 bandwidth percent 50 1159 queue-limit 100 1160 class class-default 1161 bandwidth percent 40 1162 queue-limit 100 1163 1164 We do this by quering `find_blocks('bandwidth percent')`... 1165 1166 .. code-block:: python 1167 :emphasize-lines: 22,25 1168 1169 >>> from ciscoconfparse import CiscoConfParse 1170 >>> config = ['!', 1171 ... 'policy-map EXTERNAL_CBWFQ', 1172 ... ' class IP_PREC_HIGH', 1173 ... ' priority percent 10', 1174 ... ' police cir percent 10', 1175 ... ' conform-action transmit', 1176 ... ' exceed-action drop', 1177 ... ' class IP_PREC_MEDIUM', 1178 ... ' bandwidth percent 50', 1179 ... ' queue-limit 100', 1180 ... ' class class-default', 1181 ... ' bandwidth percent 40', 1182 ... ' queue-limit 100', 1183 ... 'policy-map SHAPE_HEIR', 1184 ... ' class ALL', 1185 ... ' shape average 630000', 1186 ... ' service-policy EXTERNAL_CBWFQ', 1187 ... '!', 1188 ... ] 1189 >>> p = CiscoConfParse(config) 1190 >>> p.find_blocks('bandwidth percent') 1191 ['policy-map EXTERNAL_CBWFQ', ' class IP_PREC_MEDIUM', ' bandwidth percent 50', ' queue-limit 100', ' class class-default', ' bandwidth percent 40', ' queue-limit 100'] 1192 >>> 1193 >>> p.find_blocks(' class class-default') 1194 ['policy-map EXTERNAL_CBWFQ', ' class IP_PREC_HIGH', ' class IP_PREC_MEDIUM', ' class class-default'] 1195 >>> 1196 1197 """ 1198 tmp = set([]) 1199 1200 if ignore_ws: 1201 linespec = self._build_space_tolerant_regex(linespec) 1202 1203 # Find line objects maching the spec 1204 if exactmatch is False: 1205 objs = self._find_line_OBJ(linespec) 1206 else: 1207 objs = self._find_line_OBJ("^%s$" % linespec) 1208 1209 for obj in objs: 1210 tmp.add(obj) 1211 # Find the siblings of this line 1212 sib_objs = self._find_sibling_OBJ(obj) 1213 for sib_obj in sib_objs: 1214 tmp.add(sib_obj) 1215 1216 # Find the parents for everything 1217 pobjs = set([]) 1218 for lineobject in tmp: 1219 for pobj in lineobject.all_parents: 1220 pobjs.add(pobj) 1221 tmp.update(pobjs) 1222 1223 return list(map(attrgetter("text"), sorted(tmp))) 1224 1225 def find_objects_w_child( 1226 self, parentspec, childspec, ignore_ws=False, recurse=False 1227 ): 1228 """ 1229 Return a list of parent :class:`~models_cisco.IOSCfgLine` objects, 1230 which matched the ``parentspec`` and whose children match ``childspec``. 1231 Only the parent :class:`~models_cisco.IOSCfgLine` objects will be 1232 returned. 1233 1234 Parameters 1235 ---------- 1236 parentspec : str 1237 Text regular expression for the :class:`~models_cisco.IOSCfgLine` object to be matched; this must match the parent's line 1238 childspec : str 1239 Text regular expression for the line to be matched; this must match the child's line 1240 ignore_ws : bool 1241 boolean that controls whether whitespace is ignored 1242 recurse : bool 1243 Set True if you want to search all children (children, grand children, great grand children, etc...) 1244 1245 Returns 1246 ------- 1247 list 1248 A list of matching parent :class:`~models_cisco.IOSCfgLine` objects 1249 1250 Examples 1251 -------- 1252 This example uses :func:`~ciscoconfparse.find_objects_w_child()` to 1253 find all ports that are members of access vlan 300 in following 1254 config... 1255 1256 .. code:: 1257 1258 ! 1259 interface FastEthernet0/1 1260 switchport access vlan 532 1261 spanning-tree vlan 532 cost 3 1262 ! 1263 interface FastEthernet0/2 1264 switchport access vlan 300 1265 spanning-tree portfast 1266 ! 1267 interface FastEthernet0/3 1268 duplex full 1269 speed 100 1270 switchport access vlan 300 1271 spanning-tree portfast 1272 ! 1273 1274 The following interfaces should be returned: 1275 1276 .. code:: 1277 1278 interface FastEthernet0/2 1279 interface FastEthernet0/3 1280 1281 We do this by quering `find_objects_w_child()`; we set our 1282 parent as `^interface` and set the child as `switchport access 1283 vlan 300`. 1284 1285 .. code-block:: python 1286 :emphasize-lines: 20 1287 1288 >>> from ciscoconfparse import CiscoConfParse 1289 >>> config = ['!', 1290 ... 'interface FastEthernet0/1', 1291 ... ' switchport access vlan 532', 1292 ... ' spanning-tree vlan 532 cost 3', 1293 ... '!', 1294 ... 'interface FastEthernet0/2', 1295 ... ' switchport access vlan 300', 1296 ... ' spanning-tree portfast', 1297 ... '!', 1298 ... 'interface FastEthernet0/3', 1299 ... ' duplex full', 1300 ... ' speed 100', 1301 ... ' switchport access vlan 300', 1302 ... ' spanning-tree portfast', 1303 ... '!', 1304 ... ] 1305 >>> p = CiscoConfParse(config) 1306 >>> p.find_objects_w_child('^interface', 1307 ... 'switchport access vlan 300') 1308 ... 1309 [<IOSCfgLine # 5 'interface FastEthernet0/2'>, <IOSCfgLine # 9 'interface FastEthernet0/3'>] 1310 >>> 1311 """ 1312 1313 if ignore_ws: 1314 parentspec = self._build_space_tolerant_regex(parentspec) 1315 childspec = self._build_space_tolerant_regex(childspec) 1316 1317 return list( 1318 filter( 1319 lambda x: x.re_search_children(childspec, recurse=recurse), 1320 self.find_objects(parentspec), 1321 ) 1322 ) 1323 1324 def find_objects_w_all_children( 1325 self, parentspec, childspec, ignore_ws=False, recurse=False 1326 ): 1327 """Return a list of parent :class:`~models_cisco.IOSCfgLine` objects, 1328 which matched the ``parentspec`` and whose children match all elements 1329 in ``childspec``. Only the parent :class:`~models_cisco.IOSCfgLine` 1330 objects will be returned. 1331 1332 Parameters 1333 ---------- 1334 parentspec : str 1335 Text regular expression for the :class:`~models_cisco.IOSCfgLine` object to be matched; this must match the parent's line 1336 childspec : str 1337 A list of text regular expressions to be matched among the children 1338 ignore_ws : bool 1339 boolean that controls whether whitespace is ignored 1340 recurse : bool 1341 Set True if you want to search all children (children, grand children, great grand children, etc...) 1342 1343 Returns 1344 ------- 1345 list 1346 A list of matching parent :class:`~models_cisco.IOSCfgLine` objects 1347 1348 Examples 1349 -------- 1350 This example uses :func:`~ciscoconfparse.find_objects_w_child()` to 1351 find all ports that are members of access vlan 300 in following 1352 config... 1353 1354 .. code:: 1355 1356 ! 1357 interface FastEthernet0/1 1358 switchport access vlan 532 1359 spanning-tree vlan 532 cost 3 1360 ! 1361 interface FastEthernet0/2 1362 switchport access vlan 300 1363 spanning-tree portfast 1364 ! 1365 interface FastEthernet0/2 1366 duplex full 1367 speed 100 1368 switchport access vlan 300 1369 spanning-tree portfast 1370 ! 1371 1372 The following interfaces should be returned: 1373 1374 .. code:: 1375 1376 interface FastEthernet0/2 1377 interface FastEthernet0/3 1378 1379 We do this by quering `find_objects_w_all_children()`; we set our 1380 parent as `^interface` and set the childspec as 1381 ['switchport access vlan 300', 'spanning-tree portfast']. 1382 1383 .. code-block:: python 1384 :emphasize-lines: 19 1385 1386 >>> from ciscoconfparse import CiscoConfParse 1387 >>> config = ['!', 1388 ... 'interface FastEthernet0/1', 1389 ... ' switchport access vlan 532', 1390 ... ' spanning-tree vlan 532 cost 3', 1391 ... '!', 1392 ... 'interface FastEthernet0/2', 1393 ... ' switchport access vlan 300', 1394 ... ' spanning-tree portfast', 1395 ... '!', 1396 ... 'interface FastEthernet0/3', 1397 ... ' duplex full', 1398 ... ' speed 100', 1399 ... ' switchport access vlan 300', 1400 ... ' spanning-tree portfast', 1401 ... '!', 1402 ... ] 1403 >>> p = CiscoConfParse(config) 1404 >>> p.find_objects_w_all_children('^interface', 1405 ... ['switchport access vlan 300', 'spanning-tree portfast']) 1406 ... 1407 [<IOSCfgLine # 5 'interface FastEthernet0/2'>, <IOSCfgLine # 9 'interface FastEthernet0/3'>] 1408 >>> 1409 """ 1410 1411 assert bool(getattr(childspec, "append")) # Childspec must be a list 1412 retval = list() 1413 if ignore_ws: 1414 parentspec = self._build_space_tolerant_regex(parentspec) 1415 childspec = map(self._build_space_tolerant_regex, childspec) 1416 1417 for parentobj in self.find_objects(parentspec): 1418 results = set([]) 1419 for child_cfg in childspec: 1420 results.add( 1421 bool(parentobj.re_search_children(child_cfg, recurse=recurse)) 1422 ) 1423 if False in results: 1424 continue 1425 else: 1426 retval.append(parentobj) 1427 1428 return retval 1429 1430 def find_objects_w_missing_children(self, parentspec, childspec, ignore_ws=False): 1431 """Return a list of parent :class:`~models_cisco.IOSCfgLine` objects, 1432 which matched the ``parentspec`` and whose children do not match 1433 all elements in ``childspec``. Only the parent 1434 :class:`~models_cisco.IOSCfgLine` objects will be returned. 1435 1436 Parameters 1437 ---------- 1438 parentspec : str 1439 Text regular expression for the :class:`~models_cisco.IOSCfgLine` object to be matched; this must match the parent's line 1440 childspec : str 1441 A list of text regular expressions to be matched among the children 1442 ignore_ws : bool 1443 boolean that controls whether whitespace is ignored 1444 1445 Returns 1446 ------- 1447 list 1448 A list of matching parent :class:`~models_cisco.IOSCfgLine` objects""" 1449 assert bool(getattr(childspec, "append")) # Childspec must be a list 1450 retval = list() 1451 if ignore_ws: 1452 parentspec = self._build_space_tolerant_regex(parentspec) 1453 childspec = map(self._build_space_tolerant_regex, childspec) 1454 1455 for parentobj in self.find_objects(parentspec): 1456 results = set([]) 1457 for child_cfg in childspec: 1458 results.add(bool(parentobj.re_search_children(child_cfg))) 1459 if False in results: 1460 retval.append(parentobj) 1461 else: 1462 continue 1463 1464 return retval 1465 1466 def find_parents_w_child(self, parentspec, childspec, ignore_ws=False): 1467 """Parse through all children matching childspec, and return a list of 1468 parents that matched the parentspec. Only the parent lines will be 1469 returned. 1470 1471 Parameters 1472 ---------- 1473 parentspec : str 1474 Text regular expression for the line to be matched; this must match the parent's line 1475 childspec : str 1476 Text regular expression for the line to be matched; this must match the child's line 1477 ignore_ws : bool 1478 boolean that controls whether whitespace is ignored 1479 1480 Returns 1481 ------- 1482 list 1483 A list of matching parent configuration lines 1484 1485 Examples 1486 -------- 1487 This example finds all ports that are members of access vlan 300 1488 in following config... 1489 1490 .. code:: 1491 1492 ! 1493 interface FastEthernet0/1 1494 switchport access vlan 532 1495 spanning-tree vlan 532 cost 3 1496 ! 1497 interface FastEthernet0/2 1498 switchport access vlan 300 1499 spanning-tree portfast 1500 ! 1501 interface FastEthernet0/2 1502 duplex full 1503 speed 100 1504 switchport access vlan 300 1505 spanning-tree portfast 1506 ! 1507 1508 The following interfaces should be returned: 1509 1510 .. code:: 1511 1512 interface FastEthernet0/2 1513 interface FastEthernet0/3 1514 1515 We do this by quering `find_parents_w_child()`; we set our 1516 parent as `^interface` and set the child as 1517 `switchport access vlan 300`. 1518 1519 .. code-block:: python 1520 :emphasize-lines: 18 1521 1522 >>> from ciscoconfparse import CiscoConfParse 1523 >>> config = ['!', 1524 ... 'interface FastEthernet0/1', 1525 ... ' switchport access vlan 532', 1526 ... ' spanning-tree vlan 532 cost 3', 1527 ... '!', 1528 ... 'interface FastEthernet0/2', 1529 ... ' switchport access vlan 300', 1530 ... ' spanning-tree portfast', 1531 ... '!', 1532 ... 'interface FastEthernet0/3', 1533 ... ' duplex full', 1534 ... ' speed 100', 1535 ... ' switchport access vlan 300', 1536 ... ' spanning-tree portfast', 1537 ... '!', 1538 ... ] 1539 >>> p = CiscoConfParse(config) 1540 >>> p.find_parents_w_child('^interface', 'switchport access vlan 300') 1541 ['interface FastEthernet0/2', 'interface FastEthernet0/3'] 1542 >>> 1543 1544 """ 1545 tmp = self.find_objects_w_child(parentspec, childspec, ignore_ws=ignore_ws) 1546 return list(map(attrgetter("text"), tmp)) 1547 1548 def find_objects_wo_child(self, parentspec, childspec, ignore_ws=False): 1549 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. 1550 1551 Parameters 1552 ---------- 1553 parentspec : str 1554 Text regular expression for the :class:`~models_cisco.IOSCfgLine` object to be matched; this must match the parent's line 1555 childspec : str 1556 Text regular expression for the line to be matched; this must match the child's line 1557 ignore_ws : bool 1558 boolean that controls whether whitespace is ignored 1559 1560 Returns 1561 ------- 1562 list 1563 A list of matching parent configuration lines 1564 1565 Examples 1566 -------- 1567 This example finds all ports that are autonegotiating in the following config... 1568 1569 .. code:: 1570 1571 ! 1572 interface FastEthernet0/1 1573 switchport access vlan 532 1574 spanning-tree vlan 532 cost 3 1575 ! 1576 interface FastEthernet0/2 1577 switchport access vlan 300 1578 spanning-tree portfast 1579 ! 1580 interface FastEthernet0/2 1581 duplex full 1582 speed 100 1583 switchport access vlan 300 1584 spanning-tree portfast 1585 ! 1586 1587 The following interfaces should be returned: 1588 1589 .. code:: 1590 1591 interface FastEthernet0/1 1592 interface FastEthernet0/2 1593 1594 We do this by quering `find_objects_wo_child()`; we set our 1595 parent as `^interface` and set the child as `speed\s\d+` (a 1596 regular-expression which matches the word 'speed' followed by 1597 an integer). 1598 1599 .. code-block:: python 1600 :emphasize-lines: 19 1601 1602 >>> from ciscoconfparse import CiscoConfParse 1603 >>> config = ['!', 1604 ... 'interface FastEthernet0/1', 1605 ... ' switchport access vlan 532', 1606 ... ' spanning-tree vlan 532 cost 3', 1607 ... '!', 1608 ... 'interface FastEthernet0/2', 1609 ... ' switchport access vlan 300', 1610 ... ' spanning-tree portfast', 1611 ... '!', 1612 ... 'interface FastEthernet0/3', 1613 ... ' duplex full', 1614 ... ' speed 100', 1615 ... ' switchport access vlan 300', 1616 ... ' spanning-tree portfast', 1617 ... '!', 1618 ... ] 1619 >>> p = CiscoConfParse(config) 1620 >>> p.find_objects_wo_child(r'^interface', r'speed\s\d+') 1621 [<IOSCfgLine # 1 'interface FastEthernet0/1'>, <IOSCfgLine # 5 'interface FastEthernet0/2'>] 1622 >>> 1623 """ 1624 1625 if ignore_ws: 1626 parentspec = self._build_space_tolerant_regex(parentspec) 1627 childspec = self._build_space_tolerant_regex(childspec) 1628 1629 return [ 1630 obj 1631 for obj in self.find_objects(parentspec) 1632 if not obj.re_search_children(childspec) 1633 ] 1634 1635 def find_parents_wo_child(self, parentspec, childspec, ignore_ws=False): 1636 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. 1637 1638 Parameters 1639 ---------- 1640 parentspec : str 1641 Text regular expression for the line to be matched; this must match the parent's line 1642 childspec : str 1643 Text regular expression for the line to be matched; this must match the child's line 1644 ignore_ws : bool 1645 boolean that controls whether whitespace is ignored 1646 1647 Returns 1648 ------- 1649 list 1650 A list of matching parent configuration lines 1651 1652 Examples 1653 -------- 1654 This example finds all ports that are autonegotiating in the 1655 following config... 1656 1657 .. code:: 1658 1659 ! 1660 interface FastEthernet0/1 1661 switchport access vlan 532 1662 spanning-tree vlan 532 cost 3 1663 ! 1664 interface FastEthernet0/2 1665 switchport access vlan 300 1666 spanning-tree portfast 1667 ! 1668 interface FastEthernet0/2 1669 duplex full 1670 speed 100 1671 switchport access vlan 300 1672 spanning-tree portfast 1673 ! 1674 1675 The following interfaces should be returned: 1676 1677 .. code:: 1678 1679 interface FastEthernet0/1 1680 interface FastEthernet0/2 1681 1682 We do this by quering `find_parents_wo_child()`; we set our 1683 parent as `^interface` and set the child as `speed\s\d+` (a 1684 regular-expression which matches the word 'speed' followed by 1685 an integer). 1686 1687 .. code-block:: python 1688 :emphasize-lines: 19 1689 1690 >>> from ciscoconfparse import CiscoConfParse 1691 >>> config = ['!', 1692 ... 'interface FastEthernet0/1', 1693 ... ' switchport access vlan 532', 1694 ... ' spanning-tree vlan 532 cost 3', 1695 ... '!', 1696 ... 'interface FastEthernet0/2', 1697 ... ' switchport access vlan 300', 1698 ... ' spanning-tree portfast', 1699 ... '!', 1700 ... 'interface FastEthernet0/3', 1701 ... ' duplex full', 1702 ... ' speed 100', 1703 ... ' switchport access vlan 300', 1704 ... ' spanning-tree portfast', 1705 ... '!', 1706 ... ] 1707 >>> p = CiscoConfParse(config) 1708 >>> p.find_parents_wo_child('^interface', 'speed\s\d+') 1709 ['interface FastEthernet0/1', 'interface FastEthernet0/2'] 1710 >>> 1711 1712 """ 1713 tmp = self.find_objects_wo_child(parentspec, childspec, ignore_ws=ignore_ws) 1714 return list(map(attrgetter("text"), tmp)) 1715 1716 def find_children_w_parents(self, parentspec, childspec, ignore_ws=False): 1717 r"""Parse through the children of all parents matching parentspec, 1718 and return a list of children that matched the childspec. 1719 1720 Parameters 1721 ---------- 1722 parentspec : str 1723 Text regular expression for the line to be matched; this must match the parent's line 1724 childspec : str 1725 Text regular expression for the line to be matched; this must match the child's line 1726 ignore_ws : bool 1727 boolean that controls whether whitespace is ignored 1728 1729 Returns 1730 ------- 1731 list 1732 A list of matching child configuration lines 1733 1734 Examples 1735 -------- 1736 This example finds the port-security lines on FastEthernet0/1 in 1737 following config... 1738 1739 .. code:: 1740 1741 ! 1742 interface FastEthernet0/1 1743 switchport access vlan 532 1744 switchport port-security 1745 switchport port-security violation protect 1746 switchport port-security aging time 5 1747 switchport port-security aging type inactivity 1748 spanning-tree portfast 1749 spanning-tree bpduguard enable 1750 ! 1751 interface FastEthernet0/2 1752 switchport access vlan 300 1753 spanning-tree portfast 1754 spanning-tree bpduguard enable 1755 ! 1756 interface FastEthernet0/2 1757 duplex full 1758 speed 100 1759 switchport access vlan 300 1760 spanning-tree portfast 1761 spanning-tree bpduguard enable 1762 ! 1763 1764 The following lines should be returned: 1765 1766 .. code:: 1767 1768 switchport port-security 1769 switchport port-security violation protect 1770 switchport port-security aging time 5 1771 switchport port-security aging type inactivity 1772 1773 We do this by quering `find_children_w_parents()`; we set our 1774 parent as `^interface` and set the child as 1775 `switchport port-security`. 1776 1777 .. code-block:: python 1778 :emphasize-lines: 26 1779 1780 >>> from ciscoconfparse import CiscoConfParse 1781 >>> config = ['!', 1782 ... 'interface FastEthernet0/1', 1783 ... ' switchport access vlan 532', 1784 ... ' switchport port-security', 1785 ... ' switchport port-security violation protect', 1786 ... ' switchport port-security aging time 5', 1787 ... ' switchport port-security aging type inactivity', 1788 ... ' spanning-tree portfast', 1789 ... ' spanning-tree bpduguard enable', 1790 ... '!', 1791 ... 'interface FastEthernet0/2', 1792 ... ' switchport access vlan 300', 1793 ... ' spanning-tree portfast', 1794 ... ' spanning-tree bpduguard enable', 1795 ... '!', 1796 ... 'interface FastEthernet0/3', 1797 ... ' duplex full', 1798 ... ' speed 100', 1799 ... ' switchport access vlan 300', 1800 ... ' spanning-tree portfast', 1801 ... ' spanning-tree bpduguard enable', 1802 ... '!', 1803 ... ] 1804 >>> p = CiscoConfParse(config) 1805 >>> p.find_children_w_parents('^interface\sFastEthernet0/1', 1806 ... 'port-security') 1807 [' switchport port-security', ' switchport port-security violation protect', ' switchport port-security aging time 5', ' switchport port-security aging type inactivity'] 1808 >>> 1809 1810 """ 1811 if ignore_ws: 1812 parentspec = self._build_space_tolerant_regex(parentspec) 1813 childspec = self._build_space_tolerant_regex(childspec) 1814 1815 retval = set([]) 1816 childobjs = self._find_line_OBJ(childspec) 1817 for child in childobjs: 1818 parents = child.all_parents 1819 for parent in parents: 1820 if re.search(parentspec, parent.text): 1821 retval.add(child) 1822 1823 return list(map(attrgetter("text"), sorted(retval))) 1824 1825 def find_objects_w_parents(self, parentspec, childspec, ignore_ws=False): 1826 r"""Parse through the children of all parents matching parentspec, 1827 and return a list of child objects, which matched the childspec. 1828 1829 Parameters 1830 ---------- 1831 parentspec : str 1832 Text regular expression for the line to be matched; this must match the parent's line 1833 childspec : str 1834 Text regular expression for the line to be matched; this must match the child's line 1835 ignore_ws : bool 1836 boolean that controls whether whitespace is ignored 1837 1838 Returns 1839 ------- 1840 list 1841 A list of matching child objects 1842 1843 Examples 1844 -------- 1845 This example finds the object for "ge-0/0/0" under "interfaces" in the 1846 following config... 1847 1848 .. code:: 1849 1850 interfaces 1851 ge-0/0/0 1852 unit 0 1853 family ethernet-switching 1854 port-mode access 1855 vlan 1856 members VLAN_FOO 1857 ge-0/0/1 1858 unit 0 1859 family ethernet-switching 1860 port-mode trunk 1861 vlan 1862 members all 1863 native-vlan-id 1 1864 vlan 1865 unit 0 1866 family inet 1867 address 172.16.15.5/22 1868 1869 1870 The following object should be returned: 1871 1872 .. code:: 1873 1874 <IOSCfgLine # 7 ' ge-0/0/1' (parent is # 0)> 1875 1876 We do this by quering `find_objects_w_parents()`; we set our 1877 parent as `^\s*interface` and set the child as 1878 `^\s+ge-0/0/1`. 1879 1880 .. code-block:: python 1881 :emphasize-lines: 22,23 1882 1883 >>> from ciscoconfparse import CiscoConfParse 1884 >>> config = ['interfaces', 1885 ... ' ge-0/0/0', 1886 ... ' unit 0', 1887 ... ' family ethernet-switching', 1888 ... ' port-mode access', 1889 ... ' vlan', 1890 ... ' members VLAN_FOO', 1891 ... ' ge-0/0/1', 1892 ... ' unit 0', 1893 ... ' family ethernet-switching', 1894 ... ' port-mode trunk', 1895 ... ' vlan', 1896 ... ' members all', 1897 ... ' native-vlan-id 1', 1898 ... ' vlan', 1899 ... ' unit 0', 1900 ... ' family inet', 1901 ... ' address 172.16.15.5/22', 1902 ... ] 1903 >>> p = CiscoConfParse(config) 1904 >>> p.find_objects_w_parents('^\s*interfaces', 1905 ... r'\s+ge-0/0/1') 1906 [<IOSCfgLine # 7 ' ge-0/0/1' (parent is # 0)>] 1907 >>> 1908 1909 """ 1910 if ignore_ws: 1911 parentspec = self._build_space_tolerant_regex(parentspec) 1912 childspec = self._build_space_tolerant_regex(childspec) 1913 1914 retval = set([]) 1915 childobjs = self._find_line_OBJ(childspec) 1916 for child in childobjs: 1917 parents = child.all_parents 1918 for parent in parents: 1919 if re.search(parentspec, parent.text): 1920 retval.add(child) 1921 1922 return sorted(retval) 1923 1924 def find_lineage(self, linespec, exactmatch=False): 1925 """Iterate through to the oldest ancestor of this object, and return 1926 a list of all ancestors / children in the direct line. Cousins or 1927 aunts / uncles are *not* returned. Note, all children 1928 of this object are returned. 1929 1930 Parameters 1931 ---------- 1932 linespec : str 1933 Text regular expression for the line to be matched 1934 exactmatch : bool 1935 Defaults to False; when True, this option requires ``linespec`` the whole line (not merely a portion of the line) 1936 1937 Returns 1938 ------- 1939 list 1940 A list of matching objects 1941 """ 1942 tmp = self.find_objects(linespec, exactmatch=exactmatch) 1943 if len(tmp) > 1: 1944 error = "linespec must be unique" 1945 logger.error(error) 1946 raise ValueError(error) 1947 1948 return [obj.text for obj in tmp[0].lineage] 1949 1950 def has_line_with(self, linespec): 1951 return self.ConfigObjs.has_line_with(linespec) 1952 1953 def insert_before( 1954 self, exist_val, new_val="", exactmatch=False, ignore_ws=False, atomic=False, **kwargs 1955 ): 1956 """Find all objects whose text matches exist_val, and insert 'new_val' before those line objects""" 1957 1958 ###################################################################### 1959 # Named parameter migration warnings... 1960 # - `linespec` is now called exist_val 1961 # - `insertstr` is now called new_val 1962 ###################################################################### 1963 if kwargs.get("linespec", ""): 1964 exist_val = kwargs.get("linespec") 1965 logger.info("The parameter named `linespec` is deprecated. Please use `exist_val` instead") 1966 if kwargs.get("insertstr", ""): 1967 new_val = kwargs.get("insertstr") 1968 logger.info("The parameter named `insertstr` is deprecated. Please use `new_val` instead") 1969 1970 error_exist_val = "FATAL: exist_val:'%s' must be a string" % exist_val 1971 error_new_val = "FATAL: new_val:'%s' must be a string" % new_val 1972 assert isinstance(exist_val, str), error_exist_val 1973 assert isinstance(new_val, str), error_new_val 1974 1975 objs = self.find_objects(exist_val, exactmatch, ignore_ws) 1976 self.ConfigObjs.insert_before(exist_val, new_val, atomic=atomic) 1977 self.commit() 1978 return list(map(attrgetter("text"), sorted(objs))) 1979 1980###########################################################################start 1981 def insert_after( 1982 self, exist_val, new_val="", exactmatch=False, ignore_ws=False, atomic=False, **kwargs 1983 ): 1984 """Find all :class:`~models_cisco.IOSCfgLine` objects whose text 1985 matches ``exist_val``, and insert ``new_val`` after those line 1986 objects""" 1987 1988 ###################################################################### 1989 # Named parameter migration warnings... 1990 # - `linespec` is now called exist_val 1991 # - `insertstr` is now called new_val 1992 ###################################################################### 1993 if kwargs.get("linespec", ""): 1994 exist_val = kwargs.get("linespec") 1995 logger.info("The parameter named `linespec` is deprecated. Please use `exist_val` instead") 1996 if kwargs.get("insertstr", ""): 1997 new_val = kwargs.get("insertstr") 1998 logger.info("The parameter named `insertstr` is deprecated. Please use `new_val` instead") 1999 2000 error_exist_val = "FATAL: exist_val:'%s' must be a string" % exist_val 2001 error_new_val = "FATAL: new_val:'%s' must be a string" % new_val 2002 assert isinstance(exist_val, str), error_exist_val 2003 assert isinstance(new_val, str), error_new_val 2004 2005 objs = self.find_objects(exist_val, exactmatch, ignore_ws) 2006 self.ConfigObjs.insert_after(exist_val, new_val, atomic=atomic) 2007 return list(map(attrgetter("text"), sorted(objs))) 2008###########################################################################stop 2009 2010 def insert_after_child( 2011 self, 2012 parentspec, 2013 childspec, 2014 insertstr="", 2015 exactmatch=False, 2016 excludespec=None, 2017 ignore_ws=False, 2018 atomic=False, 2019 ): 2020 """Find all :class:`~models_cisco.IOSCfgLine` objects whose text 2021 matches ``linespec`` and have a child matching ``childspec``, and 2022 insert an :class:`~models_cisco.IOSCfgLine` object for ``insertstr`` 2023 after those child objects.""" 2024 retval = list() 2025 modified = False 2026 for pobj in self._find_line_OBJ(parentspec, exactmatch=exactmatch): 2027 if excludespec and re.search(excludespec, pobj.text): 2028 # Exclude replacements on pobj lines which match excludespec 2029 continue 2030 for cobj in pobj.children: 2031 if excludespec and re.search(excludespec, cobj.text): 2032 # Exclude replacements on pobj lines which match excludespec 2033 continue 2034 elif re.search(childspec, cobj.text): 2035 modified = True 2036 retval.append( 2037 self.ConfigObjs.insert_after(cobj, insertstr, atomic=atomic) 2038 ) 2039 else: 2040 pass 2041 return retval 2042 2043 def delete_lines(self, linespec, exactmatch=False, ignore_ws=False): 2044 """Find all :class:`~models_cisco.IOSCfgLine` objects whose text 2045 matches linespec, and delete the object""" 2046 objs = self.find_objects(linespec, exactmatch, ignore_ws) 2047 for obj in reversed(objs): 2048 # NOTE - 'del self.ConfigObjs...' was replaced in version 1.5.30 2049 # with a simpler approach 2050 # del self.ConfigObjs[obj.linenum] 2051 obj.delete() 2052 2053 def prepend_line(self, linespec): 2054 """Unconditionally insert an :class:`~models_cisco.IOSCfgLine` object 2055 for ``linespec`` (a text line) at the top of the configuration""" 2056 self.ConfigObjs.insert(0, linespec) 2057 return self.ConfigObjs[0] 2058 2059 def append_line(self, linespec): 2060 """Unconditionally insert ``linespec`` (a text line) at the end of the 2061 configuration 2062 2063 Parameters 2064 ---------- 2065 linespec : str 2066 Text IOS configuration line 2067 2068 Returns 2069 ------- 2070 The parsed :class:`~models_cisco.IOSCfgLine` object instance 2071 2072 """ 2073 self.ConfigObjs.append(linespec) 2074 return self.ConfigObjs[-1] 2075 2076 def replace_lines( 2077 self, linespec, replacestr, excludespec=None, exactmatch=False, atomic=False 2078 ): 2079 """This method is a text search and replace (Case-sensitive). You can 2080 optionally exclude lines from replacement by including a string (or 2081 compiled regular expression) in `excludespec`. 2082 2083 Parameters 2084 ---------- 2085 linespec : str 2086 Text regular expression for the line to be matched 2087 replacestr : str 2088 Text used to replace strings matching linespec 2089 excludespec : str 2090 Text regular expression used to reject lines, which would otherwise be replaced. Default value of ``excludespec`` is None, which means nothing is excluded 2091 exactmatch : bool 2092 boolean that controls whether partial matches are valid 2093 atomic : bool 2094 boolean that controls whether the config is reparsed after replacement (default False) 2095 2096 Returns 2097 ------- 2098 list 2099 A list of changed configuration lines 2100 2101 Examples 2102 -------- 2103 This example finds statements with `EXTERNAL_CBWFQ` in following 2104 config, and replaces all matching lines (in-place) with `EXTERNAL_QOS`. 2105 For the purposes of this example, let's assume that we do *not* want 2106 to make changes to any descriptions on the policy. 2107 2108 .. code:: 2109 2110 ! 2111 policy-map EXTERNAL_CBWFQ 2112 description implement an EXTERNAL_CBWFQ policy 2113 class IP_PREC_HIGH 2114 priority percent 10 2115 police cir percent 10 2116 conform-action transmit 2117 exceed-action drop 2118 class IP_PREC_MEDIUM 2119 bandwidth percent 50 2120 queue-limit 100 2121 class class-default 2122 bandwidth percent 40 2123 queue-limit 100 2124 policy-map SHAPE_HEIR 2125 class ALL 2126 shape average 630000 2127 service-policy EXTERNAL_CBWFQ 2128 ! 2129 2130 We do this by calling `replace_lines(linespec='EXTERNAL_CBWFQ', 2131 replacestr='EXTERNAL_QOS', excludespec='description')`... 2132 2133 .. code-block:: python 2134 :emphasize-lines: 23 2135 2136 >>> from ciscoconfparse import CiscoConfParse 2137 >>> config = ['!', 2138 ... 'policy-map EXTERNAL_CBWFQ', 2139 ... ' description implement an EXTERNAL_CBWFQ policy', 2140 ... ' class IP_PREC_HIGH', 2141 ... ' priority percent 10', 2142 ... ' police cir percent 10', 2143 ... ' conform-action transmit', 2144 ... ' exceed-action drop', 2145 ... ' class IP_PREC_MEDIUM', 2146 ... ' bandwidth percent 50', 2147 ... ' queue-limit 100', 2148 ... ' class class-default', 2149 ... ' bandwidth percent 40', 2150 ... ' queue-limit 100', 2151 ... 'policy-map SHAPE_HEIR', 2152 ... ' class ALL', 2153 ... ' shape average 630000', 2154 ... ' service-policy EXTERNAL_CBWFQ', 2155 ... '!', 2156 ... ] 2157 >>> p = CiscoConfParse(config) 2158 >>> p.replace_lines('EXTERNAL_CBWFQ', 'EXTERNAL_QOS', 'description') 2159 ['policy-map EXTERNAL_QOS', ' service-policy EXTERNAL_QOS'] 2160 >>> 2161 >>> # Now when we call `p.find_blocks('policy-map EXTERNAL_QOS')`, we get the 2162 >>> # changed configuration, which has the replacements except on the 2163 >>> # policy-map's description. 2164 >>> p.find_blocks('EXTERNAL_QOS') 2165 ['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'] 2166 >>> 2167 2168 """ 2169 retval = list() 2170 ## Since we are replacing text, we *must* operate on ConfigObjs 2171 if excludespec: 2172 excludespec_re = re.compile(excludespec) 2173 2174 for obj in self._find_line_OBJ(linespec, exactmatch=exactmatch): 2175 if excludespec and excludespec_re.search(obj.text): 2176 # Exclude replacements on lines which match excludespec 2177 continue 2178 retval.append(obj.re_sub(linespec, replacestr)) 2179 2180 if self.factory and atomic: 2181 # self.ConfigObjs._reassign_linenums() 2182 self.ConfigObjs._bootstrap_from_text() 2183 2184 return retval 2185 2186 def replace_children( 2187 self, 2188 parentspec, 2189 childspec, 2190 replacestr, 2191 excludespec=None, 2192 exactmatch=False, 2193 atomic=False, 2194 ): 2195 r"""Replace lines matching `childspec` within the `parentspec`'s 2196 immediate children. 2197 2198 Parameters 2199 ---------- 2200 parentspec : str 2201 Text IOS configuration line 2202 childspec : str 2203 Text IOS configuration line, or regular expression 2204 replacestr : str 2205 Text IOS configuration, which should replace text matching ``childspec``. 2206 excludespec : str 2207 A regular expression, which indicates ``childspec`` lines which *must* be skipped. If ``excludespec`` is None, no lines will be excluded. 2208 exactmatch : bool 2209 Defaults to False. When set True, this option requires ``linespec`` match the whole configuration line, instead of a portion of the configuration line. 2210 2211 Returns 2212 ------- 2213 list 2214 A list of changed :class:`~models_cisco.IOSCfgLine` instances. 2215 2216 Examples 2217 -------- 2218 `replace_children()` just searches through a parent's child lines and 2219 replaces anything matching `childspec` with `replacestr`. This method 2220 is one of my favorites for quick and dirty standardization efforts if 2221 you *know* the commands are already there (just set inconsistently). 2222 2223 One very common use case is rewriting all vlan access numbers in a 2224 configuration. The following example sets 2225 `storm-control broadcast level 0.5` on all GigabitEthernet ports. 2226 2227 .. code-block:: python 2228 :emphasize-lines: 13 2229 2230 >>> from ciscoconfparse import CiscoConfParse 2231 >>> config = ['!', 2232 ... 'interface GigabitEthernet1/1', 2233 ... ' description {I have a broken storm-control config}', 2234 ... ' switchport', 2235 ... ' switchport mode access', 2236 ... ' switchport access vlan 50', 2237 ... ' switchport nonegotiate', 2238 ... ' storm-control broadcast level 0.2', 2239 ... '!' 2240 ... ] 2241 >>> p = CiscoConfParse(config) 2242 >>> p.replace_children(r'^interface\sGigabit', r'broadcast\slevel\s\S+', 'broadcast level 0.5') 2243 [' storm-control broadcast level 0.5'] 2244 >>> 2245 2246 One thing to remember about the last example, you *cannot* use a 2247 regular expression in `replacestr`; just use a normal python string. 2248 """ 2249 retval = list() 2250 ## Since we are replacing text, we *must* operate on ConfigObjs 2251 childspec_re = re.compile(childspec) 2252 if excludespec: 2253 excludespec_re = re.compile(excludespec) 2254 for pobj in self._find_line_OBJ(parentspec, exactmatch=exactmatch): 2255 if excludespec and excludespec_re.search(pobj.text): 2256 # Exclude replacements on pobj lines which match excludespec 2257 continue 2258 for cobj in pobj.children: 2259 if excludespec and excludespec_re.search(cobj.text): 2260 # Exclude replacements on pobj lines which match excludespec 2261 continue 2262 elif childspec_re.search(cobj.text): 2263 retval.append(cobj.re_sub(childspec, replacestr)) 2264 else: 2265 pass 2266 2267 if self.factory and atomic: 2268 # self.ConfigObjs._reassign_linenums() 2269 self.ConfigObjs._bootstrap_from_text() 2270 return retval 2271 2272 def replace_all_children( 2273 self, 2274 parentspec, 2275 childspec, 2276 replacestr, 2277 excludespec=None, 2278 exactmatch=False, 2279 atomic=False, 2280 ): 2281 """Replace lines matching `childspec` within all children (recursive) of lines whilch match `parentspec`""" 2282 retval = list() 2283 ## Since we are replacing text, we *must* operate on ConfigObjs 2284 childspec_re = re.compile(childspec) 2285 if excludespec: 2286 excludespec_re = re.compile(excludespec) 2287 for pobj in self._find_line_OBJ(parentspec, exactmatch=exactmatch): 2288 if excludespec and excludespec_re.search(pobj.text): 2289 # Exclude replacements on pobj lines which match excludespec 2290 continue 2291 for cobj in self._find_all_child_OBJ(pobj): 2292 if excludespec and excludespec_re.search(cobj.text): 2293 # Exclude replacements on pobj lines which match excludespec 2294 continue 2295 elif childspec_re.search(cobj.text): 2296 retval.append(cobj.re_sub(childspec, replacestr)) 2297 else: 2298 pass 2299 2300 if self.factory and atomic: 2301 # self.ConfigObjs._reassign_linenums() 2302 self.ConfigObjs._bootstrap_from_text() 2303 2304 return retval 2305 2306 def re_search_children(self, regex, recurse=False): 2307 """Use ``regex`` to search for root parents in the config with text matching regex. If `recurse` is False, only root parent objects are returned. A list of matching objects is returned. 2308 2309 This method is very similar to :func:`~ciscoconfparse.CiscoConfParse.find_objects` (when `recurse` is True); however it was written in response to the use-case described in `Github Issue #156 <https://github.com/mpenning/ciscoconfparse/issues/156>`_. 2310 2311 Parameters 2312 ---------- 2313 regex : str 2314 A string or python regular expression, which should be matched. 2315 recurse : bool 2316 Set True if you want to search all objects, and not just the root parents 2317 2318 Returns 2319 ------- 2320 list 2321 A list of matching :class:`~models_cisco.IOSCfgLine` objects which matched. If there is no match, an empty :py:func:`list` is returned. 2322 2323 """ 2324 ## I implemented this method in response to Github issue #156 2325 if recurse is False: 2326 # Only return the matching oldest ancestor objects... 2327 return [obj for obj in self.find_objects(regex) if (obj.parent is obj)] 2328 else: 2329 # Return any matching object 2330 return [obj for obj in self.find_objects(regex)] 2331 2332 def re_match_iter_typed( 2333 self, regex, group=1, result_type=str, default="", untyped_default=False 2334 ): 2335 r"""Use ``regex`` to search the root parents in the config 2336 and return the contents of the regular expression group, at the 2337 integer ``group`` index, cast as ``result_type``; if there is no 2338 match, ``default`` is returned. 2339 2340 Notes 2341 ----- 2342 Only the first regex match is returned. 2343 2344 Parameters 2345 ---------- 2346 regex : str 2347 A string or python compiled regular expression, which should be matched. This regular expression should contain parenthesis, which bound a match group. 2348 group : int 2349 An integer which specifies the desired regex group to be returned. ``group`` defaults to 1. 2350 result_type : type 2351 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``. 2352 default : any 2353 The default value to be returned, if there is no match. The default is an empty string. 2354 untyped_default : bool 2355 Set True if you don't want the default value to be typed 2356 2357 Returns 2358 ------- 2359 ``result_type`` 2360 The text matched by the regular expression group; if there is no match, ``default`` is returned. All values are cast as ``result_type``. The default result_type is `str`. 2361 2362 2363 Examples 2364 -------- 2365 This example illustrates how you can use 2366 :func:`~ciscoconfparse.re_match_iter_typed` to get the 2367 first interface name listed in the config. 2368 2369 >>> import re 2370 >>> from ciscoconfparse import CiscoConfParse 2371 >>> config = [ 2372 ... '!', 2373 ... 'interface Serial1/0', 2374 ... ' ip address 1.1.1.1 255.255.255.252', 2375 ... '!', 2376 ... 'interface Serial2/0', 2377 ... ' ip address 1.1.1.5 255.255.255.252', 2378 ... '!', 2379 ... ] 2380 >>> parse = CiscoConfParse(config) 2381 >>> parse.re_match_iter_typed(r'interface\s(\S+)') 2382 'Serial1/0' 2383 >>> 2384 2385 The following example retrieves the hostname from the configuration 2386 2387 >>> from ciscoconfparse import CiscoConfParse 2388 >>> config = [ 2389 ... '!', 2390 ... 'hostname DEN-EDGE-01', 2391 ... '!', 2392 ... 'interface Serial1/0', 2393 ... ' ip address 1.1.1.1 255.255.255.252', 2394 ... '!', 2395 ... 'interface Serial2/0', 2396 ... ' ip address 1.1.1.5 255.255.255.252', 2397 ... '!', 2398 ... ] 2399 >>> parse = CiscoConfParse(config) 2400 >>> parse.re_match_iter_typed(r'^hostname\s+(\S+)') 2401 'DEN-EDGE-01' 2402 >>> 2403 2404 """ 2405 ## iterate through root objects, and return the matching value 2406 ## (cast as result_type) from the first object.text that matches regex 2407 2408 # if (default is True): 2409 ## Not using self.re_match_iter_typed(default=True), because I want 2410 ## to be sure I build the correct API for match=False 2411 ## 2412 ## Ref IOSIntfLine.has_dtp for an example of how to code around 2413 ## this while I build the API 2414 # raise NotImplementedError 2415 2416 for cobj in self.ConfigObjs: 2417 2418 # Only process parent objects at the root of the tree... 2419 if cobj.parent is not cobj: 2420 continue 2421 2422 mm = re.search(regex, cobj.text) 2423 if (mm is not None): 2424 return result_type(mm.group(group)) 2425 ## Ref Github issue #121 2426 if untyped_default: 2427 return default 2428 else: 2429 return result_type(default) 2430 2431 def req_cfgspec_all_diff(self, cfgspec, ignore_ws=False): 2432 """ 2433 req_cfgspec_all_diff takes a list of required configuration lines, 2434 parses through the configuration, and ensures that none of cfgspec's 2435 lines are missing from the configuration. req_cfgspec_all_diff 2436 returns a list of missing lines from the config. 2437 2438 One example use of this method is when you need to enforce routing 2439 protocol standards, or standards against interface configurations. 2440 2441 Examples 2442 -------- 2443 2444 >>> from ciscoconfparse import CiscoConfParse 2445 >>> config = [ 2446 ... 'logging trap debugging', 2447 ... 'logging 172.28.26.15', 2448 ... ] 2449 >>> p = CiscoConfParse(config) 2450 >>> required_lines = [ 2451 ... "logging 172.28.26.15", 2452 ... "logging 172.16.1.5", 2453 ... ] 2454 >>> diffs = p.req_cfgspec_all_diff(required_lines) 2455 >>> diffs 2456 ['logging 172.16.1.5'] 2457 >>> 2458 """ 2459 2460 rgx = dict() 2461 if ignore_ws: 2462 for line in cfgspec: 2463 rgx[line] = self._build_space_tolerant_regex(line) 2464 2465 skip_cfgspec = dict() 2466 retval = list() 2467 matches = self._find_line_OBJ("[a-zA-Z]") 2468 ## Make a list of unnecessary cfgspec lines 2469 for lineobj in matches: 2470 for reqline in cfgspec: 2471 if ignore_ws: 2472 if re.search(r"^" + rgx[reqline] + "$", lineobj.text.strip()): 2473 skip_cfgspec[reqline] = True 2474 else: 2475 if lineobj.text.strip() == reqline.strip(): 2476 skip_cfgspec[reqline] = True 2477 ## Add items to be configured 2478 ## TODO: Find a way to add the parent of the missing lines 2479 for line in cfgspec: 2480 if not skip_cfgspec.get(line, False): 2481 retval.append(line) 2482 2483 return retval 2484 2485 def req_cfgspec_excl_diff(self, linespec, uncfgspec, cfgspec): 2486 r""" 2487 req_cfgspec_excl_diff accepts a linespec, an unconfig spec, and 2488 a list of required configuration elements. Return a list of 2489 configuration diffs to make the configuration comply. **All** other 2490 config lines matching the linespec that are *not* listed in the 2491 cfgspec will be removed with the uncfgspec regex. 2492 2493 Uses for this method include the need to enforce syslog, acl, or 2494 aaa standards. 2495 2496 Examples 2497 -------- 2498 2499 >>> from ciscoconfparse import CiscoConfParse 2500 >>> config = [ 2501 ... 'logging trap debugging', 2502 ... 'logging 172.28.26.15', 2503 ... ] 2504 >>> p = CiscoConfParse(config) 2505 >>> required_lines = [ 2506 ... "logging 172.16.1.5", 2507 ... "logging 1.10.20.30", 2508 ... "logging 192.168.1.1", 2509 ... ] 2510 >>> linespec = "logging\s+\d+\.\d+\.\d+\.\d+" 2511 >>> unconfspec = linespec 2512 >>> diffs = p.req_cfgspec_excl_diff(linespec, unconfspec, 2513 ... required_lines) 2514 >>> diffs 2515 ['no logging 172.28.26.15', 'logging 172.16.1.5', 'logging 1.10.20.30', 'logging 192.168.1.1'] 2516 >>> 2517 """ 2518 violate_objs = list() 2519 uncfg_objs = list() 2520 skip_cfgspec = dict() 2521 retval = list() 2522 matches = self._find_line_OBJ(linespec) 2523 ## Make a list of lineobject violations 2524 for lineobj in matches: 2525 # Look for config lines to unconfigure 2526 accept_lineobj = False 2527 for reqline in cfgspec: 2528 if lineobj.text.strip() == reqline.strip(): 2529 accept_lineobj = True 2530 skip_cfgspec[reqline] = True 2531 if accept_lineobj is False: 2532 # If a violation is found... 2533 violate_objs.append(lineobj) 2534 result = re.search(uncfgspec, lineobj.text) 2535 # add uncfgtext to the violator's lineobject 2536 lineobj.add_uncfgtext(result.group(0)) 2537 ## Make the list of unconfig objects, recurse through parents 2538 for vobj in violate_objs: 2539 parent_objs = vobj.all_parents 2540 for parent_obj in parent_objs: 2541 uncfg_objs.append(parent_obj) 2542 uncfg_objs.append(vobj) 2543 retval = self._objects_to_uncfg(uncfg_objs, violate_objs) 2544 ## Add missing lines... 2545 ## TODO: Find a way to add the parent of the missing lines 2546 for line in cfgspec: 2547 if not skip_cfgspec.get(line, False): 2548 retval.append(line) 2549 2550 return retval 2551 2552 def _sequence_nonparent_lines(self, a_nonparent_objs, b_nonparent_objs): 2553 """Assume a_nonparent_objs is the existing config sequence, and 2554 b_nonparent_objs is the *desired* config sequence 2555 2556 This method walks b_nonparent_objs, and orders a_nonparent_objs 2557 the same way (as much as possible) 2558 2559 This method returns: 2560 2561 - The reordered list of a_nonparent_objs 2562 - The reordered list of a_nonparent_lines 2563 - The reordered list of a_nonparent_linenums 2564 """ 2565 a_parse = CiscoConfParse([]) # A *new* parse for reordered a lines 2566 a_lines = list() 2567 a_linenums = list() 2568 2569 ## Mark all a objects as not done 2570 for aobj in a_nonparent_objs: 2571 aobj.done = False 2572 2573 for bobj in b_nonparent_objs: 2574 for aobj in a_nonparent_objs: 2575 if aobj.text == bobj.text: 2576 aobj.done = True 2577 a_parse.append_line(aobj.text) 2578 2579 # Add any missing a_parent_objs + their children... 2580 for aobj in a_nonparent_objs: 2581 if aobj.done is False: 2582 aobj.done = True 2583 a_parse.append_line(aobj.text) 2584 2585 a_parse.commit() 2586 2587 a_nonparents_reordered = a_parse.ConfigObjs 2588 for aobj in a_nonparents_reordered: 2589 a_lines.append(aobj.text) 2590 a_linenums.append(aobj.linenum) 2591 2592 return a_parse, a_lines, a_linenums 2593 2594 def _sequence_parent_lines(self, a_parent_objs, b_parent_objs): 2595 """Assume a_parent_objs is the existing config sequence, and 2596 b_parent_objs is the *desired* config sequence 2597 2598 This method walks b_parent_objs, and orders a_parent_objs 2599 the same way (as much as possible) 2600 2601 This method returns: 2602 2603 - The reordered list of a_parent_objs 2604 - The reordered list of a_parent_lines 2605 - The reordered list of a_parent_linenums 2606 """ 2607 a_parse = CiscoConfParse([]) # A *new* parse for reordered a lines 2608 a_lines = list() 2609 a_linenums = list() 2610 2611 ## Mark all a objects as not done 2612 for aobj in a_parent_objs: 2613 aobj.done = False 2614 for child in aobj.all_children: 2615 child.done = False 2616 2617 ## Walk the b objects by parent, then child and reorder a objects 2618 for bobj in b_parent_objs: 2619 2620 for aobj in a_parent_objs: 2621 if aobj.text == bobj.text: 2622 aobj.done = True 2623 a_parse.append_line(aobj.text) 2624 2625 # Append *matching* children to this aobj in the same order 2626 for bchild in bobj.all_children: 2627 for achild in aobj.all_children: 2628 if achild.done: 2629 continue 2630 elif achild.geneology_text == bchild.geneology_text: 2631 achild.done = True 2632 a_parse.append_line(achild.text) 2633 2634 # Append *missing* children to this aobj... 2635 for achild in aobj.all_children: 2636 if achild.done is False: 2637 achild.done = True 2638 a_parse.append_line(achild.text) 2639 2640 # Add any missing a_parent_objs + their children... 2641 for aobj in a_parent_objs: 2642 if aobj.done is False: 2643 aobj.done = True 2644 a_parse.append_line(aobj.text) 2645 for achild in aobj.all_children: 2646 achild.done = True 2647 a_parse.append_line(achild.text) 2648 2649 a_parse.commit() 2650 2651 a_parents_reordered = a_parse.ConfigObjs 2652 for aobj in a_parents_reordered: 2653 a_lines.append(aobj.text) 2654 a_linenums.append(aobj.linenum) 2655 2656 return a_parse, a_lines, a_linenums 2657 2658 def sync_diff( 2659 self, 2660 cfgspec, 2661 linespec, 2662 uncfgspec=None, 2663 ignore_order=True, 2664 remove_lines=True, 2665 debug=0, 2666 ): 2667 r""" 2668 ``sync_diff()`` accepts a list of required configuration elements, 2669 a linespec, and an unconfig spec. This method return a list of 2670 configuration diffs to make the configuration comply with cfgspec. 2671 2672 Parameters 2673 ---------- 2674 cfgspec : list 2675 A list of required configuration lines 2676 linespec : str 2677 A regular expression, which filters lines to be diff'd 2678 uncfgspec : str 2679 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. 2680 ignore_order : bool 2681 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) 2682 remove_lines : bool 2683 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. 2684 debug : int 2685 Miscellaneous debugging; Default: 0 2686 2687 Returns 2688 ------- 2689 list 2690 A list of string configuration diffs 2691 2692 2693 Uses for this method include the need to enforce syslog, acl, or 2694 aaa standards. 2695 2696 Examples 2697 -------- 2698 2699 >>> from ciscoconfparse import CiscoConfParse 2700 >>> config = [ 2701 ... 'logging trap debugging', 2702 ... 'logging 172.28.26.15', 2703 ... ] 2704 >>> p = CiscoConfParse(config) 2705 >>> required_lines = [ 2706 ... "logging 172.16.1.5", 2707 ... "logging 1.10.20.30", 2708 ... "logging 192.168.1.1", 2709 ... ] 2710 >>> linespec = "logging\s+\d+\.\d+\.\d+\.\d+" 2711 >>> unconfspec = linespec 2712 >>> diffs = p.sync_diff(required_lines, 2713 ... linespec, unconfspec) # doctest: +SKIP 2714 >>> diffs # doctest: +SKIP 2715 ['no logging 172.28.26.15', 'logging 172.16.1.5', 'logging 1.10.20.30', 'logging 192.168.1.1'] 2716 >>> 2717 """ 2718 2719 tmp = self._find_line_OBJ(linespec) 2720 if uncfgspec is None: 2721 uncfgspec = linespec 2722 a_lines = map(lambda x: x.text, tmp) 2723 a = CiscoConfParse(a_lines) 2724 2725 b = CiscoConfParse(cfgspec, factory=False) 2726 b_lines = b.ioscfg 2727 2728 a_hierarchy = list() 2729 b_hierarchy = list() 2730 2731 ## Build heirarchical, equal-length lists of parents / non-parents 2732 a_parents, a_nonparents = a.ConfigObjs.config_hierarchy() 2733 b_parents, b_nonparents = b.ConfigObjs.config_hierarchy() 2734 2735 obj = DiffObject(0, a_nonparents, a_parents) 2736 a_hierarchy.append(obj) 2737 2738 obj = DiffObject(0, b_nonparents, b_parents) 2739 b_hierarchy.append(obj) 2740 2741 retval = list() 2742 ## Assign config_this and unconfig_this attributes by "diff level" 2743 for adiff_level, bdiff_level in zip(a_hierarchy, b_hierarchy): 2744 for attr in ["parents", "nonparents"]: 2745 if attr == "parents": 2746 if ignore_order: 2747 a_parents = getattr(adiff_level, attr) 2748 b_parents = getattr(bdiff_level, attr) 2749 2750 # Rewrite a, since we reordered everything 2751 a, a_lines, a_linenums = self._sequence_parent_lines( 2752 a_parents, b_parents 2753 ) 2754 else: 2755 a_lines = list() 2756 a_linenums = list() 2757 for obj in adiff_level.parents: 2758 a_lines.append(obj.text) 2759 a_linenums.append(obj.linenum) 2760 a_lines.extend( 2761 map(lambda x: getattr(x, "text"), obj.all_children) 2762 ) 2763 a_linenums.extend( 2764 map(lambda x: getattr(x, "linenum"), obj.all_children) 2765 ) 2766 b_lines = list() 2767 b_linenums = list() 2768 for obj in bdiff_level.parents: 2769 b_lines.append(obj.text) 2770 b_linenums.append(obj.linenum) 2771 b_lines.extend( 2772 map(lambda x: getattr(x, "text"), obj.all_children) 2773 ) 2774 b_linenums.extend( 2775 map(lambda x: getattr(x, "linenum"), obj.all_children) 2776 ) 2777 else: 2778 if ignore_order: 2779 a_nonparents = getattr(adiff_level, attr) 2780 b_nonparents = getattr(bdiff_level, attr) 2781 2782 # Rewrite a, since we reordered everything 2783 a, a_lines, a_linenums = self._sequence_nonparent_lines( 2784 a_nonparents, b_nonparents 2785 ) 2786 else: 2787 a_lines = map( 2788 lambda x: getattr(x, "text"), getattr(adiff_level, attr) 2789 ) 2790 # Build a map from a_lines index to a.ConfigObjs index 2791 a_linenums = map( 2792 lambda x: getattr(x, "linenum"), getattr(adiff_level, attr) 2793 ) 2794 b_lines = map( 2795 lambda x: getattr(x, "text"), getattr(bdiff_level, attr) 2796 ) 2797 # Build a map from b_lines index to b.ConfigObjs index 2798 b_linenums = map( 2799 lambda x: getattr(x, "linenum"), getattr(bdiff_level, attr) 2800 ) 2801 2802 ### 2803 ### Mark diffs here 2804 ### 2805 2806 # Get a SequenceMatcher instance to calculate diffs at this level 2807 matcher = SequenceMatcher(isjunk=None, a=a_lines, b=b_lines) 2808 2809 # Use the SequenceMatcher instance to label objects appropriately: 2810 # - tag is the diff evaluation: equal, replace, insert, or delete 2811 # - i1 and i2 are the begin and end points for arg a 2812 # - j1 and j2 are the begin and end points for arg b 2813 for tag, i1, i2, j1, j2 in matcher.get_opcodes(): 2814 # print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % (tag, i1, i2, a_lines[i1:i2], j1, j2, b_lines[j1:j2])) 2815 if (debug > 0) or (self.debug > 0): 2816 logger.debug("TAG='{0}'".format(tag)) 2817 2818 # if tag=='equal', check whether the parent objs are the same 2819 # if parent objects are the same, then do nothing 2820 # if parent objects are different, then delete a & config b 2821 # if tag=='replace' 2822 # delete a & config b 2823 # if tag=='insert', then configure b 2824 aobjs = list() # List of a IOSCfgLine objects at this level 2825 bobjs = list() # List of b IOSCfgLine objects at this level 2826 for num in range(i1, i2): 2827 aobj = a.ConfigObjs[a_linenums[num]] 2828 aobjs.append(aobj) 2829 for num in range(j1, j2): 2830 bobj = b.ConfigObjs[b_linenums[num]] 2831 bobjs.append(bobj) 2832 2833 max_len = max(len(aobjs), len(bobjs)) 2834 for idx in range(0, max_len): 2835 try: 2836 aobj = aobjs[idx] 2837 # set aparent_text to all parents' text (joined) 2838 aparent_text = " ".join( 2839 map(lambda x: x.text, aobj.all_parents) 2840 ) 2841 except IndexError: 2842 # aobj doesn't exist, if we get an index error 2843 # fake some data... 2844 aobj = None 2845 aparent_text = "__ANOTHING__" 2846 if (debug > 0) or (self.debug > 0): 2847 logger.debug(" aobj:'{0}'".format(aobj)) 2848 logger.debug(" aobj parents:'{0}'".format(aparent_text)) 2849 2850 try: 2851 bobj = bobjs[idx] 2852 # set bparent_text to all parents' text (joined) 2853 bparent_text = " ".join( 2854 map(lambda x: x.text, bobj.all_parents) 2855 ) 2856 except IndexError: 2857 # bobj doesn't exist, if we get an index error 2858 # fake some data... 2859 bobj = None 2860 bparent_text = "__BNOTHING__" 2861 2862 if (debug > 0) or (self.debug > 0): 2863 logger.debug(" bobj:'{0}'".format(bobj)) 2864 logger.debug(" bobj parents:'{0}'".format(bparent_text)) 2865 2866 if tag == "equal": 2867 # If the diff claims that these lines are equal, they 2868 # aren't truly equal unless parents match 2869 if aparent_text != bparent_text: 2870 if (debug > 0) or (self.debug > 0): 2871 logger.debug( 2872 " tagged 'equal', aparent_text!=bparent_text" 2873 ) 2874 # a & b parents are *not* the same 2875 # therefore a & b are not equal 2876 if aobj: 2877 # Only configure parent if it's not already 2878 # slated for removal 2879 if not getattr(aobj.parent, "unconfig_this", False): 2880 aobj.parent.config_this = True 2881 aobj.unconfig_this = True 2882 if debug > 0: 2883 logger.debug(" unconfigure aobj") 2884 if bobj: 2885 bobj.config_this = True 2886 bobj.parent.config_this = True 2887 if debug > 0: 2888 logger.debug(" configure bobj") 2889 elif aparent_text == bparent_text: 2890 # Both a & b parents match, so these lines are equal 2891 aobj.unconfig_this = False 2892 bobj.config_this = False 2893 if debug > 0: 2894 logger.debug( 2895 " tagged 'equal', aparent_text==bparent_text" 2896 ) 2897 logger.debug(" do nothing with aobj / bobj") 2898 elif tag == "replace": 2899 # tag: replace, I'm not going to check parents for now 2900 if debug > 0: 2901 logger.debug(" tagged 'replace'") 2902 if aobj: 2903 # Only configure parent if it's not already 2904 # slated for removal 2905 if not getattr(aobj.parent, "unconfig_this", False): 2906 aobj.parent.config_this = True 2907 aobj.unconfig_this = True 2908 if debug > 0: 2909 logger.debug(" unconfigure aobj") 2910 if bobj: 2911 bobj.config_this = True 2912 bobj.parent.config_this = True 2913 if debug > 0: 2914 logger.debug(" configure bobj") 2915 elif tag == "insert": 2916 if debug > 0: 2917 logger.debug(" tagged 'insert'") 2918 # I don't think tag: insert ever applies to a objects... 2919 if aobj: 2920 # Only configure parent if it's not already 2921 # slated for removal 2922 if not getattr(aobj.parent, "unconfig_this", False): 2923 aobj.parent.config_this = True 2924 aobj.unconfig_this = True 2925 if debug > 0: 2926 logger.debug(" unconfigure aobj") 2927 # tag: insert certainly applies to b objects... 2928 if bobj: 2929 bobj.config_this = True 2930 bobj.parent.config_this = True 2931 if debug > 0: 2932 logger.debug(" configure bobj") 2933 elif tag == "delete": 2934 # NOTE: I'm not deleting b objects, for now 2935 if debug > 0: 2936 logger.debug(" tagged 'delete'") 2937 if aobj: 2938 # Only configure parent if it's not already 2939 # slated for removal 2940 for pobj in aobj.all_parents: 2941 if not getattr(pobj, "unconfig_this", False): 2942 pobj.config_this = True 2943 aobj.unconfig_this = True 2944 if debug > 0: 2945 logger.debug(" unconfigure aobj") 2946 else: 2947 error = "Unknown action: {0}".format(tag) 2948 logger.error(error) 2949 raise ValueError(error) 2950 2951 ### 2952 ### Write a object diffs here 2953 ### 2954 2955 ## Unconfigure A objects, at *each level*, as required 2956 for obj in a.ConfigObjs: 2957 if remove_lines and getattr(obj, "unconfig_this", False): 2958 ## FIXME: This should only be applied to IOS and ASA configs 2959 if uncfgspec: 2960 mm = re.search(uncfgspec, obj.text) 2961 if (mm is not None): 2962 obj.add_uncfgtext(mm.group(0)) 2963 retval.append(obj.uncfgtext) 2964 else: 2965 retval.append( 2966 " " * obj.indent + "no " + obj.text.lstrip() 2967 ) 2968 else: 2969 retval.append(" " * obj.indent + "no " + obj.text.lstrip()) 2970 elif remove_lines and getattr(obj, "config_this", False): 2971 retval.append(obj.text) 2972 2973 # Clean up the attributes we used temporarily in this method 2974 for attr in ["config_this", "unconfig_this"]: 2975 try: 2976 delattr(obj.text, attr) 2977 except: 2978 pass 2979 2980 ### 2981 ### Write b object diffs here 2982 ### 2983 for obj in b.ConfigObjs: 2984 if getattr(obj, "config_this", False): 2985 retval.append(obj.text) 2986 2987 # Clean up the attributes we used temporarily in this method 2988 try: 2989 delattr(obj.text, "config_this") 2990 except: 2991 pass 2992 2993 ## Strip out 'double negatives' (i.e. 'no no ') 2994 for idx in range(0, len(retval)): 2995 retval[idx] = re.sub( 2996 r"(\s+)no\s+no\s+(\S+.+?)$", r"\g<1>\g<2>", retval[idx] 2997 ) 2998 2999 if debug > 0: 3000 logger.debug("Completed diff:") 3001 for line in retval: 3002 logger.debug("'{0}'".format(line)) 3003 return retval 3004 3005 def save_as(self, filepath): 3006 """Save a text copy of the configuration at ``filepath``; this 3007 method uses the OperatingSystem's native line separators (such as 3008 ``\\r\\n`` in Windows).""" 3009 try: 3010 with open(filepath, "w") as newconf: 3011 for line in self.ioscfg: 3012 newconf.write(line + "\n") 3013 return True 3014 except Exception as e: 3015 logger.error(str(e)) 3016 raise e 3017 3018 ### The methods below are marked SEMI-PRIVATE because they return an object 3019 ### or iterable of objects instead of the configuration text itself. 3020 def _build_space_tolerant_regex(self, linespec): 3021 r"""SEMI-PRIVATE: Accept a string, and return a string with all 3022 spaces replaced with '\s+'""" 3023 3024 # Unicode below... 3025 backslash = "\x5c" 3026 # escaped_space = "\\s+" (not a raw string) 3027 if sys.version_info >= ( 3028 3, 3029 0, 3030 0, 3031 ): 3032 escaped_space = (backslash + backslash + "s+").translate("utf-8") 3033 else: 3034 escaped_space = backslash + backslash + "s+" 3035 3036 LINESPEC_LIST_TYPE = bool(getattr(linespec, "append", False)) 3037 3038 if not LINESPEC_LIST_TYPE: 3039 assert bool(getattr(linespec, "upper", False)) # Ensure it's a str 3040 linespec = re.sub(r"\s+", escaped_space, linespec) 3041 else: 3042 for idx in range(0, len(linespec)): 3043 ## Ensure this element is a string 3044 assert bool(getattr(linespec[idx], "upper", False)) 3045 linespec[idx] = re.sub(r"\s+", escaped_space, linespec[idx]) 3046 3047 return linespec 3048 3049 def _find_line_OBJ(self, linespec, exactmatch=False): 3050 """SEMI-PRIVATE: Find objects whose text matches the linespec""" 3051 ## NOTE TO SELF: do not remove _find_line_OBJ(); used by Cisco employees 3052 if not exactmatch: 3053 # Return objects whose text attribute matches linespec 3054 linespec_re = re.compile(linespec) 3055 elif exactmatch: 3056 # Return objects whose text attribute matches linespec exactly 3057 linespec_re = re.compile("^%s$" % linespec) 3058 return list(filter(lambda obj: linespec_re.search(obj.text), self.ConfigObjs)) 3059 3060 def _find_sibling_OBJ(self, lineobject): 3061 """SEMI-PRIVATE: Takes a singe object and returns a list of sibling 3062 objects""" 3063 siblings = lineobject.parent.children 3064 return siblings 3065 3066 def _find_all_child_OBJ(self, lineobject): 3067 """SEMI-PRIVATE: Takes a single object and returns a list of 3068 decendants in all 'children' / 'grandchildren' / etc... after it. 3069 It should NOT return the children of siblings""" 3070 # sort the list, and get unique objects 3071 retval = set(lineobject.children) 3072 for candidate in lineobject.children: 3073 if candidate.has_children: 3074 for child in candidate.children: 3075 retval.add(child) 3076 retval = sorted(retval) 3077 return retval 3078 3079 def _unique_OBJ(self, objectlist): 3080 """SEMI-PRIVATE: Returns a list of unique objects (i.e. with no 3081 duplicates). 3082 The returned value is sorted by configuration line number 3083 (lowest first)""" 3084 retval = set([]) 3085 for obj in objectlist: 3086 retval.add(obj) 3087 return sorted(retval) 3088 3089 def _objects_to_uncfg(self, objectlist, unconflist): 3090 # Used by req_cfgspec_excl_diff() 3091 retval = list() 3092 unconfdict = dict() 3093 for unconf in unconflist: 3094 unconfdict[unconf] = "DEFINED" 3095 for obj in self._unique_OBJ(objectlist): 3096 if unconfdict.get(obj, None) == "DEFINED": 3097 retval.append(obj.uncfgtext) 3098 else: 3099 retval.append(obj.text) 3100 return retval 3101 3102 3103#########################################################################3 3104 3105 3106class IOSConfigList(MutableSequence): 3107 """A custom list to hold :class:`~models_cisco.IOSCfgLine` objects. Most people will never need to use this class directly.""" 3108 3109 def __init__( 3110 self, 3111 data=None, 3112 comment_delimiter="!", 3113 debug=0, 3114 factory=False, 3115 ignore_blank_lines=True, 3116 syntax="ios", 3117 CiscoConfParse=None, 3118 ): 3119 """Initialize the class. 3120 3121 Parameters 3122 ---------- 3123 data : list 3124 A list of parsed :class:`~models_cisco.IOSCfgLine` objects 3125 comment_delimiter : str 3126 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 '!' 3127 debug : int 3128 ``debug`` defaults to 0, and should be kept that way unless you're working on a very tricky config parsing problem. Debug output is not particularly friendly 3129 ignore_blank_lines : bool 3130 ``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). 3131 3132 Returns 3133 ------- 3134 An instance of an :class:`~ciscoconfparse.IOSConfigList` object. 3135 3136 """ 3137 # data = kwargs.get('data', None) 3138 # comment_delimiter = kwargs.get('comment_delimiter', '!') 3139 # debug = kwargs.get('debug', False) 3140 # factory = kwargs.get('factory', False) 3141 # ignore_blank_lines = kwargs.get('ignore_blank_lines', True) 3142 # syntax = kwargs.get('syntax', 'ios') 3143 # CiscoConfParse = kwargs.get('CiscoConfParse', None) 3144 super(IOSConfigList, self).__init__() 3145 3146 self._list = list() 3147 self.CiscoConfParse = CiscoConfParse 3148 self.comment_delimiter = comment_delimiter 3149 self.factory = factory 3150 self.ignore_blank_lines = ignore_blank_lines 3151 self.syntax = syntax 3152 self.dna = "IOSConfigList" 3153 self.debug = debug 3154 3155 ## Support either a list or a generator instance 3156 if getattr(data, "__iter__", False): 3157 self._list = self._bootstrap_obj_init(data) 3158 else: 3159 self._list = list() 3160 3161 def __len__(self): 3162 return len(self._list) 3163 3164 def __getitem__(self, ii): 3165 return self._list[ii] 3166 3167 def __delitem__(self, ii): 3168 del self._list[ii] 3169 self._bootstrap_from_text() 3170 3171 def __setitem__(self, ii, val): 3172 return self._list[ii] 3173 3174 def __str__(self): 3175 return self.__repr__() 3176 3177 def __enter__(self): 3178 # Add support for with statements... 3179 # FIXME: *with* statements dont work 3180 for obj in self._list: 3181 yield obj 3182 3183 def __exit__(self, *args, **kwargs): 3184 # FIXME: *with* statements dont work 3185 self._list[0].confobj.CiscoConfParse.atomic() 3186 3187 def __repr__(self): 3188 return """<IOSConfigList, comment='%s', conf=%s>""" % ( 3189 self.comment_delimiter, 3190 self._list, 3191 ) 3192 3193 def _bootstrap_from_text(self): 3194 ## reparse all objects from their text attributes... this is *very* slow 3195 ## Ultimate goal: get rid of all reparsing from text... 3196 self._list = self._bootstrap_obj_init(list(map(attrgetter("text"), self._list))) 3197 if self.debug > 0: 3198 logger.debug("self._list = {0}".format(self._list)) 3199 3200 def has_line_with(self, linespec): 3201 return bool(filter(methodcaller("re_search", linespec), self._list)) 3202 3203 @junos_unsupported 3204 def insert_before(self, exist_val, new_val, atomic=False): 3205 """ 3206 Insert new_val before all occurances of exist_val. 3207 3208 Parameters 3209 ---------- 3210 exist_val : str 3211 An existing text value. This may match multiple configuration entries. 3212 new_val : str 3213 A new value to be inserted in the configuration. 3214 atomic : bool 3215 A boolean that controls whether the config is reparsed after the insertion (default False) 3216 3217 Returns 3218 ------- 3219 list 3220 An ios-style configuration list (indented by stop_width for each configuration level). 3221 3222 Examples 3223 -------- 3224 3225 >>> parse = CiscoConfParse(["a", "b", "c", "b"]) 3226 >>> # Insert 'g' before any occurance of 'b' 3227 >>> retval = parse.insert_before("b", "g") 3228 >>> parse.commit() 3229 >>> parse.ioscfg 3230 ... ["a", "g", "b", "c", "g", "b"] 3231 >>> 3232 """ 3233 3234 calling_fn_index = 1 3235 calling_filename = inspect.stack()[calling_fn_index].filename 3236 calling_function = inspect.stack()[calling_fn_index].function 3237 calling_lineno = inspect.stack()[calling_fn_index].lineno 3238 error = "FATAL CALL: in %s line %s %s(exist_val='%s', new_val='%s')" % (calling_filename, calling_lineno, calling_function, exist_val, new_val) 3239 # exist_val MUST be a string 3240 if isinstance(exist_val, str) is True: 3241 pass 3242 3243 elif isinstance(exist_val, IOSCfgLine) is True: 3244 exist_val = exist_val.text 3245 3246 else: 3247 raise ValueError(error) 3248 3249 # new_val MUST be a string 3250 if isinstance(new_val, str) is True: 3251 pass 3252 3253 elif isinstance(new_val, IOSCfgLine) is True: 3254 new_val = new_val.text 3255 3256 else: 3257 raise ValueError(error) 3258 3259 if self.factory: 3260 new_obj = ConfigLineFactory( 3261 text=new_val, 3262 comment_delimiter=self.comment_delimiter, 3263 syntax=self.syntax, 3264 ) 3265 elif self.syntax == "ios": 3266 new_obj = IOSCfgLine(text=new_val, comment_delimiter=self.comment_delimiter) 3267 3268 # Find all config lines which need to be modified... store in all_idx 3269 all_idx = [idx for idx, val in enumerate(self._list) if val.text==exist_val] 3270 for idx in sorted(all_idx, reverse=True): 3271 self._list.insert(idx, new_obj) 3272 3273 if atomic: 3274 # Reparse the whole config as a text list 3275 self._bootstrap_from_text() 3276 else: 3277 ## Just renumber lines... 3278 self._reassign_linenums() 3279 3280####################################################################new mod 3281 @junos_unsupported 3282 def insert_after(self, exist_val, new_val, atomic=False): 3283 """ 3284 Insert new_val after all occurances of exist_val. 3285 3286 Parameters 3287 ---------- 3288 exist_val : str 3289 An existing text value. This may match multiple configuration entries. 3290 new_val : str 3291 A new value to be inserted in the configuration. 3292 atomic : bool 3293 A boolean that controls whether the config is reparsed after the insertion (default False) 3294 3295 Returns 3296 ------- 3297 list 3298 An ios-style configuration list (indented by stop_width for each configuration level). 3299 3300 Examples 3301 -------- 3302 3303 >>> parse = CiscoConfParse(["a", "b", "c", "b"]) 3304 >>> # Insert 'g' after any occurance of 'b' 3305 >>> retval = parse.insert_after("b", "g") 3306 >>> parse.commit() 3307 >>> parse.ioscfg 3308 ... ["a", "b", "g", "c", "b", "g"] 3309 >>> 3310 """ 3311 3312 calling_fn_index = 1 3313 calling_filename = inspect.stack()[calling_fn_index].filename 3314 calling_function = inspect.stack()[calling_fn_index].function 3315 calling_lineno = inspect.stack()[calling_fn_index].lineno 3316 error = "FATAL CALL: in %s line %s %s(exist_val='%s', new_val='%s')" % (calling_filename, calling_lineno, calling_function, exist_val, new_val) 3317 # exist_val MUST be a string 3318 if isinstance(exist_val, str) is True: 3319 pass 3320 3321 elif isinstance(exist_val, IOSCfgLine) is True: 3322 exist_val = exist_val.text 3323 3324 else: 3325 raise ValueError(error) 3326 3327 # new_val MUST be a string 3328 if isinstance(new_val, str) is True: 3329 pass 3330 3331 elif isinstance(new_val, IOSCfgLine) is True: 3332 new_val = new_val.text 3333 3334 else: 3335 raise ValueError(error) 3336 3337 if self.factory: 3338 new_obj = ConfigLineFactory( 3339 text=new_val, 3340 comment_delimiter=self.comment_delimiter, 3341 syntax=self.syntax, 3342 ) 3343 elif self.syntax == "ios": 3344 new_obj = IOSCfgLine(text=new_val, comment_delimiter=self.comment_delimiter) 3345 3346 # Find all config lines which need to be modified... store in all_idx 3347 all_idx = [idx for idx, val in enumerate(self._list) if val.text==exist_val] 3348 for idx in sorted(all_idx, reverse=True): 3349 self._list.insert(idx+1, new_obj) 3350 3351 if atomic: 3352 # Reparse the whole config as a text list 3353 self._bootstrap_from_text() 3354 else: 3355 ## Just renumber lines... 3356 self._reassign_linenums() 3357####################################################################new mod 3358 3359# @junos_unsupported 3360# def insert_after(self, robj, val, atomic=False): 3361# ## Insert something after robj 3362# if getattr(robj, "capitalize", False): 3363# raise ValueError 3364# 3365# ## If val is a string... 3366# if getattr(val, "capitalize", False): 3367# if self.factory: 3368# obj = ConfigLineFactory( 3369# text=val, 3370# comment_delimiter=self.comment_delimiter, 3371# syntax=self.syntax, 3372# ) 3373# elif self.syntax == "ios": 3374# obj = IOSCfgLine(text=val, comment_delimiter=self.comment_delimiter) 3375# 3376# ## FIXME: This shouldn't be required 3377# ## Removed 2015-01-24 during rewrite... 3378# # self._reassign_linenums() 3379# 3380# ii = self._list.index(robj) 3381# if (ii is not None): 3382# ## Do insertion here 3383# self._list.insert(ii + 1, obj) 3384# 3385# if atomic: 3386# # Reparse the whole config as a text list 3387# self._bootstrap_from_text() 3388# else: 3389# ## Just renumber lines... 3390# self._reassign_linenums() 3391 3392 @junos_unsupported 3393 def insert(self, ii, val): 3394 if getattr(val, "capitalize", False): 3395 if self.factory: 3396 obj = ConfigLineFactory( 3397 text=val, 3398 comment_delimiter=self.comment_delimiter, 3399 syntax=self.syntax, 3400 ) 3401 elif self.syntax == "ios": 3402 obj = IOSCfgLine(text=val, comment_delimiter=self.comment_delimiter) 3403 else: 3404 error = 'insert() cannot insert "{0}"'.format(val) 3405 logger.error(error) 3406 raise ValueError(error) 3407 else: 3408 error = 'insert() cannot insert "{0}"'.format(val) 3409 logger.error(error) 3410 raise ValueError(error) 3411 3412 ## Insert something at index ii 3413 self._list.insert(ii, obj) 3414 3415 ## Just renumber lines... 3416 self._reassign_linenums() 3417 3418 @junos_unsupported 3419 def append(self, val): 3420 list_idx = len(self._list) 3421 self.insert(list_idx, val) 3422 3423 def config_hierarchy(self): 3424 """Walk this configuration and return the following tuple 3425 at each parent 'level': (list_of_parent_sibling_objs, list_of_nonparent_sibling_objs) 3426 3427 """ 3428 parent_siblings = list() 3429 nonparent_siblings = list() 3430 3431 for obj in self.CiscoConfParse.find_objects(r"^\S+"): 3432 if obj.is_comment: 3433 continue 3434 elif len(obj.children) == 0: 3435 nonparent_siblings.append(obj) 3436 else: 3437 parent_siblings.append(obj) 3438 3439 return parent_siblings, nonparent_siblings 3440 3441 def _banner_mark_regex(self, REGEX): 3442 # Build a list of all leading banner lines 3443 banner_objs = list(filter(lambda obj: REGEX.search(obj.text), self._list)) 3444 3445 BANNER_STR_RE = r"^(?:(?P<btype>(?:set\s+)*banner\s\w+\s+)(?P<bchar>\S))" 3446 for parent in banner_objs: 3447 parent.oldest_ancestor = True 3448 3449 ## Parse out the banner type and delimiting banner character 3450 mm = re.search(BANNER_STR_RE, parent.text) 3451 if (mm is not None): 3452 mm_results = mm.groupdict() 3453 (banner_lead, bannerdelimit) = ( 3454 mm_results["btype"].rstrip(), 3455 mm_results["bchar"], 3456 ) 3457 else: 3458 (banner_lead, bannerdelimit) = ("", None) 3459 3460 if self.debug > 0: 3461 logger.debug("banner_lead = '{0}'".format(banner_lead)) 3462 logger.debug("bannerdelimit = '{0}'".format(bannerdelimit)) 3463 logger.debug( 3464 "{0} starts at line {1}".format(banner_lead, parent.linenum) 3465 ) 3466 3467 idx = parent.linenum 3468 while not (bannerdelimit is None): 3469 ## Check whether the banner line has both begin and end delimter 3470 if idx == parent.linenum: 3471 parts = parent.text.split(bannerdelimit) 3472 if len(parts) > 2: 3473 ## banner has both begin and end delimiter on one line 3474 if self.debug > 0: 3475 logger.debug( 3476 "{0} ends at line" 3477 " {1}".format(banner_lead, parent.linenum) 3478 ) 3479 break 3480 3481 ## Use code below to identify children of the banner line 3482 idx += 1 3483 try: 3484 obj = self._list[idx] 3485 if obj.text is None: 3486 if self.debug > 0: 3487 logger.warning( 3488 "found empty text while parsing '{0}' in the banner".format( 3489 obj 3490 ) 3491 ) 3492 pass 3493 elif bannerdelimit in obj.text.strip(): 3494 if self.debug > 0: 3495 logger.debug( 3496 "{0} ends at line" 3497 " {1}".format(banner_lead, obj.linenum) 3498 ) 3499 parent.children.append(obj) 3500 parent.child_indent = 0 3501 obj.parent = parent 3502 break 3503 # Commenting the following lines out; fix Github issue #115 3504 # elif obj.is_comment and (obj.indent == 0): 3505 # break 3506 parent.children.append(obj) 3507 parent.child_indent = 0 3508 obj.parent = parent 3509 except IndexError: 3510 break 3511 3512 def _macro_mark_children(self, macro_parent_idx_list): 3513 # Mark macro children appropriately... 3514 for idx in macro_parent_idx_list: 3515 pobj = self._list[idx] 3516 pobj.child_indent = 0 3517 3518 # Walk the next configuration lines looking for the macro's children 3519 finished = False 3520 while not finished: 3521 idx += 1 3522 cobj = self._list[idx] 3523 cobj.parent = pobj 3524 pobj.children.append(cobj) 3525 # If we hit the end of the macro, break out of the loop 3526 if cobj.text.rstrip() == "@": 3527 finished = True 3528 3529 def _bootstrap_obj_init(self, text_list): 3530 """Accept a text list and format into proper IOSCfgLine() objects""" 3531 # Append text lines as IOSCfgLine objects... 3532 BANNER_STR = set( 3533 [ 3534 "login", 3535 "motd", 3536 "incoming", 3537 "exec", 3538 "telnet", 3539 "lcd", 3540 ] 3541 ) 3542 BANNER_ALL = [r"^(set\s+)*banner\s+{0}".format(ii) for ii in BANNER_STR] 3543 BANNER_ALL.append("aaa authentication fail-message") # Github issue #76 3544 BANNER_RE = re.compile("|".join(BANNER_ALL)) 3545 3546 retval = list() 3547 idx = 0 3548 3549 max_indent = 0 3550 macro_parent_idx_list = list() 3551 parents = dict() 3552 for line in text_list: 3553 # Reject empty lines if ignore_blank_lines... 3554 if self.ignore_blank_lines and line.strip() == "": 3555 continue 3556 # 3557 if not self.factory: 3558 obj = IOSCfgLine(line, self.comment_delimiter) 3559 elif self.syntax == "ios": 3560 obj = ConfigLineFactory(line, self.comment_delimiter, syntax="ios") 3561 else: 3562 error = ("Cannot classify config list item '%s' " 3563 "into a proper configuration object line" % line) 3564 if self.debug > 0: 3565 logger.error(error) 3566 raise ValueError(error) 3567 3568 obj.confobj = self 3569 obj.linenum = idx 3570 indent = len(line) - len(line.lstrip()) 3571 obj.indent = indent 3572 3573 is_config_line = obj.is_config_line 3574 3575 # list out macro parent line numbers... 3576 if obj.text[0:11] == "macro name ": 3577 macro_parent_idx_list.append(obj.linenum) 3578 3579 ## Parent cache: 3580 ## Maintain indent vs max_indent in a family and 3581 ## cache the parent until indent<max_indent 3582 if (indent < max_indent) and is_config_line: 3583 parent = None 3584 # walk parents and intelligently prune stale parents 3585 stale_parent_idxs = filter( 3586 lambda ii: ii >= indent, sorted(parents.keys(), reverse=True) 3587 ) 3588 for parent_idx in stale_parent_idxs: 3589 del parents[parent_idx] 3590 else: 3591 ## As long as the child indent hasn't gone backwards, 3592 ## we can use a cached parent 3593 parent = parents.get(indent, None) 3594 3595 ## If indented, walk backwards and find the parent... 3596 ## 1. Assign parent to the child 3597 ## 2. Assign child to the parent 3598 ## 3. Assign parent's child_indent 3599 ## 4. Maintain oldest_ancestor 3600 if (indent > 0) and not (parent is None): 3601 ## Add the line as a child (parent was cached) 3602 self._add_child_to_parent(retval, idx, indent, parent, obj) 3603 elif (indent > 0) and (parent is None): 3604 ## Walk backwards to find parent, and add the line as a child 3605 candidate_parent_index = idx - 1 3606 while candidate_parent_index >= 0: 3607 candidate_parent = retval[candidate_parent_index] 3608 if ( 3609 candidate_parent.indent < indent 3610 ) and candidate_parent.is_config_line: 3611 # We found the parent 3612 parent = candidate_parent 3613 parents[indent] = parent # Cache the parent 3614 if indent == 0: 3615 parent.oldest_ancestor = True 3616 break 3617 else: 3618 candidate_parent_index -= 1 3619 3620 ## Add the line as a child... 3621 self._add_child_to_parent(retval, idx, indent, parent, obj) 3622 3623 ## Handle max_indent 3624 if (indent == 0) and is_config_line: 3625 # only do this if it's a config line... 3626 max_indent = 0 3627 elif indent > max_indent: 3628 max_indent = indent 3629 3630 retval.append(obj) 3631 idx += 1 3632 3633 self._list = retval 3634 self._banner_mark_regex(BANNER_RE) 3635 # We need to use a different method for macros than banners because 3636 # macros don't specify a delimiter on their parent line, but 3637 # banners call out a delimiter. 3638 self._macro_mark_children(macro_parent_idx_list) # Process macros 3639 return retval 3640 3641 def _add_child_to_parent(self, _list, idx, indent, parentobj, childobj): 3642 ## parentobj could be None when trying to add a child that should not 3643 ## have a parent 3644 if parentobj is None: 3645 if self.debug > 0: 3646 logger.debug("parentobj is None") 3647 return 3648 3649 if self.debug > 0: 3650 # logger.debug("Adding child '{0}' to parent" 3651 # " '{1}'".format(childobj, parentobj)) 3652 # logger.debug("BEFORE parent.children - {0}" 3653 # .format(parentobj.children)) 3654 pass 3655 if childobj.is_comment and (_list[idx - 1].indent > indent): 3656 ## I *really* hate making this exception, but legacy 3657 ## ciscoconfparse never marked a comment as a child 3658 ## when the line immediately above it was indented more 3659 ## than the comment line 3660 pass 3661 elif childobj.parent is childobj: 3662 # Child has not been assigned yet 3663 parentobj.children.append(childobj) 3664 childobj.parent = parentobj 3665 childobj.parent.child_indent = indent 3666 else: 3667 pass 3668 3669 if self.debug > 0: 3670 # logger.debug(" AFTER parent.children - {0}" 3671 # .format(parentobj.children)) 3672 pass 3673 3674 def iter_with_comments(self, begin_index=0): 3675 for idx, obj in enumerate(self._list): 3676 if idx >= begin_index: 3677 yield obj 3678 3679 def iter_no_comments(self, begin_index=0): 3680 for idx, obj in enumerate(self._list): 3681 if (idx >= begin_index) and (not obj.is_comment): 3682 yield obj 3683 3684 def _reassign_linenums(self): 3685 # Call this after any insertion or deletion 3686 for idx, obj in enumerate(self._list): 3687 obj.linenum = idx 3688 3689 @property 3690 def all_parents(self): 3691 return [obj for obj in self._list if obj.has_children] 3692 3693 @property 3694 def last_index(self): 3695 return self.__len__() - 1 3696 3697 3698#########################################################################3 3699 3700 3701class NXOSConfigList(MutableSequence): 3702 """A custom list to hold :class:`~models_nxos.NXOSCfgLine` objects. Most people will never need to use this class directly.""" 3703 3704 def __init__( 3705 self, 3706 data=None, 3707 comment_delimiter="!", 3708 debug=0, 3709 factory=False, 3710 ignore_blank_lines=True, 3711 syntax="nxos", 3712 CiscoConfParse=None, 3713 ): 3714 """Initialize the class. 3715 3716 Parameters 3717 ---------- 3718 data : list 3719 A list of parsed :class:`~models_cisco.IOSCfgLine` objects 3720 comment_delimiter : str 3721 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 '!' 3722 debug : int 3723 ``debug`` defaults to 0, and should be kept that way unless you're working on a very tricky config parsing problem. Debug output is not particularly friendly 3724 ignore_blank_lines : bool 3725 ``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). 3726 3727 Returns 3728 ------- 3729 An instance of an :class:`~ciscoconfparse.NXOSConfigList` object. 3730 3731 """ 3732 # data = kwargs.get('data', None) 3733 # comment_delimiter = kwargs.get('comment_delimiter', '!') 3734 # debug = kwargs.get('debug', False) 3735 # factory = kwargs.get('factory', False) 3736 # ignore_blank_lines = kwargs.get('ignore_blank_lines', True) 3737 # syntax = kwargs.get('syntax', 'nxos') 3738 # CiscoConfParse = kwargs.get('CiscoConfParse', None) 3739 super(NXOSConfigList, self).__init__() 3740 3741 self._list = list() 3742 self.CiscoConfParse = CiscoConfParse 3743 self.comment_delimiter = comment_delimiter 3744 self.factory = factory 3745 self.ignore_blank_lines = ignore_blank_lines 3746 self.syntax = syntax 3747 self.dna = "NXOSConfigList" 3748 self.debug = debug 3749 3750 ## Support either a list or a generator instance 3751 if getattr(data, "__iter__", False): 3752 self._list = self._bootstrap_obj_init(data) 3753 else: 3754 self._list = list() 3755 3756 def __len__(self): 3757 return len(self._list) 3758 3759 def __getitem__(self, ii): 3760 return self._list[ii] 3761 3762 def __delitem__(self, ii): 3763 del self._list[ii] 3764 self._bootstrap_from_text() 3765 3766 def __setitem__(self, ii, val): 3767 return self._list[ii] 3768 3769 def __str__(self): 3770 return self.__repr__() 3771 3772 def __enter__(self): 3773 # Add support for with statements... 3774 # FIXME: *with* statements dont work 3775 for obj in self._list: 3776 yield obj 3777 3778 def __exit__(self, *args, **kwargs): 3779 # FIXME: *with* statements dont work 3780 self._list[0].confobj.CiscoConfParse.atomic() 3781 3782 def __repr__(self): 3783 return """<NXOSConfigList, comment='%s', conf=%s>""" % ( 3784 self.comment_delimiter, 3785 self._list, 3786 ) 3787 3788 def _bootstrap_from_text(self): 3789 ## reparse all objects from their text attributes... this is *very* slow 3790 ## Ultimate goal: get rid of all reparsing from text... 3791 self._list = self._bootstrap_obj_init(list(map(attrgetter("text"), self._list))) 3792 if self.debug > 0: 3793 logger.debug("self._list = {0}".format(self._list)) 3794 3795 def has_line_with(self, linespec): 3796 return bool(filter(methodcaller("re_search", linespec), self._list)) 3797 3798 def insert_before(self, robj, val, atomic=False): 3799 ## Insert something before robj 3800 if getattr(robj, "capitalize", False): 3801 # robj must not be a string... 3802 raise ValueError 3803 3804 if getattr(val, "capitalize", False): 3805 if self.factory: 3806 obj = ConfigLineFactory( 3807 text=val, 3808 comment_delimiter=self.comment_delimiter, 3809 syntax=self.syntax, 3810 ) 3811 elif self.syntax == "nxos": 3812 obj = NXOSCfgLine(text=val, comment_delimiter=self.comment_delimiter) 3813 3814 ii = self._list.index(robj) 3815 if (ii is not None): 3816 ## Do insertion here 3817 self._list.insert(ii, obj) 3818 3819 if atomic: 3820 # Reparse the whole config as a text list 3821 self._bootstrap_from_text() 3822 else: 3823 ## Just renumber lines... 3824 self._reassign_linenums() 3825 3826 def insert_after(self, robj, val, atomic=False): 3827 ## Insert something after robj 3828 if getattr(robj, "capitalize", False): 3829 raise ValueError 3830 3831 ## If val is a string... 3832 if getattr(val, "capitalize", False): 3833 if self.factory: 3834 obj = ConfigLineFactory( 3835 text=val, 3836 comment_delimiter=self.comment_delimiter, 3837 syntax=self.syntax, 3838 ) 3839 elif self.syntax == "nxos": 3840 obj = NXOSCfgLine(text=val, comment_delimiter=self.comment_delimiter) 3841 3842 ## FIXME: This shouldn't be required 3843 ## Removed 2015-01-24 during rewrite... 3844 # self._reassign_linenums() 3845 3846 ii = self._list.index(robj) 3847 if (ii is not None): 3848 ## Do insertion here 3849 self._list.insert(ii + 1, obj) 3850 3851 if atomic: 3852 # Reparse the whole config as a text list 3853 self._bootstrap_from_text() 3854 else: 3855 ## Just renumber lines... 3856 self._reassign_linenums() 3857 3858 def insert(self, ii, val): 3859 if getattr(val, "capitalize", False): 3860 if self.factory: 3861 obj = ConfigLineFactory( 3862 text=val, 3863 comment_delimiter=self.comment_delimiter, 3864 syntax=self.syntax, 3865 ) 3866 elif self.syntax == "nxos": 3867 obj = NXOSCfgLine(text=val, comment_delimiter=self.comment_delimiter) 3868 else: 3869 error = 'insert() cannot insert "{0}"'.format(val) 3870 logger.error(error) 3871 raise ValueError(error) 3872 else: 3873 error = 'insert() cannot insert "{0}"'.format(val) 3874 logger.error(error) 3875 raise ValueError(error) 3876 3877 ## Insert something at index ii 3878 self._list.insert(ii, obj) 3879 3880 ## Just renumber lines... 3881 self._reassign_linenums() 3882 3883 def append(self, val): 3884 list_idx = len(self._list) 3885 self.insert(list_idx, val) 3886 3887 def config_hierarchy(self): 3888 """Walk this configuration and return the following tuple 3889 at each parent 'level': 3890 (list_of_parent_sibling_objs, list_of_nonparent_sibling_objs) 3891 """ 3892 parent_siblings = list() 3893 nonparent_siblings = list() 3894 3895 for obj in self.CiscoConfParse.find_objects(r"^\S+"): 3896 if obj.is_comment: 3897 continue 3898 elif len(obj.children) == 0: 3899 nonparent_siblings.append(obj) 3900 else: 3901 parent_siblings.append(obj) 3902 3903 return parent_siblings, nonparent_siblings 3904 3905 def _banner_mark_regex(self, REGEX): 3906 # Build a list of all leading banner lines 3907 banner_objs = list(filter(lambda obj: REGEX.search(obj.text), self._list)) 3908 3909 BANNER_STR_RE = r"^(?:(?P<btype>(?:set\s+)*banner\s\w+\s+)(?P<bchar>\S))" 3910 for parent in banner_objs: 3911 parent.oldest_ancestor = True 3912 3913 ## Parse out the banner type and delimiting banner character 3914 mm = re.search(BANNER_STR_RE, parent.text) 3915 if (mm is not None): 3916 mm_results = mm.groupdict() 3917 (banner_lead, bannerdelimit) = ( 3918 mm_results["btype"].rstrip(), 3919 mm_results["bchar"], 3920 ) 3921 else: 3922 (banner_lead, bannerdelimit) = ("", None) 3923 3924 if self.debug > 0: 3925 logger.debug("banner_lead = '{0}'".format(banner_lead)) 3926 logger.debug("bannerdelimit = '{0}'".format(bannerdelimit)) 3927 logger.debug( 3928 "{0} starts at line {1}".format(banner_lead, parent.linenum) 3929 ) 3930 3931 idx = parent.linenum 3932 while not (bannerdelimit is None): 3933 ## Check whether the banner line has both begin and end delimter 3934 if idx == parent.linenum: 3935 parts = parent.text.split(bannerdelimit) 3936 if len(parts) > 2: 3937 ## banner has both begin and end delimiter on one line 3938 if self.debug > 0: 3939 logger.debug( 3940 "{0} ends at line" 3941 " {1}".format(banner_lead, parent.linenum) 3942 ) 3943 break 3944 3945 idx += 1 3946 try: 3947 obj = self._list[idx] 3948 if obj.text is None: 3949 if self.debug > 0: 3950 logger.warning( 3951 "found empty text while parsing '{0}' in the banner".format( 3952 obj 3953 ) 3954 ) 3955 pass 3956 elif bannerdelimit in obj.text.strip(): 3957 if self.debug > 0: 3958 logger.debug( 3959 "{0} ends at line" 3960 " {1}".format(banner_lead, obj.linenum) 3961 ) 3962 parent.children.append(obj) 3963 parent.child_indent = 0 3964 obj.parent = parent 3965 break 3966 3967 ## Fix Github issue #75 I don't think this case is reqd now 3968 # elif obj.is_comment and (obj.indent == 0): 3969 # break 3970 3971 parent.children.append(obj) 3972 parent.child_indent = 0 3973 obj.parent = parent 3974 except IndexError: 3975 break 3976 3977 def _bootstrap_obj_init(self, text_list): 3978 """Accept a text list and format into proper objects""" 3979 # Append text lines as NXOSCfgLine objects... 3980 BANNER_STR = set( 3981 [ 3982 "login", 3983 "motd", 3984 "incoming", 3985 "exec", 3986 "telnet", 3987 "lcd", 3988 ] 3989 ) 3990 BANNER_RE = re.compile( 3991 "|".join([r"^(set\s+)*banner\s+{0}".format(ii) for ii in BANNER_STR]) 3992 ) 3993 retval = list() 3994 idx = 0 3995 3996 max_indent = 0 3997 parents = dict() 3998 for line in text_list: 3999 # Reject empty lines if ignore_blank_lines... 4000 if self.ignore_blank_lines and line.strip() == "": 4001 continue 4002 # 4003 if not self.factory: 4004 obj = NXOSCfgLine(line, self.comment_delimiter) 4005 elif self.syntax == "nxos": 4006 obj = ConfigLineFactory(line, self.comment_delimiter, syntax="nxos") 4007 else: 4008 raise ValueError 4009 4010 obj.confobj = self 4011 obj.linenum = idx 4012 indent = len(line) - len(line.lstrip()) 4013 obj.indent = indent 4014 4015 is_config_line = obj.is_config_line 4016 4017 ## Parent cache: 4018 ## Maintain indent vs max_indent in a family and 4019 ## cache the parent until indent<max_indent 4020 if (indent < max_indent) and is_config_line: 4021 parent = None 4022 # walk parents and intelligently prune stale parents 4023 stale_parent_idxs = filter( 4024 lambda ii: ii >= indent, sorted(parents.keys(), reverse=True) 4025 ) 4026 for parent_idx in stale_parent_idxs: 4027 del parents[parent_idx] 4028 else: 4029 ## As long as the child indent hasn't gone backwards, 4030 ## we can use a cached parent 4031 parent = parents.get(indent, None) 4032 4033 ## If indented, walk backwards and find the parent... 4034 ## 1. Assign parent to the child 4035 ## 2. Assign child to the parent 4036 ## 3. Assign parent's child_indent 4037 ## 4. Maintain oldest_ancestor 4038 if (indent > 0) and not (parent is None): 4039 ## Add the line as a child (parent was cached) 4040 self._add_child_to_parent(retval, idx, indent, parent, obj) 4041 elif (indent > 0) and (parent is None): 4042 ## Walk backwards to find parent, and add the line as a child 4043 candidate_parent_index = idx - 1 4044 while candidate_parent_index >= 0: 4045 candidate_parent = retval[candidate_parent_index] 4046 if ( 4047 candidate_parent.indent < indent 4048 ) and candidate_parent.is_config_line: 4049 # We found the parent 4050 parent = candidate_parent 4051 parents[indent] = parent # Cache the parent 4052 if indent == 0: 4053 parent.oldest_ancestor = True 4054 break 4055 else: 4056 candidate_parent_index -= 1 4057 4058 ## Add the line as a child... 4059 self._add_child_to_parent(retval, idx, indent, parent, obj) 4060 4061 ## Handle max_indent 4062 if (indent == 0) and is_config_line: 4063 # only do this if it's a config line... 4064 max_indent = 0 4065 elif indent > max_indent: 4066 max_indent = indent 4067 4068 retval.append(obj) 4069 idx += 1 4070 4071 self._list = retval 4072 self._banner_mark_regex(BANNER_RE) # Process IOS banners 4073 return retval 4074 4075 def _add_child_to_parent(self, _list, idx, indent, parentobj, childobj): 4076 ## parentobj could be None when trying to add a child that should not 4077 ## have a parent 4078 if parentobj is None: 4079 if self.debug > 0: 4080 logger.debug("parentobj is None") 4081 return 4082 4083 if self.debug > 0: 4084 # logger.debug("Adding child '{0}' to parent" 4085 # " '{1}'".format(childobj, parentobj)) 4086 # logger.debug("BEFORE parent.children - {0}" 4087 # .format(parentobj.children)) 4088 pass 4089 if childobj.is_comment and (_list[idx - 1].indent > indent): 4090 ## I *really* hate making this exception, but legacy 4091 ## ciscoconfparse never marked a comment as a child 4092 ## when the line immediately above it was indented more 4093 ## than the comment line 4094 pass 4095 elif childobj.parent is childobj: 4096 # Child has not been assigned yet 4097 parentobj.children.append(childobj) 4098 childobj.parent = parentobj 4099 childobj.parent.child_indent = indent 4100 else: 4101 pass 4102 4103 if self.debug > 0: 4104 # logger.debug(" AFTER parent.children - {0}" 4105 # .format(parentobj.children)) 4106 pass 4107 4108 def iter_with_comments(self, begin_index=0): 4109 for idx, obj in enumerate(self._list): 4110 if idx >= begin_index: 4111 yield obj 4112 4113 def iter_no_comments(self, begin_index=0): 4114 for idx, obj in enumerate(self._list): 4115 if (idx >= begin_index) and (not obj.is_comment): 4116 yield obj 4117 4118 def _reassign_linenums(self): 4119 # Call this after any insertion or deletion 4120 for idx, obj in enumerate(self._list): 4121 obj.linenum = idx 4122 4123 @property 4124 def all_parents(self): 4125 return [obj for obj in self._list if obj.has_children] 4126 4127 @property 4128 def last_index(self): 4129 return self.__len__() - 1 4130 4131 4132class ASAConfigList(MutableSequence): 4133 """A custom list to hold :class:`~models_asa.ASACfgLine` objects. Most 4134 people will never need to use this class directly. 4135 4136 4137 """ 4138 4139 def __init__( 4140 self, 4141 data=None, 4142 comment_delimiter="!", 4143 debug=0, 4144 factory=False, 4145 ignore_blank_lines=True, 4146 syntax="asa", 4147 CiscoConfParse=None, 4148 ): 4149 """Initialize the class. 4150 4151 Parameters 4152 ---------- 4153 data : list 4154 A list of parsed :class:`~models_cisco.IOSCfgLine` objects 4155 comment_delimiter : str 4156 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 '!' 4157 debug : int 4158 ``debug`` defaults to 0, and should be kept that way unless you're working on a very tricky config parsing problem. Debug output is not particularly friendly 4159 ignore_blank_lines : bool 4160 ``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). 4161 4162 Returns 4163 ------- 4164 An instance of an :class:`~ciscoconfparse.ASAConfigList` object. 4165 4166 """ 4167 super(ASAConfigList, self).__init__() 4168 4169 self._list = list() 4170 self.CiscoConfParse = CiscoConfParse 4171 self.comment_delimiter = comment_delimiter 4172 self.factory = factory 4173 self.ignore_blank_lines = ignore_blank_lines 4174 self.syntax = syntax 4175 self.dna = "ASAConfigList" 4176 self.debug = debug 4177 4178 ## Support either a list or a generator instance 4179 if getattr(data, "__iter__", False): 4180 self._bootstrap_obj_init(data) 4181 else: 4182 self._list = list() 4183 4184 ### 4185 ### Internal structures 4186 self._RE_NAMES = re.compile(r"^\s*name\s+(\d+\.\d+\.\d+\.\d+)\s+(\S+)") 4187 self._RE_OBJNET = re.compile(r"^\s*object-group\s+network\s+(\S+)") 4188 self._RE_OBJSVC = re.compile(r"^\s*object-group\s+service\s+(\S+)") 4189 self._RE_OBJACL = re.compile(r"^\s*access-list\s+(\S+)") 4190 self._network_cache = dict() 4191 4192 def __len__(self): 4193 return len(self._list) 4194 4195 def __getitem__(self, ii): 4196 return self._list[ii] 4197 4198 def __delitem__(self, ii): 4199 del self._list[ii] 4200 self._bootstrap_from_text() 4201 4202 def __setitem__(self, ii, val): 4203 return self._list[ii] 4204 4205 def __str__(self): 4206 return self.__repr__() 4207 4208 def __enter__(self): 4209 # Add support for with statements... 4210 # FIXME: *with* statements dont work 4211 for obj in self._list: 4212 yield obj 4213 4214 def __exit__(self, *args, **kwargs): 4215 # FIXME: *with* statements dont work 4216 self._list[0].confobj.CiscoConfParse.atomic() 4217 4218 def __repr__(self): 4219 return """<ASAConfigList, comment='%s', conf=%s>""" % ( 4220 self.comment_delimiter, 4221 self._list, 4222 ) 4223 4224 def _bootstrap_from_text(self): 4225 ## reparse all objects from their text attributes... this is *very* slow 4226 ## Ultimate goal: get rid of all reparsing from text... 4227 self._list = self._bootstrap_obj_init(list(map(attrgetter("text"), self._list))) 4228 4229 def has_line_with(self, linespec): 4230 return bool(filter(methodcaller("re_search", linespec), self._list)) 4231 4232 def insert_before(self, robj, val, atomic=False): 4233 ## Insert something before robj 4234 if getattr(robj, "capitalize", False): 4235 raise ValueError 4236 4237 if getattr(val, "capitalize", False): 4238 if self.factory: 4239 obj = ConfigLineFactory( 4240 text=val, 4241 comment_delimiter=self.comment_delimiter, 4242 syntax=self.syntax, 4243 ) 4244 elif self.syntax == "asa": 4245 obj = ASACfgLine(text=val, comment_delimiter=self.comment_delimiter) 4246 4247 ii = self._list.index(robj) 4248 if (ii is not None): 4249 ## Do insertion here 4250 self._list.insert(ii, obj) 4251 4252 if atomic: 4253 # Reparse the whole config as a text list 4254 self._bootstrap_from_text() 4255 else: 4256 ## Just renumber lines... 4257 self._reassign_linenums() 4258 4259 def insert_after(self, robj, val, atomic=False): 4260 ## Insert something after robj 4261 if getattr(robj, "capitalize", False): 4262 raise ValueError 4263 4264 if getattr(val, "capitalize", False): 4265 if self.factory: 4266 obj = ConfigLineFactory( 4267 text=val, 4268 comment_delimiter=self.comment_delimiter, 4269 syntax=self.syntax, 4270 ) 4271 elif self.syntax == "asa": 4272 obj = ASACfgLine(text=val, comment_delimiter=self.comment_delimiter) 4273 4274 ## FIXME: This shouldn't be required 4275 self._reassign_linenums() 4276 4277 ii = self._list.index(robj) 4278 if (ii is not None): 4279 ## Do insertion here 4280 self._list.insert(ii + 1, obj) 4281 4282 if atomic: 4283 # Reparse the whole config as a text list 4284 self._bootstrap_from_text() 4285 else: 4286 ## Just renumber lines... 4287 self._reassign_linenums() 4288 4289 def insert(self, ii, val): 4290 ## Insert something at index ii 4291 if getattr(val, "capitalize", False): 4292 if self.factory: 4293 obj = ConfigLineFactory( 4294 text=val, 4295 comment_delimiter=self.comment_delimiter, 4296 syntax=self.syntax, 4297 ) 4298 elif self.syntax == "asa": 4299 obj = ASACfgLine(text=val, comment_delimiter=self.comment_delimiter) 4300 4301 self._list.insert(ii, obj) 4302 4303 ## Just renumber lines... 4304 self._reassign_linenums() 4305 4306 def append(self, val, atomic=False): 4307 list_idx = len(self._list) 4308 self.insert(list_idx, val) 4309 4310 def config_hierarchy(self): 4311 """Walk this configuration and return the following tuple 4312 at each parent 'level': 4313 (list_of_parent_siblings, list_of_nonparent_siblings)""" 4314 parent_siblings = list() 4315 nonparent_siblings = list() 4316 4317 for obj in self.CiscoConfParse.find_objects(r"^\S+"): 4318 if obj.is_comment: 4319 continue 4320 elif len(obj.children) == 0: 4321 nonparent_siblings.append(obj) 4322 else: 4323 parent_siblings.append(obj) 4324 4325 return parent_siblings, nonparent_siblings 4326 4327 def _bootstrap_obj_init(self, text_list): 4328 """Accept a text list and format into proper objects""" 4329 # Append text lines as IOSCfgLine objects... 4330 retval = list() 4331 idx = 0 4332 4333 max_indent = 0 4334 parents = dict() 4335 for line in text_list: 4336 # Reject empty lines if ignore_blank_lines... 4337 if self.ignore_blank_lines and line.strip() == "": 4338 continue 4339 4340 if self.syntax == "asa" and self.factory: 4341 obj = ConfigLineFactory(line, self.comment_delimiter, syntax="asa") 4342 elif self.syntax == "asa" and not self.factory: 4343 obj = ASACfgLine(text=line, comment_delimiter=self.comment_delimiter) 4344 else: 4345 raise ValueError 4346 4347 obj.confobj = self 4348 obj.linenum = idx 4349 indent = len(line) - len(line.lstrip()) 4350 obj.indent = indent 4351 4352 is_config_line = obj.is_config_line 4353 4354 ## Parent cache: 4355 ## Maintain indent vs max_indent in a family and 4356 ## cache the parent until indent<max_indent 4357 if (indent < max_indent) and is_config_line: 4358 parent = None 4359 # walk parents and intelligently prune stale parents 4360 stale_parent_idxs = filter( 4361 lambda ii: ii >= indent, sorted(parents.keys(), reverse=True) 4362 ) 4363 for parent_idx in stale_parent_idxs: 4364 del parents[parent_idx] 4365 else: 4366 ## As long as the child indent hasn't gone backwards, 4367 ## we can use a cached parent 4368 parent = parents.get(indent, None) 4369 4370 ## If indented, walk backwards and find the parent... 4371 ## 1. Assign parent to the child 4372 ## 2. Assign child to the parent 4373 ## 3. Assign parent's child_indent 4374 ## 4. Maintain oldest_ancestor 4375 if (indent > 0) and not (parent is None): 4376 ## Add the line as a child (parent was cached) 4377 self._add_child_to_parent(retval, idx, indent, parent, obj) 4378 elif (indent > 0) and (parent is None): 4379 ## Walk backwards to find parent, and add the line as a child 4380 candidate_parent_index = idx - 1 4381 while candidate_parent_index >= 0: 4382 candidate_parent = retval[candidate_parent_index] 4383 if ( 4384 candidate_parent.indent < indent 4385 ) and candidate_parent.is_config_line: 4386 # We found the parent 4387 parent = candidate_parent 4388 parents[indent] = parent # Cache the parent 4389 if indent == 0: 4390 parent.oldest_ancestor = True 4391 break 4392 else: 4393 candidate_parent_index -= 1 4394 4395 ## Add the line as a child... 4396 self._add_child_to_parent(retval, idx, indent, parent, obj) 4397 4398 ## Handle max_indent 4399 if (indent == 0) and is_config_line: 4400 # only do this if it's a config line... 4401 max_indent = 0 4402 elif indent > max_indent: 4403 max_indent = indent 4404 4405 retval.append(obj) 4406 idx += 1 4407 4408 self._list = retval 4409 ## Insert ASA-specific banner processing here, if required 4410 return retval 4411 4412 def _add_child_to_parent(self, _list, idx, indent, parentobj, childobj): 4413 ## parentobj could be None when trying to add a child that should not 4414 ## have a parent 4415 if parentobj is None: 4416 if self.debug > 0: 4417 logger.debug("parentobj is None") 4418 return 4419 4420 if self.debug > 0: 4421 logger.debug( 4422 "Adding child '{0}' to parent" " '{1}'".format(childobj, parentobj) 4423 ) 4424 logger.debug(" BEFORE parent.children - {0}".format(parentobj.children)) 4425 if childobj.is_comment and (_list[idx - 1].indent > indent): 4426 ## I *really* hate making this exception, but legacy 4427 ## ciscoconfparse never marked a comment as a child 4428 ## when the line immediately above it was indented more 4429 ## than the comment line 4430 pass 4431 elif childobj.parent is childobj: 4432 # Child has not been assigned yet 4433 parentobj.children.append(childobj) 4434 childobj.parent = parentobj 4435 childobj.parent.child_indent = indent 4436 else: 4437 pass 4438 4439 if self.debug > 0: 4440 logger.debug(" AFTER parent.children - {0}".format(parentobj.children)) 4441 4442 def iter_with_comments(self, begin_index=0): 4443 for idx, obj in enumerate(self._list): 4444 if idx >= begin_index: 4445 yield obj 4446 4447 def iter_no_comments(self, begin_index=0): 4448 for idx, obj in enumerate(self._list): 4449 if (idx >= begin_index) and (not obj.is_comment): 4450 yield obj 4451 4452 def _reassign_linenums(self): 4453 # Call this after any insertion or deletion 4454 for idx, obj in enumerate(self._list): 4455 obj.linenum = idx 4456 4457 @property 4458 def all_parents(self): 4459 return [obj for obj in self._list if obj.has_children] 4460 4461 @property 4462 def last_index(self): 4463 return self.__len__() - 1 4464 4465 ### 4466 ### ASA-specific stuff here... 4467 ### 4468 @property 4469 def names(self): 4470 """Return a dictionary of name to address mappings""" 4471 retval = dict() 4472 name_rgx = self._RE_NAMES 4473 for obj in self.CiscoConfParse.find_objects(name_rgx): 4474 addr = obj.re_match_typed(name_rgx, group=1, result_type=str) 4475 name = obj.re_match_typed(name_rgx, group=2, result_type=str) 4476 retval[name] = addr 4477 return retval 4478 4479 @property 4480 def object_group_network(self): 4481 """Return a dictionary of name to object-group network mappings""" 4482 retval = dict() 4483 obj_rgx = self._RE_OBJNET 4484 for obj in self.CiscoConfParse.find_objects(obj_rgx): 4485 name = obj.re_match_typed(obj_rgx, group=1, result_type=str) 4486 retval[name] = obj 4487 return retval 4488 4489 @property 4490 def access_list(self): 4491 """Return a dictionary of ACL name to ACE (list) mappings""" 4492 retval = dict() 4493 for obj in self.CiscoConfParse.find_objects(self._RE_OBJACL): 4494 name = obj.re_match_typed(self._RE_OBJACL, group=1, result_type=str) 4495 tmp = retval.get(name, []) 4496 tmp.append(obj) 4497 retval[name] = tmp 4498 return retval 4499 4500 4501class DiffObject(object): 4502 """This object should be used at every level of hierarchy""" 4503 4504 def __init__(self, level, nonparents, parents): 4505 self.level = level 4506 self.nonparents = nonparents 4507 self.parents = parents 4508 4509 def __repr__(self): 4510 return "<DiffObject level: {0}>".format(self.level) 4511 4512 4513class CiscoPassword(object): 4514 def __init__(self, ep=""): 4515 self.ep = ep 4516 4517 def decrypt(self, ep=""): 4518 """Cisco Type 7 password decryption. Converted from perl code that was 4519 written by jbash [~at~] cisco.com; enhancements suggested by 4520 rucjain [~at~] cisco.com""" 4521 4522 xlat = ( 4523 0x64, 4524 0x73, 4525 0x66, 4526 0x64, 4527 0x3B, 4528 0x6B, 4529 0x66, 4530 0x6F, 4531 0x41, 4532 0x2C, 4533 0x2E, 4534 0x69, 4535 0x79, 4536 0x65, 4537 0x77, 4538 0x72, 4539 0x6B, 4540 0x6C, 4541 0x64, 4542 0x4A, 4543 0x4B, 4544 0x44, 4545 0x48, 4546 0x53, 4547 0x55, 4548 0x42, 4549 0x73, 4550 0x67, 4551 0x76, 4552 0x63, 4553 0x61, 4554 0x36, 4555 0x39, 4556 0x38, 4557 0x33, 4558 0x34, 4559 0x6E, 4560 0x63, 4561 0x78, 4562 0x76, 4563 0x39, 4564 0x38, 4565 0x37, 4566 0x33, 4567 0x32, 4568 0x35, 4569 0x34, 4570 0x6B, 4571 0x3B, 4572 0x66, 4573 0x67, 4574 0x38, 4575 0x37, 4576 ) 4577 4578 dp = "" 4579 regex = re.compile("^(..)(.+)") 4580 ep = ep or self.ep 4581 if not (len(ep) & 1): 4582 result = regex.search(ep) 4583 try: 4584 s, e = int(result.group(1)), result.group(2) 4585 except ValueError: 4586 # typically get a ValueError for int( result.group(1))) because 4587 # the method was called with an unencrypted password. For now 4588 # SILENTLY bypass the error 4589 s, e = (0, "") 4590 for ii in range(0, len(e), 2): 4591 # int( blah, 16) assumes blah is base16... cool 4592 magic = int(re.search(".{%s}(..)" % ii, e).group(1), 16) 4593 # Wrap around after 53 chars... 4594 newchar = "%c" % (magic ^ int(xlat[int(s % 53)])) 4595 dp = dp + str(newchar) 4596 s = s + 1 4597 # if s > 53: 4598 # logger.warning("password decryption failed.") 4599 return dp 4600 4601 4602def ConfigLineFactory(text="", comment_delimiter="!", syntax="ios"): 4603 # Complicted & Buggy 4604 # classes = [j for (i,j) in globals().iteritems() if isinstance(j, TypeType) and issubclass(j, BaseCfgLine)] 4605 4606 ## Manual and simple 4607 if syntax == "ios": 4608 classes = [ 4609 IOSIntfLine, 4610 IOSRouteLine, 4611 IOSAccessLine, 4612 IOSAaaLoginAuthenticationLine, 4613 IOSAaaEnableAuthenticationLine, 4614 IOSAaaCommandsAuthorizationLine, 4615 IOSAaaCommandsAccountingLine, 4616 IOSAaaExecAccountingLine, 4617 IOSAaaGroupServerLine, 4618 IOSHostnameLine, 4619 IOSIntfGlobal, 4620 IOSCfgLine, 4621 ] # This is simple 4622 elif syntax == "nxos": 4623 classes = [ 4624 NXOSIntfLine, 4625 NXOSRouteLine, 4626 NXOSAccessLine, 4627 NXOSAaaLoginAuthenticationLine, 4628 NXOSAaaEnableAuthenticationLine, 4629 NXOSAaaCommandsAuthorizationLine, 4630 NXOSAaaCommandsAccountingLine, 4631 NXOSAaaExecAccountingLine, 4632 NXOSAaaGroupServerLine, 4633 NXOSvPCLine, 4634 NXOSHostnameLine, 4635 NXOSIntfGlobal, 4636 NXOSCfgLine, 4637 ] # This is simple 4638 elif syntax == "asa": 4639 classes = [ 4640 ASAName, 4641 ASAObjNetwork, 4642 ASAObjService, 4643 ASAObjGroupNetwork, 4644 ASAObjGroupService, 4645 ASAIntfLine, 4646 ASAIntfGlobal, 4647 ASAHostnameLine, 4648 ASAAclLine, 4649 ASACfgLine, 4650 ] 4651 elif syntax == "junos": 4652 classes = [IOSCfgLine] 4653 else: 4654 error = "'{0}' is an unknown syntax".format(syntax) 4655 logger.error(error) 4656 raise ValueError("'{0}' is an unknown syntax".format(syntax)) 4657 4658 for cls in classes: 4659 if cls.is_object_for(text): 4660 inst = cls( 4661 text=text, comment_delimiter=comment_delimiter 4662 ) # instance of the proper subclass 4663 return inst 4664 error = "Could not find an object for '%s'" % line 4665 logger.error(error) 4666 raise ValueError(error) 4667 4668 4669### TODO: Add unit tests below 4670if __name__ == "__main__": 4671 import optparse 4672 4673 pp = optparse.OptionParser() 4674 pp.add_option( 4675 "-c", dest="config", help="Config file to be parsed", metavar="FILENAME" 4676 ) 4677 pp.add_option("-m", dest="method", help="Command for parsing", metavar="METHOD") 4678 pp.add_option("--a1", dest="arg1", help="Command's first argument", metavar="ARG") 4679 pp.add_option("--a2", dest="arg2", help="Command's second argument", metavar="ARG") 4680 pp.add_option("--a3", dest="arg3", help="Command's third argument", metavar="ARG") 4681 (opts, args) = pp.parse_args() 4682 4683 if opts.method == "find_lines": 4684 diff = CiscoConfParse(opts.config).find_lines(opts.arg1) 4685 elif opts.method == "find_children": 4686 diff = CiscoConfParse(opts.config).find_children(opts.arg1) 4687 elif opts.method == "find_all_children": 4688 diff = CiscoConfParse(opts.config).find_all_children(opts.arg1) 4689 elif opts.method == "find_blocks": 4690 diff = CiscoConfParse(opts.config).find_blocks(opts.arg1) 4691 elif opts.method == "find_parents_w_child": 4692 diff = CiscoConfParse(opts.config).find_parents_w_child(opts.arg1, opts.arg2) 4693 elif opts.method == "find_parents_wo_child": 4694 diff = CiscoConfParse(opts.config).find_parents_wo_child(opts.arg1, opts.arg2) 4695 elif opts.method == "req_cfgspec_excl_diff": 4696 diff = CiscoConfParse(opts.config).req_cfgspec_excl_diff( 4697 opts.arg1, opts.arg2, opts.arg3.split(",") 4698 ) 4699 elif opts.method == "req_cfgspec_all_diff": 4700 diff = CiscoConfParse(opts.config).req_cfgspec_all_diff(opts.arg1.split(",")) 4701 elif opts.method == "decrypt": 4702 pp = CiscoPassword() 4703 print(pp.decrypt(opts.arg1)) 4704 exit(1) 4705 elif opts.method == "help": 4706 print("Valid methods and their arguments:") 4707 print(" find_lines: arg1=linespec") 4708 print(" find_children: arg1=linespec") 4709 print(" find_all_children: arg1=linespec") 4710 print(" find_blocks: arg1=linespec") 4711 print(" find_parents_w_child: arg1=parentspec arg2=childspec") 4712 print(" find_parents_wo_child: arg1=parentspec arg2=childspec") 4713 print( 4714 " req_cfgspec_excl_diff: arg1=linespec arg2=uncfgspec" 4715 + " arg3=cfgspec" 4716 ) 4717 print(" req_cfgspec_all_diff: arg1=cfgspec") 4718 print(" decrypt: arg1=encrypted_passwd") 4719 exit(1) 4720 else: 4721 import doctest 4722 4723 doctest.testmod() 4724 exit(0) 4725 4726 if len(diff) > 0: 4727 for line in diff: 4728 print(line) 4729 else: 4730 error = "ciscoconfparse was called with unknown parameters" 4731 logger.error(error) 4732 raise RuntimeError(error) 4733