From 0b37e50e57b58ca0d9ae52b272a241479c768cf6 Mon Sep 17 00:00:00 2001 From: "Gary T. Giesen" Date: Sat, 11 Jul 2026 23:41:55 -0400 Subject: [PATCH 1/3] Fix inline template_source crash and ignored commit_at in napalm_network - load_template ran the salt://-prefix precheck as template_name.startswith(...) even when template_name is None (rendering an inline template_source), crashing with AttributeError. Run that precheck only for a single string template_name; None (inline source) and lists are handled separately. - _config_logic scheduled a commit with get_time_at(time_in=commit_in, time_at=commit_in), so commit_at (commit at an absolute time) was ignored. Pass time_at=commit_at, matching the revert path. --- salt/modules/napalm_network.py | 8 +++- .../unit/modules/napalm/test_network.py | 39 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/salt/modules/napalm_network.py b/salt/modules/napalm_network.py index d760d8319516..fb35b1eb0879 100644 --- a/salt/modules/napalm_network.py +++ b/salt/modules/napalm_network.py @@ -243,7 +243,7 @@ def _config_logic( # and there are changes to commit if commit_in or commit_at: commit_time = __utils__["timeutil.get_time_at"]( - time_in=commit_in, time_at=commit_in + time_in=commit_in, time_at=commit_at ) # schedule job scheduled_job_name = f"__napalm_commit_{current_jid}" @@ -1957,7 +1957,11 @@ def load_template( salt_render_prefixes = ("salt://", "http://", "https://", "ftp://") salt_render = False file_exists = False - if not isinstance(template_name, (tuple, list)): + # Only a single, named template goes through the salt:// / file precheck. + # ``template_name`` is ``None`` when rendering an inline ``template_source``, + # and calling ``None.startswith(...)`` here raised ``AttributeError``; a list + # of names is handled further down. + if isinstance(template_name, str): for salt_render_prefix in salt_render_prefixes: if not salt_render: salt_render = salt_render or template_name.startswith( diff --git a/tests/pytests/unit/modules/napalm/test_network.py b/tests/pytests/unit/modules/napalm/test_network.py index 2ce2d6e621b9..5835856612c8 100644 --- a/tests/pytests/unit/modules/napalm/test_network.py +++ b/tests/pytests/unit/modules/napalm/test_network.py @@ -185,6 +185,45 @@ def test_load_template(): assert ret["out"] is None +def test_load_template_inline_source(): + # Rendering an inline ``template_source`` passes template_name=None; the + # salt:// precheck used to call ``None.startswith`` and crash. + with patch( + "salt.utils.napalm.get_device", + MagicMock(return_value=napalm_test_support.MockNapalmDevice()), + ), patch.dict( + napalm_network.__salt__, + {"file.apply_template_on_contents": MagicMock(return_value="new config")}, + ): + ret = napalm_network.load_template(template_source="system { host-name r1; }") + assert ret["result"] + + +def test_load_config_commit_at_uses_absolute_time(): + # Regression: commit_at was passed to get_time_at as ``time_at=commit_in``, + # so scheduling a commit at an absolute time was silently ignored. + get_time_at = MagicMock(return_value="2026-07-11T02:00:00") + with patch( + "salt.utils.napalm.get_device", + MagicMock(return_value=napalm_test_support.MockNapalmDevice()), + ), patch.dict( + napalm_network.__opts__, {"id": "test-minion"} + ), patch.dict( + napalm_network.__utils__, {"timeutil.get_time_at": get_time_at} + ), patch.dict( + napalm_network.__salt__, + { + "schedule.add": MagicMock(return_value={"result": True, "comment": ""}), + "schedule.save": MagicMock(return_value={"result": True, "comment": ""}), + }, + ): + napalm_network.load_config(text="new config", commit_at="2026-07-11T02:00:00") + get_time_at.assert_called_once() + _, kwargs = get_time_at.call_args + assert kwargs["time_at"] == "2026-07-11T02:00:00" + assert kwargs["time_in"] is None + + def test_commit(): with patch( "salt.utils.napalm.get_device", From eb6b61ae5f41c452b18931898829c1e091510574 Mon Sep 17 00:00:00 2001 From: "Gary T. Giesen" Date: Sat, 11 Jul 2026 23:42:13 -0400 Subject: [PATCH 2/3] Add changelog for #69795 --- changelog/69795.fixed.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelog/69795.fixed.md diff --git a/changelog/69795.fixed.md b/changelog/69795.fixed.md new file mode 100644 index 000000000000..f143cdc18c03 --- /dev/null +++ b/changelog/69795.fixed.md @@ -0,0 +1,5 @@ +Fixed two bugs in the ``napalm_network`` execution module. ``net.load_template`` +no longer crashes with ``AttributeError: 'NoneType' object has no attribute +'startswith'`` when rendering an inline ``template_source`` (no +``template_name``), and ``_config_logic`` now honours ``commit_at`` when +scheduling a commit instead of passing ``commit_in`` for both times. From 9e9347d74e834198975bd35f9ff35d7206b2cbe5 Mon Sep 17 00:00:00 2001 From: "Gary T. Giesen" Date: Sun, 12 Jul 2026 01:11:53 -0400 Subject: [PATCH 3/3] Reformat test_network.py with black --- tests/pytests/unit/modules/napalm/test_network.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/pytests/unit/modules/napalm/test_network.py b/tests/pytests/unit/modules/napalm/test_network.py index 5835856612c8..8243b4604b91 100644 --- a/tests/pytests/unit/modules/napalm/test_network.py +++ b/tests/pytests/unit/modules/napalm/test_network.py @@ -206,9 +206,7 @@ def test_load_config_commit_at_uses_absolute_time(): with patch( "salt.utils.napalm.get_device", MagicMock(return_value=napalm_test_support.MockNapalmDevice()), - ), patch.dict( - napalm_network.__opts__, {"id": "test-minion"} - ), patch.dict( + ), patch.dict(napalm_network.__opts__, {"id": "test-minion"}), patch.dict( napalm_network.__utils__, {"timeutil.get_time_at": get_time_at} ), patch.dict( napalm_network.__salt__,