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