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
6 changes: 6 additions & 0 deletions docs/admin/release_notes/version_1.17.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@

This document describes all new features and changes in the release. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [v1.17.1 (2026-02-04)](https://github.com/networktocode/netutils/releases/tag/v1.17.1)

### Fixed

- [#803](https://github.com/networktocode/netutils/issues/803) - Fixed an issue where an empty config would raise an error when parsing Palo Alto Networks PanOS.

## [v1.17.0 (2026-01-30)](https://github.com/networktocode/netutils/releases/tag/v1.17.0)

### Added
Expand Down
6 changes: 4 additions & 2 deletions netutils/config/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -1553,12 +1553,14 @@ def build_config_relationship(self) -> t.List[ConfigLine]: # pylint: disable=to
True
"""
if self.config_lines_only is None:
raise ValueError("Config is empty.")
return []
config_lines = self.config_lines_only.splitlines()
if not config_lines:
return []

if "@dirtyId" in self.config_lines_only:
# We have to specifically check for JSON format because it can be confused with the brace format
raise ValueError("Found 'json' configuration format. Please provide in 'set' or 'default' (brace) format.")
config_lines = self.config_lines_only.splitlines()
if any(line.endswith("{") for line in config_lines):
converted_config = paloalto_panos_brace_to_set(cfg=self.config, cfg_type="string")
list_config = converted_config.splitlines()
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "netutils"
version = "1.17.0"
version = "1.17.1"
description = "Common helper functions useful in network automation."
authors = ["Network to Code, LLC <opensource@networktocode.com>"]
license = "Apache-2.0"
Expand Down
31 changes: 20 additions & 11 deletions tests/unit/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,18 @@
base_parameters = []
find_all_children_parameters = []
find_children_w_parents_parameters = []
for network_os in list(compliance.parser_map.keys()):
for _file in glob.glob(f"{MOCK_DIR}/base/{network_os}/*{TXT_FILE}"):
base_parameters.append([_file, network_os])
for _file in glob.glob(f"{MOCK_DIR}/find_all_children/{network_os}/*{TXT_FILE}"):
find_all_children_parameters.append([_file, network_os])
for _file in glob.glob(f"{MOCK_DIR}/find_children_w_parents/{network_os}/*{TXT_FILE}"):
find_children_w_parents_parameters.append([_file, network_os])
all_network_os = list(compliance.parser_map.keys())
for _network_os in all_network_os:
for _file in glob.glob(f"{MOCK_DIR}/base/{_network_os}/*{TXT_FILE}"):
base_parameters.append([_file, _network_os])
for _file in glob.glob(f"{MOCK_DIR}/find_all_children/{_network_os}/*{TXT_FILE}"):
find_all_children_parameters.append([_file, _network_os])
for _file in glob.glob(f"{MOCK_DIR}/find_children_w_parents/{_network_os}/*{TXT_FILE}"):
find_children_w_parents_parameters.append([_file, _network_os])


@pytest.mark.parametrize("_file, network_os", base_parameters)
def test_parser(_file, network_os, get_text_data, get_python_data): # pylint: disable=redefined-outer-name
def test_parser(_file, network_os, get_text_data, get_python_data):
truncate_file = os.path.join(MOCK_DIR, "base", _file[: -len(TXT_FILE)])

device_cfg = get_text_data(os.path.join(MOCK_DIR, "base", _file))
Expand All @@ -33,7 +34,7 @@ def test_parser(_file, network_os, get_text_data, get_python_data): # pylint: d


@pytest.mark.parametrize("_file, network_os", find_all_children_parameters)
def test_find_all_children(_file, network_os, get_text_data, get_json_data): # pylint: disable=redefined-outer-name
def test_find_all_children(_file, network_os, get_text_data, get_json_data):
truncate_file = os.path.join(MOCK_DIR, "find_all_children", _file[: -len(TXT_FILE)])

device_cfg = get_text_data(os.path.join(MOCK_DIR, "find_all_children", _file))
Expand All @@ -44,7 +45,7 @@ def test_find_all_children(_file, network_os, get_text_data, get_json_data): #


@pytest.mark.parametrize("_file, network_os", find_children_w_parents_parameters)
def test_find_children_w_parents(_file, network_os, get_text_data, get_json_data): # pylint: disable=redefined-outer-name
def test_find_children_w_parents(_file, network_os, get_text_data, get_json_data):
truncate_file = os.path.join(MOCK_DIR, "find_children_w_parents", _file[: -len(TXT_FILE)])

device_cfg = get_text_data(os.path.join(MOCK_DIR, "find_children_w_parents", _file))
Expand Down Expand Up @@ -81,7 +82,7 @@ def test_duplicate_line():


@pytest.mark.parametrize("network_os", ["cisco_ios", "arista_eos", "cisco_iosxr"])
def test_leading_spaces_config_start(network_os): # pylint: disable=redefined-outer-name
def test_leading_spaces_config_start(network_os):
logging = (
"! Command: show running-config\n"
" 24.1.4\n"
Expand All @@ -95,3 +96,11 @@ def test_leading_spaces_config_start(network_os): # pylint: disable=redefined-o
)
with pytest.raises(IndexError, match=r".*Validate the first line does not begin with a space.*"):
compliance.parser_map[network_os](logging).config_lines # pylint: disable=expression-not-assigned


@pytest.mark.parametrize("network_os", all_network_os)
def test_empty_config(network_os):
"""Test that an empty config returns an empty list and does not raise an error."""
config = ""
os_parser = compliance.parser_map[network_os]
assert os_parser(config).config_lines == []