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
5 changes: 5 additions & 0 deletions changelog/69795.fixed.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 6 additions & 2 deletions salt/modules/napalm_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down Expand Up @@ -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(
Expand Down
37 changes: 37 additions & 0 deletions tests/pytests/unit/modules/napalm/test_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,43 @@ 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",
Expand Down
Loading