diff --git a/changelog/66592.fixed.md b/changelog/66592.fixed.md new file mode 100644 index 000000000000..228e35292b0b --- /dev/null +++ b/changelog/66592.fixed.md @@ -0,0 +1 @@ +Fix minion config option startup_states diff --git a/salt/minion.py b/salt/minion.py index 6cfca07d29ca..c1928853ec8e 100644 --- a/salt/minion.py +++ b/salt/minion.py @@ -1146,7 +1146,7 @@ async def _connect_minion(self, minion): minion.setup_scheduler(before_connect=True) if minion.opts.get("master_type", "str") != "disable": await minion.connect_master(failed=failed) - minion.tune_in(start=False) + await minion.tune_in(start=False) self.minions.append(minion) break except SaltClientError as exc: @@ -2407,7 +2407,7 @@ def timeout_handler(*_): log.trace("ret_val = %s", ret_val) # pylint: disable=no-member return ret_val - def _state_run(self): + async def _state_run(self): """ Execute a state run based on information set in the minion config file """ @@ -2432,7 +2432,7 @@ def _state_run(self): else: data["fun"] = "state.highstate" data["arg"] = [] - self._handle_decoded_payload(data) + await self._handle_decoded_payload(data) def _refresh_grains_watcher(self, refresh_interval_in_minutes): """ @@ -3111,7 +3111,7 @@ def remove_periodic_callback(self, name): return True # Main Minion Tune In - def tune_in(self, start=True): + async def tune_in(self, start=True): """ Lock onto the publisher. This is the main event loop for the minion :rtype : None @@ -3138,7 +3138,7 @@ def tune_in(self, start=True): salt.utils.win_functions.enable_ctrl_logoff_handler() # On first startup execute a state run if configured to do so - self._state_run() + await self._state_run() self.setup_beacons() self.setup_scheduler() diff --git a/tests/pytests/integration/minion/test_startup_states.py b/tests/pytests/integration/minion/test_startup_states.py new file mode 100644 index 000000000000..d3bc22041615 --- /dev/null +++ b/tests/pytests/integration/minion/test_startup_states.py @@ -0,0 +1,114 @@ +"""Test minion configuration option startup_states. + +There are four valid values for this option, which are validated by checking the jobs +executed after minion start. +""" + +import pytest + + +@pytest.fixture +def salt_minion_startup_states_empty_string(salt_master, salt_minion_id): + config_overrides = { + "startup_states": "", + } + factory = salt_master.salt_minion_daemon( + f"{salt_minion_id}-empty-string", + overrides=config_overrides, + ) + with factory.started(): + yield factory + + +@pytest.fixture +def salt_minion_startup_states_highstate(salt_master, salt_minion_id): + config_overrides = { + "startup_states": "highstate", + } + factory = salt_master.salt_minion_daemon( + f"{salt_minion_id}-highstate", + overrides=config_overrides, + ) + with factory.started(): + yield factory + + +@pytest.fixture +def salt_minion_startup_states_sls(salt_master, salt_minion_id): + config_overrides = {"startup_states": "sls", "sls_list": ["example-sls"]} + factory = salt_master.salt_minion_daemon( + f"{salt_minion_id}-sls", + overrides=config_overrides, + ) + with factory.started(): + yield factory + + +@pytest.fixture +def salt_minion_startup_states_top(salt_master, salt_minion_id): + config_overrides = {"startup_states": "top", "top_file": "example-top.sls"} + factory = salt_master.salt_minion_daemon( + f"{salt_minion_id}-top", + overrides=config_overrides, + ) + with factory.started(): + yield factory + + +def test_startup_states_empty_string( + salt_run_cli, salt_minion_startup_states_empty_string +): + # Get jobs for this minion + ret = salt_run_cli.run( + "jobs.list_jobs", f"search_target={salt_minion_startup_states_empty_string.id}" + ) + # Check no job was run + assert len(ret.data.keys()) == 0 + + +def test_startup_states_highstate(salt_run_cli, salt_minion_startup_states_highstate): + with salt_minion_startup_states_highstate: + # Get jobs for this minion + ret = salt_run_cli.run( + "jobs.list_jobs", f"search_target={salt_minion_startup_states_highstate.id}" + ) + # Check there is exactly one job + assert len(ret.data.keys()) == 1 + # Check that job executes state.highstate + job_ret = next(iter(ret.data.values())) + assert "Function" in job_ret + assert job_ret["Function"] == "state.highstate" + assert "Arguments" in job_ret + assert job_ret["Arguments"] == [] + + +def test_startup_states_sls(salt_run_cli, salt_minion_startup_states_sls): + with salt_minion_startup_states_sls: + # Get jobs for this minion + ret = salt_run_cli.run( + "jobs.list_jobs", f"search_target={salt_minion_startup_states_sls.id}" + ) + # Check there is exactly one job + assert len(ret.data.keys()) == 1 + # Check that job executes state.sls + job_ret = next(iter(ret.data.values())) + assert "Function" in job_ret + assert job_ret["Function"] == "state.sls" + assert "Arguments" in job_ret + assert job_ret["Arguments"] == [["example-sls"]] + + +def test_startup_states_top(salt_run_cli, salt_minion_startup_states_top): + with salt_minion_startup_states_top: + # Get jobs for this minion + ret = salt_run_cli.run( + "jobs.list_jobs", f"search_target={salt_minion_startup_states_top.id}" + ) + # Check there is exactly one job + assert len(ret.data.keys()) == 1 + # Check that job executes state.top + job_ret = next(iter(ret.data.values())) + assert "Function" in job_ret + assert job_ret["Function"] == "state.top" + assert "Arguments" in job_ret + assert job_ret["Arguments"] == ["example-top.sls"] diff --git a/tests/pytests/unit/test_minion.py b/tests/pytests/unit/test_minion.py index dfbf0b1d8bdf..e11304d68892 100644 --- a/tests/pytests/unit/test_minion.py +++ b/tests/pytests/unit/test_minion.py @@ -495,7 +495,7 @@ class SleepCalledException(Exception): @pytest.mark.slow_test -def test_beacons_before_connect(minion_opts): +async def test_beacons_before_connect(minion_opts): """ Tests that the 'beacons_before_connect' option causes the beacons to be initialized before connect. """ @@ -515,7 +515,7 @@ def test_beacons_before_connect(minion_opts): try: try: - minion.tune_in(start=True) + await minion.tune_in(start=True) except RuntimeError: pass @@ -527,7 +527,7 @@ def test_beacons_before_connect(minion_opts): @pytest.mark.slow_test -def test_scheduler_before_connect(minion_opts): +async def test_scheduler_before_connect(minion_opts): """ Tests that the 'scheduler_before_connect' option causes the scheduler to be initialized before connect. """ @@ -546,7 +546,7 @@ def test_scheduler_before_connect(minion_opts): minion = salt.minion.Minion(minion_opts, io_loop=io_loop) try: try: - minion.tune_in(start=True) + await minion.tune_in(start=True) except RuntimeError: pass @@ -616,7 +616,7 @@ def test_minion_module_refresh_beacons_refresh(minion_opts): @pytest.mark.slow_test -def test_when_ping_interval_is_set_the_callback_should_be_added_to_periodic_callbacks( +async def test_when_ping_interval_is_set_the_callback_should_be_added_to_periodic_callbacks( minion_opts, ): with patch("salt.minion.Minion.ctx", MagicMock(return_value={})), patch( @@ -636,7 +636,7 @@ def test_when_ping_interval_is_set_the_callback_should_be_added_to_periodic_call try: minion.connected = MagicMock(side_effect=(False, True)) minion._fire_master_minion_start = MagicMock() - minion.tune_in(start=False) + await minion.tune_in(start=False) except RuntimeError: pass