From 5317af0be1e77452683e892e677eea798636ca31 Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Tue, 18 Aug 2026 10:30:11 +0700 Subject: [PATCH] fix(config): drop the uptime line from get_config() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. --- napalm_netgear/netgear_smart.py | 31 ++++++++++++++-- tests/unit/test_driver.py | 64 +++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/napalm_netgear/netgear_smart.py b/napalm_netgear/netgear_smart.py index b989a8f..8449e8e 100644 --- a/napalm_netgear/netgear_smart.py +++ b/napalm_netgear/netgear_smart.py @@ -821,6 +821,29 @@ class NetgearSmartDriver(ConfigLifecycleMixin, SwitchDriver): 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( self, retrieve: str = "all", @@ -836,10 +859,14 @@ class NetgearSmartDriver(ConfigLifecycleMixin, SwitchDriver): configs = {"running": "", "startup": "", "candidate": ""} 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"): - configs["startup"] = self._send_paged_command("show startup-config") + configs["startup"] = self._strip_runtime_state( + self._send_paged_command("show startup-config") + ) if sanitized: configs = napalm_helpers.sanitize_configs(configs, C.CISCO_SANITIZE_FILTERS) diff --git a/tests/unit/test_driver.py b/tests/unit/test_driver.py index f6abb14..3b11f73 100644 --- a/tests/unit/test_driver.py +++ b/tests/unit/test_driver.py @@ -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: def test_running_retrieved(self, driver): driver._send_paged_command = lambda cmd: "! running config"