fix(config): drop the uptime line from get_config()
`show running-config` is preceded by a header block, and one of its lines reports the system uptime. That value necessarily differs between any two reads, so every caller comparing consecutive configs sees a change each time. Measured on a GS110TPv3 under NetOrk: 876 of 894 stored config snapshots marked as changed, one git commit and one config_changed warning per poll, while every other device at the same installation sat between 2 and 18. The history was worthless for that switch — a genuine change would have been invisible among hundreds of uptime diffs. Only the uptime line is removed. Model, firmware version, serial and MAC are stable and belong in a config backup; a changed firmware version is exactly the kind of change worth recording. netgear_plus is unaffected — its get_config() returns empty strings. The first read after this lands reports one real change, since the line disappears from the stored config. That is unavoidable and happens once.
This commit is contained in:
@@ -821,6 +821,29 @@ class NetgearSmartDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
|
|
||||||
return interfaces_ip
|
return interfaces_ip
|
||||||
|
|
||||||
|
_RUNTIME_STATE_LINE = re.compile(r"^\s*!\s*System Up Time:", re.IGNORECASE)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _strip_runtime_state(cls, config: str) -> str:
|
||||||
|
"""Drop lines describing state rather than configuration.
|
||||||
|
|
||||||
|
``show running-config`` is preceded by a header block, and one of its
|
||||||
|
lines reports the system uptime. That value necessarily differs
|
||||||
|
between any two reads, so a caller diffing consecutive configs sees a
|
||||||
|
change every single time — on a GS110TPv3 that marked 876 of 894
|
||||||
|
stored snapshots as changed and made the history useless.
|
||||||
|
|
||||||
|
Only the uptime line goes. The rest of the header (model, firmware,
|
||||||
|
serial, MAC) is stable, and a changed firmware version is precisely
|
||||||
|
the kind of change a config backup should record.
|
||||||
|
"""
|
||||||
|
if not config:
|
||||||
|
return config
|
||||||
|
kept = [
|
||||||
|
ln for ln in config.splitlines(keepends=True) if not cls._RUNTIME_STATE_LINE.match(ln)
|
||||||
|
]
|
||||||
|
return "".join(kept)
|
||||||
|
|
||||||
def get_config(
|
def get_config(
|
||||||
self,
|
self,
|
||||||
retrieve: str = "all",
|
retrieve: str = "all",
|
||||||
@@ -836,10 +859,14 @@ class NetgearSmartDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
configs = {"running": "", "startup": "", "candidate": ""}
|
configs = {"running": "", "startup": "", "candidate": ""}
|
||||||
|
|
||||||
if retrieve in ("all", "running"):
|
if retrieve in ("all", "running"):
|
||||||
configs["running"] = self._send_paged_command("show running-config")
|
configs["running"] = self._strip_runtime_state(
|
||||||
|
self._send_paged_command("show running-config")
|
||||||
|
)
|
||||||
|
|
||||||
if retrieve in ("all", "startup"):
|
if retrieve in ("all", "startup"):
|
||||||
configs["startup"] = self._send_paged_command("show startup-config")
|
configs["startup"] = self._strip_runtime_state(
|
||||||
|
self._send_paged_command("show startup-config")
|
||||||
|
)
|
||||||
|
|
||||||
if sanitized:
|
if sanitized:
|
||||||
configs = napalm_helpers.sanitize_configs(configs, C.CISCO_SANITIZE_FILTERS)
|
configs = napalm_helpers.sanitize_configs(configs, C.CISCO_SANITIZE_FILTERS)
|
||||||
|
|||||||
@@ -587,6 +587,70 @@ exit
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
_RUNNING_CONFIG_WITH_HEADER = """\
|
||||||
|
show running-config
|
||||||
|
SYSTEM CONFIG FILE ::= BEGIN
|
||||||
|
! Model: GS110TPv3
|
||||||
|
! Firmware Version: 7.1.1.17 [Apr 16 2026 - 17:26:24]
|
||||||
|
! MAC Address: 28:94:01:6D:26:7D
|
||||||
|
! Serial Number: 7LE4535SA0117
|
||||||
|
! System Up Time: 3 days, 9 hours, 22 mins, 45 secs
|
||||||
|
!
|
||||||
|
vlan database
|
||||||
|
vlan 10
|
||||||
|
exit
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetConfigDropsRuntimeState:
|
||||||
|
"""`show running-config` prefixes a header block that includes uptime.
|
||||||
|
|
||||||
|
Uptime is state, not configuration: it differs between any two reads, so
|
||||||
|
every caller diffing consecutive results sees a change each time. On a
|
||||||
|
real GS110TPv3 that marked 876 of 894 config snapshots as changed and
|
||||||
|
buried any genuine change in the noise.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_uptime_line_is_dropped(self, driver):
|
||||||
|
driver._send_paged_command = lambda cmd: _RUNNING_CONFIG_WITH_HEADER
|
||||||
|
cfg = driver.get_config(retrieve="running")
|
||||||
|
assert "System Up Time" not in cfg["running"]
|
||||||
|
|
||||||
|
def test_two_reads_with_different_uptimes_are_equal(self, driver):
|
||||||
|
"""The whole point: consecutive reads must compare equal."""
|
||||||
|
driver._send_paged_command = lambda cmd: _RUNNING_CONFIG_WITH_HEADER
|
||||||
|
first = driver.get_config(retrieve="running")["running"]
|
||||||
|
|
||||||
|
later = _RUNNING_CONFIG_WITH_HEADER.replace(
|
||||||
|
"3 days, 9 hours, 22 mins, 45 secs", "3 days, 9 hours, 27 mins, 51 secs"
|
||||||
|
)
|
||||||
|
driver._send_paged_command = lambda cmd: later
|
||||||
|
assert driver.get_config(retrieve="running")["running"] == first
|
||||||
|
|
||||||
|
def test_stable_header_lines_are_kept(self, driver):
|
||||||
|
"""Firmware, model and serial belong in a config backup."""
|
||||||
|
driver._send_paged_command = lambda cmd: _RUNNING_CONFIG_WITH_HEADER
|
||||||
|
running = driver.get_config(retrieve="running")["running"]
|
||||||
|
assert "! Model: GS110TPv3" in running
|
||||||
|
assert "! Firmware Version: 7.1.1.17 [Apr 16 2026 - 17:26:24]" in running
|
||||||
|
assert "! Serial Number: 7LE4535SA0117" in running
|
||||||
|
|
||||||
|
def test_actual_configuration_survives(self, driver):
|
||||||
|
driver._send_paged_command = lambda cmd: _RUNNING_CONFIG_WITH_HEADER
|
||||||
|
running = driver.get_config(retrieve="running")["running"]
|
||||||
|
assert "vlan 10" in running
|
||||||
|
|
||||||
|
def test_startup_config_is_filtered_too(self, driver):
|
||||||
|
driver._send_paged_command = lambda cmd: _RUNNING_CONFIG_WITH_HEADER
|
||||||
|
cfg = driver.get_config(retrieve="startup")
|
||||||
|
assert "System Up Time" not in cfg["startup"]
|
||||||
|
|
||||||
|
def test_config_without_the_header_is_untouched(self, driver):
|
||||||
|
plain = "vlan database\nvlan 10\nexit\n"
|
||||||
|
driver._send_paged_command = lambda cmd: plain
|
||||||
|
assert driver.get_config(retrieve="running")["running"] == plain
|
||||||
|
|
||||||
|
|
||||||
class TestGetConfig:
|
class TestGetConfig:
|
||||||
def test_running_retrieved(self, driver):
|
def test_running_retrieved(self, driver):
|
||||||
driver._send_paged_command = lambda cmd: "! running config"
|
driver._send_paged_command = lambda cmd: "! running config"
|
||||||
|
|||||||
Reference in New Issue
Block a user