⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/+class.deprecated
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Deprecated the public HPEConfigParser class in lieu of a private class that should be subclassed for specific HP platforms.
1 change: 1 addition & 0 deletions changes/752.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added custom parsing of HP Network OS devices.
10 changes: 9 additions & 1 deletion netutils/config/compliance.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@
}


# Network OSes that we do not strip leading whitespace from the config lines.
NON_STRIP_NETWORK_OS = [
"hp_comware",
]


# TODO: Once support for 3.7 is dropped, there should be a typing.TypedDict for this which should then also be used
# as the return type for a bunch of the following methods.
default_feature: t.Dict[str, t.Union[str, bool, None]] = {
Expand Down Expand Up @@ -461,7 +467,9 @@ def section_config(
else:
match = False
for line_start in section_starts_with: # type: ignore
if not match and line.config_line.startswith(line_start):
if not match and not line.parents and line.config_line.startswith(line_start):
section_config_list.append(line.config_line)
match = True
if network_os in NON_STRIP_NETWORK_OS:
return "\n".join(section_config_list)
return "\n".join(section_config_list).strip()
98 changes: 87 additions & 11 deletions netutils/config/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from netutils.banner import normalise_delimiter_caret_c
from netutils.config.conversion import paloalto_panos_brace_to_set
from netutils.config.utils import _deprecated

ConfigLine = namedtuple("ConfigLine", "config_line,parents")

Expand Down Expand Up @@ -1675,16 +1676,91 @@ def config_lines_only(self) -> str:
return "\n".join(config_lines)


class HPEConfigParser(BaseSpaceConfigParser):
class _HPEConfigParser(BaseSpaceConfigParser):
"""HPE Implementation of ConfigParser Class."""

regex_banner = re.compile(r"^header\s(\w+)\s+(?P<banner_delimiter>\^C|\S?)")
regex_banner = re.compile(r"^\s*header\s(\w+)\s+(?P<banner_delimiter>\^C|\S?)")
banner_start: t.List[str] = ["header "]
comment_chars: t.List[str] = ["#"]

def __init__(self, config: str):
"""Initialize the HPEConfigParser object."""
"""Initialize the _HPEConfigParser object."""
self.delimiter = ""
self._banner_end: t.Optional[str] = None
super(HPEConfigParser, self).__init__(config)
super(_HPEConfigParser, self).__init__(config)

@property
def config_lines_only(self) -> str:
"""Remove spaces and unwanted lines from config lines, but leave comments.

Returns:
The non-space lines from ``config``.
"""
if self._config is None:
config_lines = (line.rstrip() for line in self.config.splitlines() if line and not line.isspace())
self._config = "\n".join(config_lines)
return self._config

def build_config_relationship(self) -> t.List[ConfigLine]:
r"""This is a custom build method for HPE Network OS.

HP config is a bit different from other network operating systems.
It uses comments (#) to demarcate sections of the config.
Each new section that starts without a leading space is a new section.
That new section may or may not have children.
Each config line that has a leading space but not a parent is just a single config line.
Single lines that have leading spaces also sometimes differs between models (e.g., 59XX vs 79XX series).

Examples:
>>> from netutils.config.parser import _HPEConfigParser, ConfigLine
>>> config = '''#
... version 7.1.045, Release 2418P06
... #
... sysname NTC123456
... #
... vlan 101
... name Test-Vlan-101
... description Test Vlan 101
... #'''
>>> config_tree = _HPEConfigParser(config)
>>> config_tree.build_config_relationship() == \
... [
... ConfigLine(config_line="version 7.1.045, Release 2418P06", parents=()),
... ConfigLine(config_line=" sysname NTC123456", parents=()),
... ConfigLine(config_line="vlan 101", parents=()),
... ConfigLine(config_line=" name Test-Vlan-101", parents=("vlan 101",)),
... ConfigLine(config_line=" description Test Vlan 101", parents=("vlan 101",)),
... ]
True
>>>
"""
new_section = True
for line in self.generator_config:
if line.startswith(tuple(self.comment_chars)):
# Closing any previous sections
self._current_parents = ()
self.indent_level = 0
new_section = True
continue
if line.strip().startswith(tuple(self.comment_chars)):
# Just ignore comments inside sections
continue
if self.is_banner_start(line):
# Special case for banners
self._build_banner(line)
continue

current_spaces = self.get_leading_space_count(line) if line[0].isspace() else 0
if current_spaces > self.indent_level and not new_section:
previous_config = self.config_lines[-1]
self._current_parents += (previous_config.config_line,)
elif current_spaces < self.indent_level:
self._current_parents = self._remove_parents(line, current_spaces)

new_section = False
self.indent_level = current_spaces
self._update_config_lines(line)
return self.config_lines

def _build_banner(self, config_line: str) -> t.Optional[str]:
"""
Expand Down Expand Up @@ -1744,7 +1820,7 @@ def is_banner_one_line(self, config_line: str) -> bool:

def is_banner_start(self, line: str) -> bool:
"""Checks if the given line is the start of a banner."""
state = super(HPEConfigParser, self).is_banner_start(line)
state = super(_HPEConfigParser, self).is_banner_start(line)
if state:
self.banner_end = line
return state
Expand All @@ -1763,15 +1839,15 @@ def banner_end(self, banner_start_line: str) -> None:
self._banner_end = self.delimiter


class HPComwareConfigParser(HPEConfigParser, BaseSpaceConfigParser):
class HPComwareConfigParser(_HPEConfigParser):
"""HP Comware Implementation of ConfigParser Class."""

banner_start: t.List[str] = ["header "]
comment_chars: t.List[str] = ["#"]

def _build_banner(self, config_line: str) -> t.Optional[str]:
"""Build a banner from the given config line."""
return super(HPComwareConfigParser, self)._build_banner(config_line)
@_deprecated(
"HPEConfigParser is deprecated and will be removed in a future version. Use subclasses like HPComwareConfigParser instead."
)
class HPEConfigParser(_HPEConfigParser):
"""Deprecated in favor of internal class _HPEConfigParser."""


class NvidiaOnyxConfigParser(BaseConfigParser): # pylint: disable=abstract-method
Expand Down
42 changes: 42 additions & 0 deletions netutils/config/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
"""Utility functions for working with device configurations."""

import typing as t
import warnings
from functools import wraps


def _open_file_config(cfg_path: str) -> str:
"""Open config file from local disk."""
Expand All @@ -8,3 +12,41 @@ def _open_file_config(cfg_path: str) -> str:
device_cfg = filehandler.read()

return device_cfg.strip()


def _deprecated(custom_message: t.Optional[str] = None) -> t.Callable[[t.Any], t.Any]:
"""Deprecate a function or class.

Args:
custom_message: Custom deprecation message. If None, uses default message.

Returns:
Decorator function that issues a deprecation warning when the decorated item is used.
"""
if custom_message is None:
custom_message = "This function or class is deprecated and will be removed in a future version."

def decorator(obj: t.Any) -> t.Any:
"""Decorator that wraps a class or function to issue deprecation warning."""
if isinstance(obj, type):
# For classes, wrap __init__ to issue warning on instantiation
original_init = getattr(obj, "__init__", None)
if original_init is None:
return obj

def __init__(self: t.Any, *args: t.Any, **kwargs: t.Any) -> None:
warnings.warn(custom_message, DeprecationWarning, stacklevel=2)
original_init(self, *args, **kwargs)

setattr(obj, "__init__", __init__)
return obj

# For functions, wrap the function
@wraps(obj)
def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any:
warnings.warn(custom_message, DeprecationWarning, stacklevel=2)
return obj(*args, **kwargs)

return wrapper

return decorator
Loading