From 20ee6498e5b545c38d478f2877b5acaae86b1cd4 Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Thu, 11 Jun 2026 17:59:31 -0700 Subject: [PATCH] Fix LocalClient.cmd_subset crashing on failed minion probing cmd_subset called sys.list_functions on the targets and then iterated the response with ``if fun in minion_ret[minion]``. When a targeted minion failed to respond the LocalClient.cmd code path records that minion's value as ``False``, which made the membership test raise ``TypeError: argument of type 'bool' is not iterable``. Skip minions whose probe response is not a container so the subset loop transparently passes over failed minions instead of crashing. Fixes #68103 --- changelog/68103.fixed.md | 1 + salt/client/__init__.py | 8 ++++++- tests/pytests/unit/test_client.py | 40 +++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 changelog/68103.fixed.md diff --git a/changelog/68103.fixed.md b/changelog/68103.fixed.md new file mode 100644 index 000000000000..d69a8ef21bd7 --- /dev/null +++ b/changelog/68103.fixed.md @@ -0,0 +1 @@ +Fixed `LocalClient.cmd_subset` raising `TypeError: argument of type 'bool' is not iterable` when one or more targeted minions failed to respond to the `sys.list_functions` probe. Failed minions are now skipped during subset selection. diff --git a/salt/client/__init__.py b/salt/client/__init__.py index a85cf0b158d6..da27e29ed3a2 100644 --- a/salt/client/__init__.py +++ b/salt/client/__init__.py @@ -536,7 +536,13 @@ def cmd_subset( random.shuffle(minions) f_tgt = [] for minion in minions: - if fun in minion_ret[minion]: + # Minions that failed to respond to ``sys.list_functions`` are + # represented as ``False`` (see the failed-minion handling in + # ``cmd``). Skip them rather than letting ``in`` raise. + functions = minion_ret[minion] + if not isinstance(functions, (list, tuple, set, dict)): + continue + if fun in functions: f_tgt.append(minion) if len(f_tgt) >= subset: break diff --git a/tests/pytests/unit/test_client.py b/tests/pytests/unit/test_client.py index f9d5f1035be4..908cee0bc27b 100644 --- a/tests/pytests/unit/test_client.py +++ b/tests/pytests/unit/test_client.py @@ -177,6 +177,46 @@ def test_cmd_subset(salt_master_factory): ) +def test_cmd_subset_skips_failed_minions_68103(salt_master_factory): + """ + Regression test for #68103. + + When ``LocalClient.cmd`` cannot reach a minion it returns ``False`` for + that minion's entry in the result dict. ``cmd_subset`` previously did + ``if fun in minion_ret[minion]`` without checking the value type and + raised ``TypeError: argument of type 'bool' is not iterable``. The + failed minion(s) should simply be skipped. + """ + salt_local_client = salt.client.get_local_client(mopts=salt_master_factory.config) + + # ``cmd_subset`` calls ``random.shuffle`` on the minion list before + # iterating it. Replace shuffle with a no-op so the iteration order is + # deterministic and the failed minion is encountered first. + with patch("salt.client.random.shuffle", lambda x: None), patch( + "salt.client.LocalClient.cmd", + return_value={ + # A minion that did not respond to ``sys.list_functions`` shows + # up in the return as ``False`` (see LocalClient.cmd's failed + # minion handling). + "minion1": False, + "minion2": ["first.func", "second.func"], + }, + ): + with patch("salt.client.LocalClient.cmd_cli") as cmd_cli_mock: + # Should not raise TypeError; failed minion must be skipped. + salt_local_client.cmd_subset("*", "first.func", subset=1, cli=True) + cmd_cli_mock.assert_called_with( + ["minion2"], + "first.func", + (), + progress=False, + kwarg=None, + tgt_type="list", + full_return=False, + ret="", + ) + + @pytest.mark.skip_on_windows(reason="Not supported on Windows") def test_pub(salt_master_factory): """