Skip to content
Open
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
1 change: 1 addition & 0 deletions changelog/57357.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed the ``salt`` CLI exiting 0 in batch mode when the target matched no minions; it now exits 2 ("No return received"), matching the non-batch behavior.
6 changes: 6 additions & 0 deletions salt/cli/salt.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,12 @@ def _run_batch(self):
if job_retcode > retcode:
# Exit with the highest retcode we find
retcode = job_retcode
if not batch.minions:
# No minions matched the target. Mirror the non-batch CLI,
# which prints "No return received" and exits 2 rather than
# silently exiting 0 (#57357).
sys.stderr.write("ERROR: No return received\n")
sys.exit(2)
sys.exit(retcode)

def _print_errors_summary(self, errors):
Expand Down
66 changes: 66 additions & 0 deletions tests/pytests/unit/cli/test_salt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""
Unit tests for the salt CLI (salt.cli.salt.SaltCMD).
"""

import pytest

from salt.cli.salt import SaltCMD
from tests.support.mock import MagicMock, patch


def _fake_saltcmd():
"""
A stand-in SaltCMD self with just the attributes _run_batch's non-static
branch touches, so the method can be exercised without full CLI parsing.
"""
fake = MagicMock()
fake.config = {}
fake.options.eauth = ""
fake.options.static = False
fake.options.batch = "100%"
return fake


def _run_batch_exit_code(fake_batch):
fake = _fake_saltcmd()
with patch("salt.cli.batch.Batch", return_value=fake_batch):
with pytest.raises(SystemExit) as exc:
SaltCMD._run_batch(fake)
return exc.value.code


def test_run_batch_no_minions_exits_nonzero():
"""
Regression test for #57357.

When a batch run matches zero minions, ``batch.run()`` yields nothing. The
CLI must exit non-zero -- matching the non-batch path, which prints
"No return received" and exits 2 -- instead of silently exiting 0. Pins the
bug: before the fix the empty loop leaves ``retcode=0`` and the CLI exits 0.
"""
fake_batch = MagicMock()
fake_batch.run.return_value = iter([])
fake_batch.minions = []
assert _run_batch_exit_code(fake_batch) == 2


def test_run_batch_matched_minions_uses_highest_job_retcode():
"""
Inverse of #57357: when minions match, the exit code is the highest job
retcode seen, and the no-return path is not taken.
"""
fake_batch = MagicMock()
fake_batch.run.return_value = iter([({"m1": {}}, 0), ({"m2": {}}, 3)])
fake_batch.minions = ["m1", "m2"]
assert _run_batch_exit_code(fake_batch) == 3


def test_run_batch_matched_minions_success_exits_zero():
"""
A successful batch that matched minions still exits 0 -- the fix must not
regress the normal path.
"""
fake_batch = MagicMock()
fake_batch.run.return_value = iter([({"m1": {}}, 0)])
fake_batch.minions = ["m1"]
assert _run_batch_exit_code(fake_batch) == 0
Loading