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