1import copy
2import logging
3import os
4
5import pytest
6import salt.ext.tornado
7import salt.ext.tornado.gen
8import salt.ext.tornado.testing
9import salt.minion
10import salt.syspaths
11import salt.utils.crypt
12import salt.utils.event as event
13import salt.utils.jid
14import salt.utils.platform
15import salt.utils.process
16from salt._compat import ipaddress
17from salt.exceptions import SaltClientError, SaltMasterUnresolvableError, SaltSystemExit
18from tests.support.mock import MagicMock, patch
19
20log = logging.getLogger(__name__)
21
22
23def test_minion_load_grains_false():
24    """
25    Minion does not generate grains when load_grains is False
26    """
27    opts = {"random_startup_delay": 0, "grains": {"foo": "bar"}}
28    with patch("salt.loader.grains") as grainsfunc:
29        minion = salt.minion.Minion(opts, load_grains=False)
30        assert minion.opts["grains"] == opts["grains"]
31        grainsfunc.assert_not_called()
32
33
34def test_minion_load_grains_true():
35    """
36    Minion generates grains when load_grains is True
37    """
38    opts = {"random_startup_delay": 0, "grains": {}}
39    with patch("salt.loader.grains") as grainsfunc:
40        minion = salt.minion.Minion(opts, load_grains=True)
41        assert minion.opts["grains"] != {}
42        grainsfunc.assert_called()
43
44
45def test_minion_load_grains_default():
46    """
47    Minion load_grains defaults to True
48    """
49    opts = {"random_startup_delay": 0, "grains": {}}
50    with patch("salt.loader.grains") as grainsfunc:
51        minion = salt.minion.Minion(opts)
52        assert minion.opts["grains"] != {}
53        grainsfunc.assert_called()
54
55
56@pytest.mark.parametrize(
57    "req_channel",
58    [
59        (
60            "salt.transport.client.AsyncReqChannel.factory",
61            lambda load, timeout, tries: salt.ext.tornado.gen.maybe_future(tries),
62        ),
63        (
64            "salt.transport.client.ReqChannel.factory",
65            lambda load, timeout, tries: tries,
66        ),
67    ],
68)
69def test_send_req_tries(req_channel):
70    channel_enter = MagicMock()
71    channel_enter.send.side_effect = req_channel[1]
72    channel = MagicMock()
73    channel.__enter__.return_value = channel_enter
74
75    with patch(req_channel[0], return_value=channel):
76        opts = {
77            "random_startup_delay": 0,
78            "grains": {},
79            "return_retry_tries": 30,
80            "minion_sign_messages": False,
81        }
82        with patch("salt.loader.grains"):
83            minion = salt.minion.Minion(opts)
84
85            load = {"load": "value"}
86            timeout = 60
87
88            if "Async" in req_channel[0]:
89                rtn = minion._send_req_async(load, timeout).result()
90            else:
91                rtn = minion._send_req_sync(load, timeout)
92
93            assert rtn == 30
94
95
96@patch("salt.transport.client.ReqChannel.factory")
97def test_mine_send_tries(req_channel_factory):
98    channel_enter = MagicMock()
99    channel_enter.send.side_effect = lambda load, timeout, tries: tries
100    channel = MagicMock()
101    channel.__enter__.return_value = channel_enter
102    req_channel_factory.return_value = channel
103
104    opts = {
105        "random_startup_delay": 0,
106        "grains": {},
107        "return_retry_tries": 20,
108        "minion_sign_messages": False,
109    }
110    with patch("salt.loader.grains"):
111        minion = salt.minion.Minion(opts)
112        minion.tok = "token"
113
114        data = {}
115        tag = "tag"
116
117        rtn = minion._mine_send(tag, data)
118        assert rtn == 20
119
120
121def test_invalid_master_address():
122    opts = salt.config.DEFAULT_MINION_OPTS.copy()
123    with patch.dict(
124        opts,
125        {
126            "ipv6": False,
127            "master": float("127.0"),
128            "master_port": "4555",
129            "retry_dns": False,
130        },
131    ):
132        pytest.raises(SaltSystemExit, salt.minion.resolve_dns, opts)
133
134
135def test_source_int_name_local():
136    """
137    test when file_client local and
138    source_interface_name is set
139    """
140    interfaces = {
141        "bond0.1234": {
142            "hwaddr": "01:01:01:d0:d0:d0",
143            "up": True,
144            "inet": [
145                {
146                    "broadcast": "111.1.111.255",
147                    "netmask": "111.1.0.0",
148                    "label": "bond0",
149                    "address": "111.1.0.1",
150                }
151            ],
152        }
153    }
154    opts = salt.config.DEFAULT_MINION_OPTS.copy()
155    with patch.dict(
156        opts,
157        {
158            "ipv6": False,
159            "master": "127.0.0.1",
160            "master_port": "4555",
161            "file_client": "local",
162            "source_interface_name": "bond0.1234",
163            "source_ret_port": 49017,
164            "source_publish_port": 49018,
165        },
166    ), patch("salt.utils.network.interfaces", MagicMock(return_value=interfaces)):
167        assert salt.minion.resolve_dns(opts) == {
168            "master_ip": "127.0.0.1",
169            "source_ip": "111.1.0.1",
170            "source_ret_port": 49017,
171            "source_publish_port": 49018,
172            "master_uri": "tcp://127.0.0.1:4555",
173        }
174
175
176@pytest.mark.slow_test
177def test_source_int_name_remote():
178    """
179    test when file_client remote and
180    source_interface_name is set and
181    interface is down
182    """
183    interfaces = {
184        "bond0.1234": {
185            "hwaddr": "01:01:01:d0:d0:d0",
186            "up": False,
187            "inet": [
188                {
189                    "broadcast": "111.1.111.255",
190                    "netmask": "111.1.0.0",
191                    "label": "bond0",
192                    "address": "111.1.0.1",
193                }
194            ],
195        }
196    }
197    opts = salt.config.DEFAULT_MINION_OPTS.copy()
198    with patch.dict(
199        opts,
200        {
201            "ipv6": False,
202            "master": "127.0.0.1",
203            "master_port": "4555",
204            "file_client": "remote",
205            "source_interface_name": "bond0.1234",
206            "source_ret_port": 49017,
207            "source_publish_port": 49018,
208        },
209    ), patch("salt.utils.network.interfaces", MagicMock(return_value=interfaces)):
210        assert salt.minion.resolve_dns(opts) == {
211            "master_ip": "127.0.0.1",
212            "source_ret_port": 49017,
213            "source_publish_port": 49018,
214            "master_uri": "tcp://127.0.0.1:4555",
215        }
216
217
218@pytest.mark.slow_test
219def test_source_address():
220    """
221    test when source_address is set
222    """
223    interfaces = {
224        "bond0.1234": {
225            "hwaddr": "01:01:01:d0:d0:d0",
226            "up": False,
227            "inet": [
228                {
229                    "broadcast": "111.1.111.255",
230                    "netmask": "111.1.0.0",
231                    "label": "bond0",
232                    "address": "111.1.0.1",
233                }
234            ],
235        }
236    }
237    opts = salt.config.DEFAULT_MINION_OPTS.copy()
238    with patch.dict(
239        opts,
240        {
241            "ipv6": False,
242            "master": "127.0.0.1",
243            "master_port": "4555",
244            "file_client": "local",
245            "source_interface_name": "",
246            "source_address": "111.1.0.1",
247            "source_ret_port": 49017,
248            "source_publish_port": 49018,
249        },
250    ), patch("salt.utils.network.interfaces", MagicMock(return_value=interfaces)):
251        assert salt.minion.resolve_dns(opts) == {
252            "source_publish_port": 49018,
253            "source_ret_port": 49017,
254            "master_uri": "tcp://127.0.0.1:4555",
255            "source_ip": "111.1.0.1",
256            "master_ip": "127.0.0.1",
257        }
258
259
260# Tests for _handle_decoded_payload in the salt.minion.Minion() class: 3
261@pytest.mark.slow_test
262def test_handle_decoded_payload_jid_match_in_jid_queue():
263    """
264    Tests that the _handle_decoded_payload function returns when a jid is given that is already present
265    in the jid_queue.
266
267    Note: This test doesn't contain all of the patch decorators above the function like the other tests
268    for _handle_decoded_payload below. This is essential to this test as the call to the function must
269    return None BEFORE any of the processes are spun up because we should be avoiding firing duplicate
270    jobs.
271    """
272    mock_opts = salt.config.DEFAULT_MINION_OPTS.copy()
273    mock_data = {"fun": "foo.bar", "jid": 123}
274    mock_jid_queue = [123]
275    minion = salt.minion.Minion(
276        mock_opts,
277        jid_queue=copy.copy(mock_jid_queue),
278        io_loop=salt.ext.tornado.ioloop.IOLoop(),
279    )
280    try:
281        ret = minion._handle_decoded_payload(mock_data).result()
282        assert minion.jid_queue == mock_jid_queue
283        assert ret is None
284    finally:
285        minion.destroy()
286
287
288@pytest.mark.slow_test
289def test_handle_decoded_payload_jid_queue_addition():
290    """
291    Tests that the _handle_decoded_payload function adds a jid to the minion's jid_queue when the new
292    jid isn't already present in the jid_queue.
293    """
294    with patch("salt.minion.Minion.ctx", MagicMock(return_value={})), patch(
295        "salt.utils.process.SignalHandlingProcess.start",
296        MagicMock(return_value=True),
297    ), patch(
298        "salt.utils.process.SignalHandlingProcess.join",
299        MagicMock(return_value=True),
300    ):
301        mock_jid = 11111
302        mock_opts = salt.config.DEFAULT_MINION_OPTS.copy()
303        mock_data = {"fun": "foo.bar", "jid": mock_jid}
304        mock_jid_queue = [123, 456]
305        minion = salt.minion.Minion(
306            mock_opts,
307            jid_queue=copy.copy(mock_jid_queue),
308            io_loop=salt.ext.tornado.ioloop.IOLoop(),
309        )
310        try:
311
312            # Assert that the minion's jid_queue attribute matches the mock_jid_queue as a baseline
313            # This can help debug any test failures if the _handle_decoded_payload call fails.
314            assert minion.jid_queue == mock_jid_queue
315
316            # Call the _handle_decoded_payload function and update the mock_jid_queue to include the new
317            # mock_jid. The mock_jid should have been added to the jid_queue since the mock_jid wasn't
318            # previously included. The minion's jid_queue attribute and the mock_jid_queue should be equal.
319            minion._handle_decoded_payload(mock_data).result()
320            mock_jid_queue.append(mock_jid)
321            assert minion.jid_queue == mock_jid_queue
322        finally:
323            minion.destroy()
324
325
326@pytest.mark.slow_test
327def test_handle_decoded_payload_jid_queue_reduced_minion_jid_queue_hwm():
328    """
329    Tests that the _handle_decoded_payload function removes a jid from the minion's jid_queue when the
330    minion's jid_queue high water mark (minion_jid_queue_hwm) is hit.
331    """
332    with patch("salt.minion.Minion.ctx", MagicMock(return_value={})), patch(
333        "salt.utils.process.SignalHandlingProcess.start",
334        MagicMock(return_value=True),
335    ), patch(
336        "salt.utils.process.SignalHandlingProcess.join",
337        MagicMock(return_value=True),
338    ):
339        mock_opts = salt.config.DEFAULT_MINION_OPTS.copy()
340        mock_opts["minion_jid_queue_hwm"] = 2
341        mock_data = {"fun": "foo.bar", "jid": 789}
342        mock_jid_queue = [123, 456]
343        minion = salt.minion.Minion(
344            mock_opts,
345            jid_queue=copy.copy(mock_jid_queue),
346            io_loop=salt.ext.tornado.ioloop.IOLoop(),
347        )
348        try:
349
350            # Assert that the minion's jid_queue attribute matches the mock_jid_queue as a baseline
351            # This can help debug any test failures if the _handle_decoded_payload call fails.
352            assert minion.jid_queue == mock_jid_queue
353
354            # Call the _handle_decoded_payload function and check that the queue is smaller by one item
355            # and contains the new jid
356            minion._handle_decoded_payload(mock_data).result()
357            assert len(minion.jid_queue) == 2
358            assert minion.jid_queue == [456, 789]
359        finally:
360            minion.destroy()
361
362
363@pytest.mark.slow_test
364def test_process_count_max():
365    """
366    Tests that the _handle_decoded_payload function does not spawn more than the configured amount of processes,
367    as per process_count_max.
368    """
369    with patch("salt.minion.Minion.ctx", MagicMock(return_value={})), patch(
370        "salt.utils.process.SignalHandlingProcess.start",
371        MagicMock(return_value=True),
372    ), patch(
373        "salt.utils.process.SignalHandlingProcess.join",
374        MagicMock(return_value=True),
375    ), patch(
376        "salt.utils.minion.running", MagicMock(return_value=[])
377    ), patch(
378        "salt.ext.tornado.gen.sleep",
379        MagicMock(return_value=salt.ext.tornado.concurrent.Future()),
380    ):
381        process_count_max = 10
382        mock_opts = salt.config.DEFAULT_MINION_OPTS.copy()
383        mock_opts["__role"] = "minion"
384        mock_opts["minion_jid_queue_hwm"] = 100
385        mock_opts["process_count_max"] = process_count_max
386
387        io_loop = salt.ext.tornado.ioloop.IOLoop()
388        minion = salt.minion.Minion(mock_opts, jid_queue=[], io_loop=io_loop)
389        try:
390
391            # mock gen.sleep to throw a special Exception when called, so that we detect it
392            class SleepCalledException(Exception):
393                """Thrown when sleep is called"""
394
395            salt.ext.tornado.gen.sleep.return_value.set_exception(
396                SleepCalledException()
397            )
398
399            # up until process_count_max: gen.sleep does not get called, processes are started normally
400            for i in range(process_count_max):
401                mock_data = {"fun": "foo.bar", "jid": i}
402                io_loop.run_sync(
403                    lambda data=mock_data: minion._handle_decoded_payload(data)
404                )
405                assert (
406                    salt.utils.process.SignalHandlingProcess.start.call_count == i + 1
407                )
408                assert len(minion.jid_queue) == i + 1
409                salt.utils.minion.running.return_value += [i]
410
411            # above process_count_max: gen.sleep does get called, JIDs are created but no new processes are started
412            mock_data = {"fun": "foo.bar", "jid": process_count_max + 1}
413
414            pytest.raises(
415                SleepCalledException,
416                lambda: io_loop.run_sync(
417                    lambda: minion._handle_decoded_payload(mock_data)
418                ),
419            )
420            assert (
421                salt.utils.process.SignalHandlingProcess.start.call_count
422                == process_count_max
423            )
424            assert len(minion.jid_queue) == process_count_max + 1
425        finally:
426            minion.destroy()
427
428
429@pytest.mark.slow_test
430def test_beacons_before_connect():
431    """
432    Tests that the 'beacons_before_connect' option causes the beacons to be initialized before connect.
433    """
434    with patch("salt.minion.Minion.ctx", MagicMock(return_value={})), patch(
435        "salt.minion.Minion.sync_connect_master",
436        MagicMock(side_effect=RuntimeError("stop execution")),
437    ), patch(
438        "salt.utils.process.SignalHandlingProcess.start",
439        MagicMock(return_value=True),
440    ), patch(
441        "salt.utils.process.SignalHandlingProcess.join",
442        MagicMock(return_value=True),
443    ):
444        mock_opts = salt.config.DEFAULT_MINION_OPTS.copy()
445        mock_opts["beacons_before_connect"] = True
446        io_loop = salt.ext.tornado.ioloop.IOLoop()
447        io_loop.make_current()
448        minion = salt.minion.Minion(mock_opts, io_loop=io_loop)
449        try:
450
451            try:
452                minion.tune_in(start=True)
453            except RuntimeError:
454                pass
455
456            # Make sure beacons are initialized but the sheduler is not
457            assert "beacons" in minion.periodic_callbacks
458            assert "schedule" not in minion.periodic_callbacks
459        finally:
460            minion.destroy()
461
462
463@pytest.mark.slow_test
464def test_scheduler_before_connect():
465    """
466    Tests that the 'scheduler_before_connect' option causes the scheduler to be initialized before connect.
467    """
468    with patch("salt.minion.Minion.ctx", MagicMock(return_value={})), patch(
469        "salt.minion.Minion.sync_connect_master",
470        MagicMock(side_effect=RuntimeError("stop execution")),
471    ), patch(
472        "salt.utils.process.SignalHandlingProcess.start",
473        MagicMock(return_value=True),
474    ), patch(
475        "salt.utils.process.SignalHandlingProcess.join",
476        MagicMock(return_value=True),
477    ):
478        mock_opts = salt.config.DEFAULT_MINION_OPTS.copy()
479        mock_opts["scheduler_before_connect"] = True
480        io_loop = salt.ext.tornado.ioloop.IOLoop()
481        io_loop.make_current()
482        minion = salt.minion.Minion(mock_opts, io_loop=io_loop)
483        try:
484            try:
485                minion.tune_in(start=True)
486            except RuntimeError:
487                pass
488
489            # Make sure the scheduler is initialized but the beacons are not
490            assert "schedule" in minion.periodic_callbacks
491            assert "beacons" not in minion.periodic_callbacks
492        finally:
493            minion.destroy()
494
495
496def test_minion_module_refresh():
497    """
498    Tests that the 'module_refresh' just return in case there is no 'schedule'
499    because destroy method was already called.
500    """
501    with patch("salt.minion.Minion.ctx", MagicMock(return_value={})), patch(
502        "salt.utils.process.SignalHandlingProcess.start",
503        MagicMock(return_value=True),
504    ), patch(
505        "salt.utils.process.SignalHandlingProcess.join",
506        MagicMock(return_value=True),
507    ):
508        try:
509            mock_opts = salt.config.DEFAULT_MINION_OPTS.copy()
510            minion = salt.minion.Minion(
511                mock_opts,
512                io_loop=salt.ext.tornado.ioloop.IOLoop(),
513            )
514            minion.schedule = salt.utils.schedule.Schedule(mock_opts, {}, returners={})
515            assert hasattr(minion, "schedule")
516            minion.destroy()
517            assert not hasattr(minion, "schedule")
518            assert not minion.module_refresh()
519        finally:
520            minion.destroy()
521
522
523def test_minion_module_refresh_beacons_refresh():
524    """
525    Tests that 'module_refresh' calls beacons_refresh and that the
526    minion object has a beacons attribute with beacons.
527    """
528    with patch("salt.minion.Minion.ctx", MagicMock(return_value={})), patch(
529        "salt.utils.process.SignalHandlingProcess.start",
530        MagicMock(return_value=True),
531    ), patch(
532        "salt.utils.process.SignalHandlingProcess.join",
533        MagicMock(return_value=True),
534    ):
535        try:
536            mock_opts = salt.config.DEFAULT_MINION_OPTS.copy()
537            minion = salt.minion.Minion(
538                mock_opts,
539                io_loop=salt.ext.tornado.ioloop.IOLoop(),
540            )
541            minion.schedule = salt.utils.schedule.Schedule(mock_opts, {}, returners={})
542            assert not hasattr(minion, "beacons")
543            minion.module_refresh()
544            assert hasattr(minion, "beacons")
545            assert hasattr(minion.beacons, "beacons")
546            assert "service.beacon" in minion.beacons.beacons
547            minion.destroy()
548        finally:
549            minion.destroy()
550
551
552@pytest.mark.slow_test
553def test_when_ping_interval_is_set_the_callback_should_be_added_to_periodic_callbacks():
554    with patch("salt.minion.Minion.ctx", MagicMock(return_value={})), patch(
555        "salt.minion.Minion.sync_connect_master",
556        MagicMock(side_effect=RuntimeError("stop execution")),
557    ), patch(
558        "salt.utils.process.SignalHandlingProcess.start",
559        MagicMock(return_value=True),
560    ), patch(
561        "salt.utils.process.SignalHandlingProcess.join",
562        MagicMock(return_value=True),
563    ):
564        mock_opts = salt.config.DEFAULT_MINION_OPTS.copy()
565        mock_opts["ping_interval"] = 10
566        io_loop = salt.ext.tornado.ioloop.IOLoop()
567        io_loop.make_current()
568        minion = salt.minion.Minion(mock_opts, io_loop=io_loop)
569        try:
570            try:
571                minion.connected = MagicMock(side_effect=(False, True))
572                minion._fire_master_minion_start = MagicMock()
573                minion.tune_in(start=False)
574            except RuntimeError:
575                pass
576
577            # Make sure the scheduler is initialized but the beacons are not
578            assert "ping" in minion.periodic_callbacks
579        finally:
580            minion.destroy()
581
582
583@pytest.mark.slow_test
584def test_when_passed_start_event_grains():
585    mock_opts = salt.config.DEFAULT_MINION_OPTS.copy()
586    # provide mock opts an os grain since we'll look for it later.
587    mock_opts["grains"]["os"] = "linux"
588    mock_opts["start_event_grains"] = ["os"]
589    io_loop = salt.ext.tornado.ioloop.IOLoop()
590    io_loop.make_current()
591    minion = salt.minion.Minion(mock_opts, io_loop=io_loop)
592    try:
593        minion.tok = MagicMock()
594        minion._send_req_sync = MagicMock()
595        minion._fire_master(
596            "Minion has started", "minion_start", include_startup_grains=True
597        )
598        load = minion._send_req_sync.call_args[0][0]
599
600        assert "grains" in load
601        assert "os" in load["grains"]
602    finally:
603        minion.destroy()
604
605
606@pytest.mark.slow_test
607def test_when_not_passed_start_event_grains():
608    mock_opts = salt.config.DEFAULT_MINION_OPTS.copy()
609    io_loop = salt.ext.tornado.ioloop.IOLoop()
610    io_loop.make_current()
611    minion = salt.minion.Minion(mock_opts, io_loop=io_loop)
612    try:
613        minion.tok = MagicMock()
614        minion._send_req_sync = MagicMock()
615        minion._fire_master("Minion has started", "minion_start")
616        load = minion._send_req_sync.call_args[0][0]
617
618        assert "grains" not in load
619    finally:
620        minion.destroy()
621
622
623@pytest.mark.slow_test
624def test_when_other_events_fired_and_start_event_grains_are_set():
625    mock_opts = salt.config.DEFAULT_MINION_OPTS.copy()
626    mock_opts["start_event_grains"] = ["os"]
627    io_loop = salt.ext.tornado.ioloop.IOLoop()
628    io_loop.make_current()
629    minion = salt.minion.Minion(mock_opts, io_loop=io_loop)
630    try:
631        minion.tok = MagicMock()
632        minion._send_req_sync = MagicMock()
633        minion._fire_master("Custm_event_fired", "custom_event")
634        load = minion._send_req_sync.call_args[0][0]
635
636        assert "grains" not in load
637    finally:
638        minion.destroy()
639
640
641@pytest.mark.slow_test
642def test_minion_retry_dns_count():
643    """
644    Tests that the resolve_dns will retry dns look ups for a maximum of
645    3 times before raising a SaltMasterUnresolvableError exception.
646    """
647    opts = salt.config.DEFAULT_MINION_OPTS.copy()
648    with patch.dict(
649        opts,
650        {
651            "ipv6": False,
652            "master": "dummy",
653            "master_port": "4555",
654            "retry_dns": 1,
655            "retry_dns_count": 3,
656        },
657    ):
658        pytest.raises(SaltMasterUnresolvableError, salt.minion.resolve_dns, opts)
659
660
661@pytest.mark.slow_test
662def test_gen_modules_executors():
663    """
664    Ensure gen_modules is called with the correct arguments #54429
665    """
666    mock_opts = salt.config.DEFAULT_MINION_OPTS.copy()
667    io_loop = salt.ext.tornado.ioloop.IOLoop()
668    io_loop.make_current()
669    minion = salt.minion.Minion(mock_opts, io_loop=io_loop)
670
671    class MockPillarCompiler:
672        def compile_pillar(self):
673            return {}
674
675    try:
676        with patch("salt.pillar.get_pillar", return_value=MockPillarCompiler()):
677            with patch("salt.loader.executors") as execmock:
678                minion.gen_modules()
679        assert execmock.called_with(minion.opts, minion.functions)
680    finally:
681        minion.destroy()
682
683
684@patch("salt.utils.process.default_signals")
685@pytest.mark.slow_test
686def test_reinit_crypto_on_fork(def_mock):
687    """
688    Ensure salt.utils.crypt.reinit_crypto() is executed when forking for new job
689    """
690    mock_opts = salt.config.DEFAULT_MINION_OPTS.copy()
691    mock_opts["multiprocessing"] = True
692
693    io_loop = salt.ext.tornado.ioloop.IOLoop()
694    io_loop.make_current()
695    minion = salt.minion.Minion(mock_opts, io_loop=io_loop)
696
697    job_data = {"jid": "test-jid", "fun": "test.ping"}
698
699    def mock_start(self):
700        # pylint: disable=comparison-with-callable
701        assert (
702            len(
703                [
704                    x
705                    for x in self._after_fork_methods
706                    if x[0] == salt.utils.crypt.reinit_crypto
707                ]
708            )
709            == 1
710        )
711        # pylint: enable=comparison-with-callable
712
713    with patch.object(salt.utils.process.SignalHandlingProcess, "start", mock_start):
714        io_loop.run_sync(lambda: minion._handle_decoded_payload(job_data))
715
716
717def test_minion_manage_schedule():
718    """
719    Tests that the manage_schedule will call the add function, adding
720    schedule data into opts.
721    """
722    with patch("salt.minion.Minion.ctx", MagicMock(return_value={})), patch(
723        "salt.minion.Minion.sync_connect_master",
724        MagicMock(side_effect=RuntimeError("stop execution")),
725    ), patch(
726        "salt.utils.process.SignalHandlingMultiprocessingProcess.start",
727        MagicMock(return_value=True),
728    ), patch(
729        "salt.utils.process.SignalHandlingMultiprocessingProcess.join",
730        MagicMock(return_value=True),
731    ):
732        mock_opts = salt.config.DEFAULT_MINION_OPTS.copy()
733        io_loop = salt.ext.tornado.ioloop.IOLoop()
734        io_loop.make_current()
735
736        with patch("salt.utils.schedule.clean_proc_dir", MagicMock(return_value=None)):
737            try:
738                mock_functions = {"test.ping": None}
739
740                minion = salt.minion.Minion(mock_opts, io_loop=io_loop)
741                minion.schedule = salt.utils.schedule.Schedule(
742                    mock_opts,
743                    mock_functions,
744                    returners={},
745                    new_instance=True,
746                )
747
748                minion.opts["foo"] = "bar"
749                schedule_data = {
750                    "test_job": {
751                        "function": "test.ping",
752                        "return_job": False,
753                        "jid_include": True,
754                        "maxrunning": 2,
755                        "seconds": 10,
756                    }
757                }
758
759                data = {
760                    "name": "test-item",
761                    "schedule": schedule_data,
762                    "func": "add",
763                    "persist": False,
764                }
765                tag = "manage_schedule"
766
767                minion.manage_schedule(tag, data)
768                assert "test_job" in minion.opts["schedule"]
769            finally:
770                del minion.schedule
771                minion.destroy()
772                del minion
773
774
775def test_minion_manage_beacons():
776    """
777    Tests that the manage_beacons will call the add function, adding
778    beacon data into opts.
779    """
780    with patch("salt.minion.Minion.ctx", MagicMock(return_value={})), patch(
781        "salt.minion.Minion.sync_connect_master",
782        MagicMock(side_effect=RuntimeError("stop execution")),
783    ), patch(
784        "salt.utils.process.SignalHandlingMultiprocessingProcess.start",
785        MagicMock(return_value=True),
786    ), patch(
787        "salt.utils.process.SignalHandlingMultiprocessingProcess.join",
788        MagicMock(return_value=True),
789    ):
790        try:
791            mock_opts = salt.config.DEFAULT_MINION_OPTS.copy()
792            mock_opts["beacons"] = {}
793
794            io_loop = salt.ext.tornado.ioloop.IOLoop()
795            io_loop.make_current()
796
797            mock_functions = {"test.ping": None}
798            minion = salt.minion.Minion(mock_opts, io_loop=io_loop)
799            minion.beacons = salt.beacons.Beacon(mock_opts, mock_functions)
800
801            bdata = [{"salt-master": "stopped"}, {"apache2": "stopped"}]
802            data = {"name": "ps", "beacon_data": bdata, "func": "add"}
803
804            tag = "manage_beacons"
805            log.debug("==== minion.opts %s ====", minion.opts)
806
807            minion.manage_beacons(tag, data)
808            assert "ps" in minion.opts["beacons"]
809            assert minion.opts["beacons"]["ps"] == bdata
810        finally:
811            minion.destroy()
812
813
814def test_prep_ip_port():
815    _ip = ipaddress.ip_address
816
817    opts = {"master": "10.10.0.3", "master_uri_format": "ip_only"}
818    ret = salt.minion.prep_ip_port(opts)
819    assert ret == {"master": _ip("10.10.0.3")}
820
821    opts = {
822        "master": "10.10.0.3",
823        "master_port": 1234,
824        "master_uri_format": "default",
825    }
826    ret = salt.minion.prep_ip_port(opts)
827    assert ret == {"master": "10.10.0.3"}
828
829    opts = {"master": "10.10.0.3:1234", "master_uri_format": "default"}
830    ret = salt.minion.prep_ip_port(opts)
831    assert ret == {"master": "10.10.0.3", "master_port": 1234}
832
833    opts = {"master": "host name", "master_uri_format": "default"}
834    pytest.raises(SaltClientError, salt.minion.prep_ip_port, opts)
835
836    opts = {"master": "10.10.0.3:abcd", "master_uri_format": "default"}
837    pytest.raises(SaltClientError, salt.minion.prep_ip_port, opts)
838
839    opts = {"master": "10.10.0.3::1234", "master_uri_format": "default"}
840    pytest.raises(SaltClientError, salt.minion.prep_ip_port, opts)
841
842
843@pytest.mark.skip_if_not_root
844def test_sock_path_len():
845    """
846    This tests whether or not a larger hash causes the sock path to exceed
847    the system's max sock path length. See the below link for more
848    information.
849
850    https://github.com/saltstack/salt/issues/12172#issuecomment-43903643
851    """
852    opts = {
853        "id": "salt-testing",
854        "hash_type": "sha512",
855        "sock_dir": os.path.join(salt.syspaths.SOCK_DIR, "minion"),
856        "extension_modules": "",
857    }
858    opts = salt.config.DEFAULT_MINION_OPTS.copy()
859    with patch.dict(opts, opts):
860        try:
861            event_publisher = event.AsyncEventPublisher(opts)
862            result = True
863        except ValueError:
864            #  There are rare cases where we operate a closed socket, especially in containers.
865            # In this case, don't fail the test because we'll catch it down the road.
866            result = True
867        except SaltSystemExit:
868            result = False
869    assert result
870
871
872@pytest.mark.skip_on_windows(reason="Skippin, no Salt master running on Windows.")
873def test_master_type_failover():
874    """
875    Tests master_type "failover" to not fall back to 127.0.0.1 address when master does not resolve in DNS
876    """
877    mock_opts = salt.config.DEFAULT_MINION_OPTS.copy()
878    mock_opts.update(
879        {
880            "master_type": "failover",
881            "master": ["master1", "master2"],
882            "__role": "",
883            "retry_dns": 0,
884        }
885    )
886
887    class MockPubChannel:
888        def connect(self):
889            raise SaltClientError("MockedChannel")
890
891        def close(self):
892            return
893
894    def mock_resolve_dns(opts, fallback=False):
895        assert not fallback
896
897        if opts["master"] == "master1":
898            raise SaltClientError("Cannot resolve {}".format(opts["master"]))
899
900        return {
901            "master_ip": "192.168.2.1",
902            "master_uri": "tcp://192.168.2.1:4505",
903        }
904
905    def mock_transport_factory(opts, **kwargs):
906        assert opts["master"] == "master2"
907        return MockPubChannel()
908
909    with patch("salt.minion.resolve_dns", mock_resolve_dns), patch(
910        "salt.transport.client.AsyncPubChannel.factory", mock_transport_factory
911    ), patch("salt.loader.grains", MagicMock(return_value=[])):
912        with pytest.raises(SaltClientError):
913            minion = salt.minion.Minion(mock_opts)
914            yield minion.connect_master()
915
916
917def test_master_type_failover_no_masters():
918    """
919    Tests master_type "failover" to not fall back to 127.0.0.1 address when no master can be resolved
920    """
921    mock_opts = salt.config.DEFAULT_MINION_OPTS.copy()
922    mock_opts.update(
923        {
924            "master_type": "failover",
925            "master": ["master1", "master2"],
926            "__role": "",
927            "retry_dns": 0,
928        }
929    )
930
931    def mock_resolve_dns(opts, fallback=False):
932        assert not fallback
933        raise SaltClientError("Cannot resolve {}".format(opts["master"]))
934
935    with patch("salt.minion.resolve_dns", mock_resolve_dns), patch(
936        "salt.loader.grains", MagicMock(return_value=[])
937    ):
938        with pytest.raises(SaltClientError):
939            minion = salt.minion.Minion(mock_opts)
940            yield minion.connect_master()
941
942
943def test_config_cache_path_overrides():
944    cachedir = os.path.abspath("/path/to/master/cache")
945    opts = {"cachedir": cachedir, "conf_file": None}
946
947    mminion = salt.minion.MasterMinion(opts)
948    assert mminion.opts["cachedir"] == cachedir
949