PK!;a qqnetworkparse/__init__.py""" A simple network configuration searcher """ from pathlib import Path from . import core from . import parse PK!)hCCnetworkparse/core.py""" Search a network configuration file This module holds the primitives that make up a network configuration--configuration lines and lists of lines. To begin using `networkparse`, start with :module:`~parse` """ import re from typing import List class MultipleLinesError(Exception): """ More than one line was found """ class NoLinesError(Exception): """ No lines were found and at least one was expected """ class ConfigLine: """ A line of a configuration file, which may or may not have children Generally supports the same functions as Python strings. """ #: The :class:`~.parse.ConfigBase`-subclass which created the line and will be used for #: checking style-specific questions config_manager = None #: Parent block line (ie, might point to a line that's interface Eth1/1) #: Could be a :class:`~.parse.ConfigBase` or another :class:`~ConfigLine` parent = None #: The line number in the original configuration file line_number = None #: Text of configuration line, not including any leading or trailing whitespace #: Generally, treat the :class:`~ConfigLine` itself as a string, rather than #: using `.text`. For example: #: #: .. code:: python #: #: # Prefer these: #: parts = line.split() #: print(line) #: #: # To these: #: parts = line.text.split() #: print(line.text) text = "" #: Lines "under" this one--for example, lines 2 and 3 below would be children #: to line 1. #: #: .. code:: #: #: 1. interface Ethernet0/1 #: 2. no shutdown #: 3. switchport mode access children = None def __init__( self, config_manager, parent, text: str, line_number: int = None, children: List = None, ): """ A configuration line. Typically not created manually """ self.config_manager = config_manager self.text = text self.parent = parent self.line_number = line_number self.children = children or ConfigLineList() def __str__(self): return self.text def __repr__(self): return f"Line {self.line_number}: {self.text} ({len(self.children)} children)" @property def siblings(self): """ Returns a :class:`~ConfigLineList` of all sibling lines Does not include this line in the list. If you do want this line in the list, try :code:`line.parent.children`. """ siblings = self.parent.children.copy() siblings.remove(self) return siblings def tree_display( self, line_number: bool = False, initial_indent: int = 0, internal_indent: int = 0, ) -> str: """ Print this line and child lines indented to show hierachy :param line_number: Display original line numbers of each line :param initial_indent: How many spaces to put before the first text on the line. :param internal_indent: How many spaces to place *after* the line number :return: String of this and all child line items in an indented, human-readable format. """ start_str = " " * initial_indent if line_number: start_str += f"{self.line_number}: " start_str += " " * internal_indent lines = [f"{start_str}{self.text} ({len(self.children)} children)"] for c in self.children: lines.append( c.tree_display( initial_indent=initial_indent, line_number=line_number, internal_indent=internal_indent + self.config_manager.indent_size, ) ) return "\n".join(lines) def is_comment(self) -> bool: """ Check if this line is a comment :return: True if line is a commment, False otherwise """ return self.text.startswith(self.config_manager.comment_marker) def __contains__(self, x): """ Support "in" """ return x in self.text def __eq__(self, other): """ If compared to another :class:`~ConfigLine`, ensure it's has the same instance Otherwise do a string compare """ if isinstance(other, ConfigLine): return self is other else: return self.text == str(other) def __len__(self): return len(self.text) def __getitem__(self, index): return self.text[index] def __iter__(self): return iter(self.text) def find(self, sub, start=None, end=None): """ See `str.find`_ .. _`str.find`: https://docs.python.org/3/library/stdtypes.html#str.find """ return self.text.find(sub, start, end) def rfind(self, sub, start=None, end=None): """ See `str.rfind`_ .. _`str.rfind`: https://docs.python.org/3/library/stdtypes.html#str.rfind """ return self.text.rfind(sub, start, end) def rindex(self, sub, start=None, end=None): """ See `str.rindex`_ .. _`str.rindex`: https://docs.python.org/3/library/stdtypes.html#str.rindex """ return self.text.rindex(sub, start, end) def index(self, sub, start=None, end=None): """ See `str.index`_ .. _`str.index`: https://docs.python.org/3/library/stdtypes.html#str.index """ return self.text.index(sub, start, end) def count(self, sub, start=None, end=None): """ See `str.count`_ .. _`str.count`: https://docs.python.org/3/library/stdtypes.html#str.count """ return self.text.count(sub, start, end) def endswith(self, sub, start=None, end=None): """ See `str.endswith`_ .. _`str.endswith`: https://docs.python.org/3/library/stdtypes.html#str.endswith """ return self.text.endswith(sub, start, end) def startswith(self, sub, start=None, end=None): """ See `str.startswith`_ .. _`str.startswith`: https://docs.python.org/3/library/stdtypes.html#str.startswith """ return self.text.startswith(sub, start, end) def upper(self): """ See `str.upper`_ .. _`str.upper`: https://docs.python.org/3/library/stdtypes.html#str.upper """ return self.text.upper() def lower(self): """ See `str.lower`_ .. _`str.lower`: https://docs.python.org/3/library/stdtypes.html#str.lower """ return self.text.lower() def partition(self, sep): """ See `str.partition`_ .. _`str.partition`: https://docs.python.org/3/library/stdtypes.html#str.partition """ return self.text.partition(sep) def rpartition(self, sep): """ See `str.rpartition`_ .. _`str.rpartition`: https://docs.python.org/3/library/stdtypes.html#str.rpartition """ return self.text.rpartition(sep) def split(self, sep=None, maxsplit=-1): """ See `str.split`_ .. _`str.split`: https://docs.python.org/3/library/stdtypes.html#str.split """ return self.text.split(sep, maxsplit) def rsplit(self, sep=None, maxsplit=-1): """ See `str.rsplit`_ .. _`str.rsplit`: https://docs.python.org/3/library/stdtypes.html#str.rsplit """ return self.text.rsplit(sep, maxsplit) class ConfigLineList: """ A searchable list of :class:`~ConfigLine` s This class acts like a standard Python list, so indexed access via `[]`, `len()`, etc. all work. See the Python 3 documentation on `list`_ for more methods. This class may not hold only :class:`~ConfigLine` items from the same parent--it can store *any* :class:`~ConfigLine`, so be aware that iterating through a :class:`~ConfigLineList` does not necessarily mean all items have the same parent. In particular, after running :func:`~filter` or :func:`~flatten` the returned list will be a mixture of parents. .. _list: https://docs.python.org/3/tutorial/datastructures.html#more-on-lists """ def __init__(self, lines: List[ConfigLine] = None): self.lines = lines or [] def __len__(self): return len(self.lines) def __getitem__(self, index): return self.lines[index] def __iter__(self): return iter(self.lines) def __getattr__(self, name): """ Defers all failing calls to list """ return getattr(self.lines, name) def __str__(self): return self.tree_display(line_number=True) def __repr__(self): return str(self) def copy(self) -> List: """ Create a copy of this list :return: Copy of list. :class:`~ConfigLine` s are not duplicated. :rtype: ConfigLineList """ return ConfigLineList(self.lines.copy()) def tree_display(self, line_number: bool = False, initial_indent: int = 0) -> str: """ Print all lines in list with indents to show hierachy :param line_number: Display original line numbers of each line :param initial_indent: How many spaces to put before the first text on the line. :return: String of this and all child line items in an indented, human-readable format. Also refer to :class:`~ConfigLine`'s :func:`~ConfigLine.tree_display`. .. note:: Top-level items may not all have the same parent, as a :class:`~ConfigLineList` can hold any combination of lines (even duplicates). """ if not self.lines: return (" " * initial_indent) + "(empty line list)" lines = [] for line in self.lines: lines.append( line.tree_display( initial_indent=initial_indent, line_number=line_number ) ) return "\n".join(lines) def one(self) -> ConfigLine: """ Returns the single ConfigLine in list :raises MultipleLinesError: There is more than one item in list. :raises NoLinesError: There are no items in list. Use :func:`~one_or_none` to return `None` if there are no items :return: First and only :class:`~ConfigLine` """ item = self.one_or_none() if item is None: raise NoLinesError() return item def one_or_none(self) -> ConfigLine: """ Returns the single ConfigLine in list :raises MultipleLinesError: There is more than one item in list. Use :func:`~one` to raise an exception if there are no items :return: First and only :class:`~ConfigLine` if there is one, otherwise `None` """ if len(self) == 0: return None elif len(self) > 1: raise MultipleLinesError() else: return self[0] def flatten(self, depth: int = None) -> List: """ Return a ConfigLineList of all of this list *and* the children :param depth: If `None`, returns all children, recursing as deeply as needed into the hierarchy (technically, limited to 500). Otherwise, flattens only the top `depth` levels. :return: New :class:`~ConfigLineList` For example, say you have the structure: .. code:: level 1 level 2 level 3 `flatten(depth=None)` returns: .. code:: level 1 level 2 level 3 `flatten(depth=1)` returns: .. code:: level 1 level 2 level 3 """ if depth is None: depth = 500 flattened = ConfigLineList() for line in self.lines: flattened.append(line) if depth > 0: flattened.extend(line.children.flatten(depth=depth - 1)) return flattened def filter( self, regex: str, full_match: bool = None, invert: bool = False, depth: int = 0 ) -> List: """ Find all lines that match a regular expression :param regex: A string in Python's regex format that will be checked against each line :param full_match: If True, the regex must match the entire line (using Python's `re.fullmatch`). If False, the regex can match any where in the line (using `re.search`). If None, uses the default for contained lines' `config_manager` :param invert: If True, excludes matched items from the list. Default is False, matching lines are returned in the new list :param depth: Controls whether searches look at direct descendents - `depth = 0`: Line must be in this list, not a child - `depth > 0`: Also search the `children` of each line, up to the given depth - `depth = None`: Search through entire tree :return: New ConfigLineList with the filtered items. Returns an empty list if none are found. :rtype: ConfigLineList """ if not self.lines: return ConfigLineList() if full_match is None: full_match = self.lines[0].config_manager.full_match compiled = re.compile(regex) if full_match and not invert: match_func = compiled.fullmatch elif full_match and invert: match_func = lambda t: not compiled.fullmatch(t) elif not full_match and not invert: match_func = compiled.search elif not full_match and invert: match_func = lambda t: not compiled.search(t) starting_list = self if depth != 0: starting_list = self.flatten(depth=depth) matches = ConfigLineList() for s in starting_list: if not s.is_comment() and match_func(s.text): matches.append(s) return matches def exclude(self, regex: str, **kwargs) -> List: """ Calls :func:`~filter` with `invert` = `True` kwargs are passed through to :func:`~filter`, see that for for argument descriptions. :return: List with matching lines removed :rtype: ConfigLineList """ return self.filter(regex=regex, invert=True, **kwargs) def filter_with_child( self, child_regex: str, full_match: bool = None, depth: int = 0 ) -> List: """ Find all lines that have a *child* that matches the given regex. :param child_regex: Regular expression that child line must match :param full_match: Whether the regex must match the entire child line. See :func:`~filter` for more details. :param depth: If is not 0, the child does not need to be a direct descendent. See :func:`~filter`'s `depth` argument for more details. :return: New filtered list :rtype: ConfigLineList For example, if this list has these items (children shown as well): .. code:: child child 1 child 2 child 3 child 4 child 5 child 6 child 7 child 8 child 9 Then `filter_with_child("child 4")` will return the list: .. code:: child 3 child 4 child 5 If `depth` is not 0, the child does not need to be a direct descendent. See `filter()`'s `depth` argument for more details. Given the example above, _with_child("child 8", depth=None)` will return the list: .. code:: child 6 child 7 child 8 child 9 """ real_match = ConfigLineList() for match in self.lines: if match.children.flatten(depth=depth).filter( regex=child_regex, full_match=full_match ): real_match.append(match) return real_match def filter_without_child( self, child_regex: str, full_match: bool = None, depth: int = 0, skip_childless: bool = False, ) -> List: """ Find all lines that do not have a child that matches the given regex. Follows the symmatics of :func:`~filter_with_child`, see that function for more details. :param skip_childless: If False (the default), a line that has no children will always be matched by this function. :return: New filtered list :rtype: ConfigLineList """ real_match = ConfigLineList() for match in self.lines: if skip_childless and not match.children: continue if not match.children.flatten(depth=depth).filter( regex=child_regex, full_match=full_match ): real_match.append(match) return real_match PK!9 9 networkparse/parse.py""" Parse a network configuration file To begin using `networkparse`, typically an subclass of :class:`~ConfigBase` will be instantiated with the text of the configuration file. Currently, `networkparse` has support for: - Cisco IOS: :class:`~ConfigIOS` - Cisco NX-OS: :class:`~ConfigNXOS` - Junos: :class:`~ConfigJunos` """ import re from collections import namedtuple from typing import List from .core import ConfigLineList, ConfigLine class ConfigBase(ConfigLineList): """ Common configuration base operations :class:`~ConfigBase` is really just a specialized :class:`~.core.ConfigLineList` that can hold some settings and act like a :class:`~.core.ConfigLine` in terms of having a parent (`None`) and children. Refer to :class:`~.core.ConfigLineList` for filtering and searching options after you've parsed a configuration file. """ #: Defaults to ! as the comment marker, following Cisco convention. If more # complex comment checking is needed override is_comment() comment_marker = "!" #: Default setting for `full_match` in `filter`. Defaults to True to prevent #: a search from also matching the "no" version of the line. full_match = True #: How far tree_display() should indent children. Has no effect on parsing indent_size = 2 #: Original configuration lines, before any parsing occured. The #: :attr:`~ConfigLine.line_number` from a :class:`~ConfigLine` will match #: up with this list original_lines = None #: Exists to make walking up a parent tree easier--just look for parent=None to stop #: #: Contrived example: #: #: .. code:: python #: #: current_line = config.filter("no shutdown", depth=None) #: while current_line.parent is not None: #: print(current_line) #: current_line = current_line.parent parent = None def __init__( self, name="Network Config", original_lines: List[str] = None, comment_marker: str = "!", full_match_default: bool = True, indent_size_default: int = 2, ): """ Configures settings used by :class:`~ConfigLine` methods In addition, subclasses should override this to parse the configuration file into :class:`~ConfigLine`s. See :class:`~ConfigIOS` for an example of this. """ super().__init__() self.name = name self.comment_marker = comment_marker self.full_match = full_match_default self.original_lines = original_lines or [] self.indent_size = indent_size_default @property def children(self) -> ConfigLineList: """ Allow for use of ".children" for consistency with :class:`~ConfigLine` Returns `self`, which is already a :class:`~ConfigLineList`. It is likely cleaner to not use this. I.E.: .. code:: python config = ConfigIOS(running_config_contents) # Prefer this config.filter("interface .+") # Only use this if it looks clearer in context config.children.filter("interface .+") """ return self class ConfigIOS(ConfigBase): """ Parses Cisco IOS-style configuration into common config format Supported command output: - `show running-config` - `show running-config all` - `show startup-config` See :class:`~ConfigBase` for more information. """ def __init__(self, config_content): """ Break all lines up into tree """ super().__init__( name="IOS Config", original_lines=config_content.splitlines(), comment_marker="!", ) parent_stack = {0: self} last_line = None last_indent = 0 for lineno, line in enumerate(self.original_lines): # Determine our config depth and compare to the previous line's depth # The top-level config is always on the stack, so account for that matches = re.match(r"^(?P\s*)", line) new_indent = len(matches.group("spaces")) if new_indent > last_indent: # Need to change parents to the last item of our current parent parent_stack[new_indent] = last_line curr_parent = parent_stack[new_indent] last_indent = new_indent last_line = ConfigLine( config_manager=self, parent=curr_parent, text=line.strip(), line_number=lineno, ) curr_parent.children.append(last_line) class ConfigNXOS(ConfigIOS): """ Parses Cisco NX-OS-style configuration into common config format See :class:`~ConfigIOS` for more information. """ class ConfigASA(ConfigBase): """ Parses Cisco ASA-style configuration into common config format Supported command output: - `show running-config` - `show running-config all` - `show startup-config` See :class:`~ConfigBase` for more information. """ def __init__(self, config_content): """ Break all lines up into tree """ super().__init__( name="ASA Config", original_lines=config_content.splitlines(), comment_marker="!", ) parent_stack = {0: self} last_line = None last_indent = 0 for lineno, line in enumerate(self.original_lines): # ASAs are full of blank lines that don't matter if not line.strip(): continue # Determine our config depth and compare to the previous line's depth # The top-level config is always on the stack, so account for that matches = re.match(r"^(?P\s*)", line) new_indent = len(matches.group("spaces")) if new_indent > last_indent: # Need to change parents to the last item of our current parent parent_stack[new_indent] = last_line curr_parent = parent_stack[new_indent] last_indent = new_indent last_line = ConfigLine( config_manager=self, parent=curr_parent, text=line.strip(), line_number=lineno, ) curr_parent.children.append(last_line) class ConfigHPCommware(ConfigIOS): """ Parses HP Commware-style configuration into common config format See :class:`~ConfigBase` for more information. """ # Currently excactly the same as IOS class ConfigJunos(ConfigBase): """ Parses a Juniper OS (Junos)-style configuration into common config format Supported command outputs are: - `show configuration` - `save` See :class:`~ConfigBase` for more information. """ def __init__(self, config_content): """ Break all lines up into tree """ super().__init__( name="Junos Config", original_lines=config_content.splitlines(), comment_marker="#", ) parent_stack = [self] last_line = None for lineno, line in enumerate(self.original_lines): curr_parent = parent_stack[-1] command = True block_start = False block_end = False modified_line = line.strip() if modified_line.endswith(";"): command = True elif modified_line.endswith("{"): block_start = True elif modified_line.endswith("}"): block_end = True if block_start or block_end or command: modified_line = modified_line[:-1] if not block_end: last_line = ConfigLine( config_manager=self, parent=curr_parent, text=modified_line.strip(), line_number=lineno, ) curr_parent.children.append(last_line) # Change indent? if block_start: parent_stack.append(last_line) elif block_end: parent_stack.pop() PK!=B$$$networkparse-1.0.1.dist-info/LICENSECopyright © 2016-2018, Xylok, LLC. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!H+dUT"networkparse-1.0.1.dist-info/WHEEL HM K-*ϳR03rOK-J,/R(O-)T0343 /, (-JLR()*M IL*4KM̫PK!H٭ @%networkparse-1.0.1.dist-info/METADATATn0 +x\nעuXhdt(PLUډ9iGJf-XOWEz*FFFx[4ȃ p |a{Яv1#gA6-aSja]{P q8ryK},W-umgޭ0QvPb^uV|zur ;7eYz5ҫB){di\(Ok{<$IC/Wwt#3AlUmyՒ<֌=ѯwV^IR2푡͌ H!;[hvӻ .avA&;d2z҃ORX03h *wt㚛}Y<:?YL5[#]R<,BifʊS' PK!H&[&#networkparse-1.0.1.dist-info/RECORDn@f`*Q( P8@+5MLtnKOpn١XO^0i=`<.\_e^@X xFP H3J F>s8G/oy^R! !BȜ6޶xTI>pe`{8[lsnny`> w꬘S%Y{xq*LiQ_Ӳ}q&CtUꔻVO-Ui(Hstr[x2GȽ4^tԘ6\nIKFtCSaEGVcWrgYyE{vZlfVv#s{og?qPK!;a qqnetworkparse/__init__.pyPK!)hCCnetworkparse/core.pyPK!9 9 eDnetworkparse/parse.pyPK!=B$$$dnetworkparse-1.0.1.dist-info/LICENSEPK!H+dUT"7inetworkparse-1.0.1.dist-info/WHEELPK!H٭ @%inetworkparse-1.0.1.dist-info/METADATAPK!H&[&#lnetworkparse-1.0.1.dist-info/RECORDPKHn