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:
Christian Manivong
2026-08-18 10:30:11 +07:00
parent 20460e7a5a
commit 5317af0be1
2 changed files with 93 additions and 2 deletions
+29 -2
View File
@@ -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)
+64
View File
@@ -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"