1from __future__ import absolute_import 2import sys 3import re 4import os 5 6from ciscoconfparse.ccp_abc import BaseCfgLine 7from ciscoconfparse.ccp_util import IPv4Obj 8 9### HUGE UGLY WARNING: 10### Anything in models_junos.py could change at any time, until I remove this 11### warning. I have good reason to believe that these methods are stable and 12### function correctly, but I've been wrong before. There are no unit tests 13### for this functionality yet, so I consider all this code alpha quality. 14### 15### Use models_junos.py at your own risk. You have been warned :-) 16 17""" models_junos.py - Parse, Query, Build, and Modify Junos-style configurations 18 19 Copyright (C) 2020-2021 David Michael Pennington at Cisco Systems 20 Copyright (C) 2019 David Michael Pennington at ThousandEyes 21 Copyright (C) 2015-2019 David Michael Pennington at Samsung Data Services 22 23 This program is free software: you can redistribute it and/or modify 24 it under the terms of the GNU General Public License as published by 25 the Free Software Foundation, either version 3 of the License, or 26 (at your option) any later version. 27 28 This program is distributed in the hope that it will be useful, 29 but WITHOUT ANY WARRANTY; without even the implied warranty of 30 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 31 GNU General Public License for more details. 32 33 You should have received a copy of the GNU General Public License 34 along with this program. If not, see <http://www.gnu.org/licenses/>. 35 36 If you need to contact the author, you can do so by emailing: 37 mike [~at~] pennington [/dot\] net 38""" 39 40## 41##------------- Junos Configuration line object 42## 43 44 45class JunosCfgLine(BaseCfgLine): 46 """An object for a parsed Junos-style configuration line. 47 :class:`~models_junos.JunosCfgLine` objects contain references to other 48 parent and child :class:`~models_junos.JunosCfgLine` objects. 49 50 Notes 51 ----- 52 Originally, :class:`~models_junos.JunosCfgLine` objects were only 53 intended for advanced ciscoconfparse users. As of ciscoconfparse 54 version 0.9.10, *all users* are strongly encouraged to prefer the 55 methods directly on :class:`~models_junos.JunosCfgLine` objects. 56 Ultimately, if you write scripts which call methods on 57 :class:`~models_junos.JunosCfgLine` objects, your scripts will be much 58 more efficient than if you stick strictly to the classic 59 :class:`~ciscoconfparse.CiscoConfParse` methods. 60 61 Parameters 62 ---------- 63 text : str 64 A string containing a text copy of the Junos configuration line. :class:`~ciscoconfparse.CiscoConfParse` will automatically identify the parent and children (if any) when it parses the configuration. 65 comment_delimiter : str 66 A string which is considered a comment for the configuration format. Since this is for Cisco Junos-style configurations, it defaults to ``!``. 67 68 Attributes 69 ---------- 70 text : str 71 A string containing the parsed Junos configuration statement 72 linenum : int 73 The line number of this configuration statement in the original config; default is -1 when first initialized. 74 parent : :class:`~models_junos.JunosCfgLine()` 75 The parent of this object; defaults to ``self``. 76 children : list 77 A list of ``JunosCfgLine()`` objects which are children of this object. 78 child_indent : int 79 An integer with the indentation of this object's children 80 indent : int 81 An integer with the indentation of this object's ``text`` oldest_ancestor (bool): A boolean indicating whether this is the oldest ancestor in a family 82 is_comment : bool 83 A boolean indicating whether this is a comment 84 85 Returns 86 ------- 87 :class:`~models_junos.JunosCfgLine` 88 89 """ 90 91 def __init__(self, *args, **kwargs): 92 """Accept an Junos line number and initialize family relationship 93 attributes""" 94 super(JunosCfgLine, self).__init__(*args, **kwargs) 95 96 @classmethod 97 def is_object_for(cls, line="", re=re): 98 ## Default object, for now 99 return True 100 101 @property 102 def is_intf(self): 103 # Includes subinterfaces 104 """Returns a boolean (True or False) to answer whether this 105 :class:`~models_junos.JunosCfgLine` is an interface; subinterfaces 106 also return True. 107 108 Returns 109 ------- 110 bool 111 112 Examples 113 -------- 114 115 .. code-block:: python 116 :emphasize-lines: 17,20 117 118 >>> config = [ 119 ... '!', 120 ... 'interface Serial1/0', 121 ... ' ip address 1.1.1.1 255.255.255.252', 122 ... '!', 123 ... 'interface ATM2/0', 124 ... ' no ip address', 125 ... '!', 126 ... 'interface ATM2/0.100 point-to-point', 127 ... ' ip address 1.1.1.5 255.255.255.252', 128 ... ' pvc 0/100', 129 ... ' vbr-nrt 704 704', 130 ... '!', 131 ... ] 132 >>> parse = CiscoConfParse(config) 133 >>> obj = parse.find_objects('^interface\sSerial')[0] 134 >>> obj.is_intf 135 True 136 >>> obj = parse.find_objects('^interface\sATM')[0] 137 >>> obj.is_intf 138 True 139 >>> 140 """ 141 intf_regex = r"^interface\s+(\S+.+)" 142 if self.re_match(intf_regex): 143 return True 144 return False 145 146 @property 147 def is_subintf(self): 148 """Returns a boolean (True or False) to answer whether this 149 :class:`~models_junos.JunosCfgLine` is a subinterface. 150 151 Returns: 152 - bool. 153 154 This example illustrates use of the method. 155 156 .. code-block:: python 157 :emphasize-lines: 17,20 158 159 >>> config = [ 160 ... '!', 161 ... 'interface Serial1/0', 162 ... ' ip address 1.1.1.1 255.255.255.252', 163 ... '!', 164 ... 'interface ATM2/0', 165 ... ' no ip address', 166 ... '!', 167 ... 'interface ATM2/0.100 point-to-point', 168 ... ' ip address 1.1.1.5 255.255.255.252', 169 ... ' pvc 0/100', 170 ... ' vbr-nrt 704 704', 171 ... '!', 172 ... ] 173 >>> parse = CiscoConfParse(config) 174 >>> obj = parse.find_objects('^interface\sSerial')[0] 175 >>> obj.is_subintf 176 False 177 >>> obj = parse.find_objects('^interface\sATM')[0] 178 >>> obj.is_subintf 179 True 180 >>> 181 """ 182 intf_regex = r"^interface\s+(\S+?\.\d+)" 183 if self.re_match(intf_regex): 184 return True 185 return False 186 187 @property 188 def is_virtual_intf(self): 189 intf_regex = ( 190 r"^interface\s+(Loopback|Tunnel|Dialer|Virtual-Template|Port-Channel)" 191 ) 192 if self.re_match(intf_regex): 193 return True 194 return False 195 196 @property 197 def is_loopback_intf(self): 198 """Returns a boolean (True or False) to answer whether this 199 :class:`~models_junos.JunosCfgLine` is a loopback interface. 200 201 Returns: 202 - bool. 203 204 This example illustrates use of the method. 205 206 .. code-block:: python 207 :emphasize-lines: 11,14 208 209 >>> config = [ 210 ... '!', 211 ... 'interface FastEthernet1/0', 212 ... ' ip address 1.1.1.1 255.255.255.252', 213 ... '!', 214 ... 'interface Loopback0', 215 ... ' ip address 1.1.1.5 255.255.255.255', 216 ... '!', 217 ... ] 218 >>> parse = CiscoConfParse(config) 219 >>> obj = parse.find_objects('^interface\sFast')[0] 220 >>> obj.is_loopback_intf 221 False 222 >>> obj = parse.find_objects('^interface\sLoop')[0] 223 >>> obj.is_loopback_intf 224 True 225 >>> 226 """ 227 intf_regex = r"^interface\s+(\Soopback)" 228 if self.re_match(intf_regex): 229 return True 230 return False 231 232 @property 233 def is_ethernet_intf(self): 234 """Returns a boolean (True or False) to answer whether this 235 :class:`~models_junos.JunosCfgLine` is an ethernet interface. 236 Any ethernet interface (10M through 10G) is considered an ethernet 237 interface. 238 239 Returns: 240 - bool. 241 242 This example illustrates use of the method. 243 244 .. code-block:: python 245 :emphasize-lines: 17,20 246 247 >>> config = [ 248 ... '!', 249 ... 'interface FastEthernet1/0', 250 ... ' ip address 1.1.1.1 255.255.255.252', 251 ... '!', 252 ... 'interface ATM2/0', 253 ... ' no ip address', 254 ... '!', 255 ... 'interface ATM2/0.100 point-to-point', 256 ... ' ip address 1.1.1.5 255.255.255.252', 257 ... ' pvc 0/100', 258 ... ' vbr-nrt 704 704', 259 ... '!', 260 ... ] 261 >>> parse = CiscoConfParse(config) 262 >>> obj = parse.find_objects('^interface\sFast')[0] 263 >>> obj.is_ethernet_intf 264 True 265 >>> obj = parse.find_objects('^interface\sATM')[0] 266 >>> obj.is_ethernet_intf 267 False 268 >>> 269 """ 270 intf_regex = r"^interface\s+(.*?\Sthernet)" 271 if self.re_match(intf_regex): 272 return True 273 return False 274 275 276## 277##------------- Junos Interface ABC 278## 279 280# Valid method name substitutions: 281# switchport -> switch 282# spanningtree -> stp 283# interfce -> intf 284# address -> addr 285# default -> def 286 287 288class BaseJunosIntfLine(JunosCfgLine): 289 def __init__(self, *args, **kwargs): 290 super(BaseJunosIntfLine, self).__init__(*args, **kwargs) 291 self.ifindex = None # Optional, for user use 292 self.default_ipv4_addr_object = IPv4Obj("127.0.0.1/32", strict=False) 293 294 def __repr__(self): 295 if not self.is_switchport: 296 if self.ipv4_addr_object == self.default_ipv4_addr_object: 297 addr = "No IPv4" 298 else: 299 ip = str(self.ipv4_addr_object.ip) 300 prefixlen = str(self.ipv4_addr_object.prefixlen) 301 addr = "{0}/{1}".format(ip, prefixlen) 302 return "<%s # %s '%s' info: '%s'>" % ( 303 self.classname, 304 self.linenum, 305 self.name, 306 addr, 307 ) 308 else: 309 return "<%s # %s '%s' info: 'switchport'>" % ( 310 self.classname, 311 self.linenum, 312 self.name, 313 ) 314 315 def reset(self, atomic=True): 316 # Insert build_reset_string() before this line... 317 self.insert_before(self.build_reset_string(), atomic=atomic) 318 319 def build_reset_string(self): 320 # Junos interfaces are defaulted like this... 321 return "default " + self.text 322 323 @property 324 def verbose(self): 325 if not self.is_switchport: 326 return ( 327 "<%s # %s '%s' info: '%s' (child_indent: %s / len(children): %s / family_endpoint: %s)>" 328 % ( 329 self.classname, 330 self.linenum, 331 self.text, 332 self.ipv4_addr_object or "No IPv4", 333 self.child_indent, 334 len(self.children), 335 self.family_endpoint, 336 ) 337 ) 338 else: 339 return ( 340 "<%s # %s '%s' info: 'switchport' (child_indent: %s / len(children): %s / family_endpoint: %s)>" 341 % ( 342 self.classname, 343 self.linenum, 344 self.text, 345 self.child_indent, 346 len(self.children), 347 self.family_endpoint, 348 ) 349 ) 350 351 @classmethod 352 def is_object_for(cls, line="", re=re): 353 return False 354 355 ##------------- Basic interface properties 356 357 @property 358 def name(self): 359 """Return the interface name as a string, such as 'GigabitEthernet0/1' 360 361 Returns: 362 - str. The interface name as a string, or '' if the object is not an interface. 363 364 This example illustrates use of the method. 365 366 .. code-block:: python 367 :emphasize-lines: 17,20,23 368 369 >>> config = [ 370 ... '!', 371 ... 'interface FastEthernet1/0', 372 ... ' ip address 1.1.1.1 255.255.255.252', 373 ... '!', 374 ... 'interface ATM2/0', 375 ... ' no ip address', 376 ... '!', 377 ... 'interface ATM2/0.100 point-to-point', 378 ... ' ip address 1.1.1.5 255.255.255.252', 379 ... ' pvc 0/100', 380 ... ' vbr-nrt 704 704', 381 ... '!', 382 ... ] 383 >>> parse = CiscoConfParse(config, factory=True) 384 >>> obj = parse.find_objects('^interface\sFast')[0] 385 >>> obj.name 386 'FastEthernet1/0' 387 >>> obj = parse.find_objects('^interface\sATM')[0] 388 >>> obj.name 389 'ATM2/0' 390 >>> obj = parse.find_objects('^interface\sATM')[1] 391 >>> obj.name 392 'ATM2/0.100' 393 >>> 394 """ 395 if not self.is_intf: 396 return "" 397 intf_regex = r"^interface\s+(\S+[0-9\/\.\s]+)\s*" 398 name = self.re_match(intf_regex).strip() 399 return name 400 401 @property 402 def port(self): 403 """Return the interface's port number 404 405 Returns: 406 - int. The interface number. 407 408 This example illustrates use of the method. 409 410 .. code-block:: python 411 :emphasize-lines: 17,20 412 413 >>> config = [ 414 ... '!', 415 ... 'interface FastEthernet1/0', 416 ... ' ip address 1.1.1.1 255.255.255.252', 417 ... '!', 418 ... 'interface ATM2/0', 419 ... ' no ip address', 420 ... '!', 421 ... 'interface ATM2/0.100 point-to-point', 422 ... ' ip address 1.1.1.5 255.255.255.252', 423 ... ' pvc 0/100', 424 ... ' vbr-nrt 704 704', 425 ... '!', 426 ... ] 427 >>> parse = CiscoConfParse(config, factory=True) 428 >>> obj = parse.find_objects('^interface\sFast')[0] 429 >>> obj.port 430 0 431 >>> obj = parse.find_objects('^interface\sATM')[0] 432 >>> obj.port 433 0 434 >>> 435 """ 436 return self.ordinal_list[-1] 437 438 @property 439 def port_type(self): 440 """Return Loopback, ATM, GigabitEthernet, Virtual-Template, etc... 441 442 Returns: 443 - str. The port type. 444 445 This example illustrates use of the method. 446 447 .. code-block:: python 448 :emphasize-lines: 17,20 449 450 >>> config = [ 451 ... '!', 452 ... 'interface FastEthernet1/0', 453 ... ' ip address 1.1.1.1 255.255.255.252', 454 ... '!', 455 ... 'interface ATM2/0', 456 ... ' no ip address', 457 ... '!', 458 ... 'interface ATM2/0.100 point-to-point', 459 ... ' ip address 1.1.1.5 255.255.255.252', 460 ... ' pvc 0/100', 461 ... ' vbr-nrt 704 704', 462 ... '!', 463 ... ] 464 >>> parse = CiscoConfParse(config, factory=True) 465 >>> obj = parse.find_objects('^interface\sFast')[0] 466 >>> obj.port_type 467 'FastEthernet' 468 >>> obj = parse.find_objects('^interface\sATM')[0] 469 >>> obj.port_type 470 'ATM' 471 >>> 472 """ 473 port_type_regex = r"^interface\s+([A-Za-z\-]+)" 474 return self.re_match(port_type_regex, group=1, default="") 475 476 @property 477 def ordinal_list(self): 478 """Return a tuple of numbers representing card, slot, port for this interface. If you call ordinal_list on GigabitEthernet2/25.100, you'll get this python tuple of integers: (2, 25). If you call ordinal_list on GigabitEthernet2/0/25.100 you'll get this python list of integers: (2, 0, 25). This method strips all subinterface information in the returned value. 479 480 Returns: 481 - tuple. A tuple of port numbers as integers. 482 483 .. warning:: 484 485 ordinal_list should silently fail (returning an empty python list) if the interface doesn't parse correctly 486 487 This example illustrates use of the method. 488 489 .. code-block:: python 490 :emphasize-lines: 17,20 491 492 >>> config = [ 493 ... '!', 494 ... 'interface FastEthernet1/0', 495 ... ' ip address 1.1.1.1 255.255.255.252', 496 ... '!', 497 ... 'interface ATM2/0', 498 ... ' no ip address', 499 ... '!', 500 ... 'interface ATM2/0.100 point-to-point', 501 ... ' ip address 1.1.1.5 255.255.255.252', 502 ... ' pvc 0/100', 503 ... ' vbr-nrt 704 704', 504 ... '!', 505 ... ] 506 >>> parse = CiscoConfParse(config, factory=True) 507 >>> obj = parse.find_objects('^interface\sFast')[0] 508 >>> obj.ordinal_list 509 (1, 0) 510 >>> obj = parse.find_objects('^interface\sATM')[0] 511 >>> obj.ordinal_list 512 (2, 0) 513 >>> 514 """ 515 if not self.is_intf: 516 return () 517 else: 518 intf_regex = r"^interface\s+[A-Za-z\-]+\s*(\d+.*?)(\.\d+)*(\s\S+)*\s*$" 519 intf_number = self.re_match(intf_regex, group=1, default="") 520 if intf_number: 521 return tuple([int(ii) for ii in intf_number.split("/")]) 522 else: 523 return () 524 525 @property 526 def description(self): 527 """Return the current interface description string. 528 529 """ 530 retval = self.re_match_iter_typed( 531 r"^\s*description\s+(\S.+)$", result_type=str, default="" 532 ) 533 return retval 534 535 @property 536 def manual_bandwidth(self): 537 retval = self.re_match_iter_typed( 538 r"^\s*bandwidth\s+(\d+)$", result_type=int, default=0 539 ) 540 return retval 541 542 @property 543 def manual_delay(self): 544 retval = self.re_match_iter_typed( 545 r"^\s*delay\s+(\d+)$", result_type=int, default=0 546 ) 547 return retval 548 549 550## 551##------------- Junos Interface Globals 552## 553 554 555class JunosIntfGlobal(BaseCfgLine): 556 def __init__(self, *args, **kwargs): 557 super(JunosIntfGlobal, self).__init__(*args, **kwargs) 558 self.feature = "interface global" 559 560 def __repr__(self): 561 return "<%s # %s '%s'>" % (self.classname, self.linenum, self.text) 562 563 @classmethod 564 def is_object_for(cls, line="", re=re): 565 if re.search( 566 "^(no\s+cdp\s+run)|(logging\s+event\s+link-status\s+global)|(spanning-tree\sportfast\sdefault)|(spanning-tree\sportfast\sbpduguard\sdefault)", 567 line, 568 ): 569 return True 570 return False 571 572 @property 573 def has_cdp_disabled(self): 574 if self.re_search("^no\s+cdp\s+run\s*"): 575 return True 576 return False 577 578 @property 579 def has_intf_logging_def(self): 580 if self.re_search("^logging\s+event\s+link-status\s+global"): 581 return True 582 return False 583 584 @property 585 def has_stp_portfast_def(self): 586 if self.re_search("^spanning-tree\sportfast\sdefault"): 587 return True 588 return False 589 590 @property 591 def has_stp_portfast_bpduguard_def(self): 592 if self.re_search("^spanning-tree\sportfast\sbpduguard\sdefault"): 593 return True 594 return False 595 596 @property 597 def has_stp_mode_rapidpvst(self): 598 if self.re_search("^spanning-tree\smode\srapid-pvst"): 599 return True 600 return False 601 602 603## 604##------------- Junos Hostname Line 605## 606 607 608class JunosHostnameLine(BaseCfgLine): 609 def __init__(self, *args, **kwargs): 610 super(JunosHostnameLine, self).__init__(*args, **kwargs) 611 self.feature = "hostname" 612 613 def __repr__(self): 614 return "<%s # %s '%s'>" % (self.classname, self.linenum, self.hostname) 615 616 @classmethod 617 def is_object_for(cls, line="", re=re): 618 if re.search("^hostname", line): 619 return True 620 return False 621 622 @property 623 def hostname(self): 624 retval = self.re_match_typed(r"^hostname\s+(\S+)", result_type=str, default="") 625 return retval 626 627 628## 629##------------- Base Junos Route line object 630## 631 632 633class BaseJunosRouteLine(BaseCfgLine): 634 def __init__(self, *args, **kwargs): 635 super(BaseJunosRouteLine, self).__init__(*args, **kwargs) 636 637 def __repr__(self): 638 return "<%s # %s '%s' info: '%s'>" % ( 639 self.classname, 640 self.linenum, 641 self.network_object, 642 self.routeinfo, 643 ) 644 645 @property 646 def routeinfo(self): 647 ### Route information for the repr string 648 if self.tracking_object_name: 649 return ( 650 self.nexthop_str 651 + " AD: " 652 + str(self.admin_distance) 653 + " Track: " 654 + self.tracking_object_name 655 ) 656 else: 657 return self.nexthop_str + " AD: " + str(self.admin_distance) 658 659 @classmethod 660 def is_object_for(cls, line="", re=re): 661 return False 662 663 @property 664 def vrf(self): 665 raise NotImplementedError 666 667 @property 668 def address_family(self): 669 ## ipv4, ipv6, etc 670 raise NotImplementedError 671 672 @property 673 def network(self): 674 raise NotImplementedError 675 676 @property 677 def netmask(self): 678 raise NotImplementedError 679 680 @property 681 def admin_distance(self): 682 raise NotImplementedError 683 684 @property 685 def nexthop_str(self): 686 raise NotImplementedError 687 688 @property 689 def tracking_object_name(self): 690 raise NotImplementedError 691 692 693## 694##------------- Junos Configuration line object 695## 696 697 698class JunosRouteLine(BaseJunosRouteLine): 699 def __init__(self, *args, **kwargs): 700 super(JunosRouteLine, self).__init__(*args, **kwargs) 701 if "ipv6" in self.text: 702 self.feature = "ipv6 route" 703 else: 704 self.feature = "ip route" 705 706 @classmethod 707 def is_object_for(cls, line="", re=re): 708 if re.search("^(ip|ipv6)\s+route\s+\S", line): 709 return True 710 return False 711 712 @property 713 def vrf(self): 714 retval = self.re_match_typed( 715 r"^(ip|ipv6)\s+route\s+(vrf\s+)*(\S+)", group=3, result_type=str, default="" 716 ) 717 return retval 718 719 @property 720 def address_family(self): 721 ## ipv4, ipv6, etc 722 retval = self.re_match_typed( 723 r"^(ip|ipv6)\s+route\s+(vrf\s+)*(\S+)", group=1, result_type=str, default="" 724 ) 725 return retval 726 727 @property 728 def network(self): 729 if self.address_family == "ip": 730 retval = self.re_match_typed( 731 r"^ip\s+route\s+(vrf\s+)*(\S+)", group=2, result_type=str, default="" 732 ) 733 elif self.address_family == "ipv6": 734 retval = self.re_match_typed( 735 r"^ipv6\s+route\s+(vrf\s+)*(\S+?)\/\d+", 736 group=2, 737 result_type=str, 738 default="", 739 ) 740 return retval 741 742 @property 743 def netmask(self): 744 if self.address_family == "ip": 745 retval = self.re_match_typed( 746 r"^ip\s+route\s+(vrf\s+)*\S+\s+(\S+)", 747 group=2, 748 result_type=str, 749 default="", 750 ) 751 elif self.address_family == "ipv6": 752 retval = self.re_match_typed( 753 r"^ipv6\s+route\s+(vrf\s+)*\S+?\/(\d+)", 754 group=2, 755 result_type=str, 756 default="", 757 ) 758 return retval 759 760 @property 761 def network_object(self): 762 try: 763 if self.address_family == "ip": 764 return IPv4Obj("%s/%s" % (self.network, self.netmask), strict=False) 765 elif self.address_family == "ipv6": 766 return IPv6Network("%s/%s" % (self.network, self.netmask)) 767 except: 768 return None 769 770 @property 771 def nexthop_str(self): 772 if self.address_family == "ip": 773 retval = self.re_match_typed( 774 r"^ip\s+route\s+(vrf\s+)*\S+\s+\S+\s+(\S+)", 775 group=2, 776 result_type=str, 777 default="", 778 ) 779 elif self.address_family == "ipv6": 780 retval = self.re_match_typed( 781 r"^ipv6\s+route\s+(vrf\s+)*\S+\s+(\S+)", 782 group=2, 783 result_type=str, 784 default="", 785 ) 786 return retval 787 788 @property 789 def admin_distance(self): 790 retval = self.re_match_typed(r"(\d+)$", group=1, result_type=int, default=1) 791 return retval 792 793 @property 794 def tracking_object_name(self): 795 retval = self.re_match_typed( 796 r"^ip(v6)*\s+route\s+.+?track\s+(\S+)", group=2, result_type=str, default="" 797 ) 798 return retval 799