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