1"""
2    :codeauthor: Justin Anderson <janderson@saltstack.com>
3"""
4import pathlib
5import shutil
6
7import attr
8import pytest
9from tests.support.helpers import PRE_PYTEST_SKIP_OR_NOT
10
11pytestmark = [
12    pytest.mark.slow_test,
13    pytest.mark.windows_whitelisted,
14]
15
16
17@attr.s(frozen=True, slots=True)
18class SaltCallCliWrapper:
19    salt_call_cli = attr.ib()
20
21    def run(self, *args, **kwargs):
22        beacon_event_timeout = 120
23        cli_run_timeout = beacon_event_timeout + 30
24        return self.salt_call_cli.run(
25            *args, timeout=beacon_event_timeout, _timeout=cli_run_timeout, **kwargs
26        )
27
28
29@pytest.fixture(scope="module")
30def salt_call_cli(salt_call_cli):
31    return SaltCallCliWrapper(salt_call_cli)
32
33
34@pytest.fixture(scope="module")
35def cleanup_beacons_config_module(salt_minion, salt_call_cli):
36    minion_conf_d_dir = (
37        pathlib.Path(salt_minion.config_dir)
38        / pathlib.Path(salt_minion.config["default_include"]).parent
39    )
40    if not minion_conf_d_dir.is_dir():
41        minion_conf_d_dir.mkdir()
42    beacons_config_file_path = minion_conf_d_dir / "beacons.conf"
43    ret = salt_call_cli.run("beacons.reset")
44    assert ret.exitcode == 0
45    assert ret.json
46    assert ret.json["result"] is True
47    if beacons_config_file_path.exists():
48        beacons_config_file_path.unlink()
49    try:
50        yield beacons_config_file_path
51    finally:
52        if beacons_config_file_path.exists():
53            beacons_config_file_path.unlink()
54
55
56@pytest.fixture(autouse=True)
57def cleanup_beacons_config(cleanup_beacons_config_module, salt_call_cli):
58    try:
59        yield cleanup_beacons_config_module
60    finally:
61        ret = salt_call_cli.run("beacons.reset")
62        assert ret.exitcode == 0
63        assert ret.json
64        assert ret.json["result"] is True
65
66
67@pytest.fixture(scope="module")
68def inotify_file_path(tmp_path_factory):
69    inotify_directory = tmp_path_factory.mktemp("important-files")
70    try:
71        yield inotify_directory / "really-important"
72    finally:
73        shutil.rmtree(str(inotify_directory), ignore_errors=True)
74
75
76@pytest.fixture(scope="module")
77def pillar_tree(
78    base_env_pillar_tree_root_dir, salt_minion, salt_call_cli, inotify_file_path
79):
80    top_file = """
81    base:
82      '{}':
83        - beacons
84    """.format(
85        salt_minion.id
86    )
87    beacon_pillar_file = """
88    beacons:
89      inotify:
90        - files:
91            {}:
92              mask:
93                - open
94                - create
95                - close_write
96    """.format(
97        inotify_file_path
98    )
99    top_tempfile = pytest.helpers.temp_file(
100        "top.sls", top_file, base_env_pillar_tree_root_dir
101    )
102    beacon_tempfile = pytest.helpers.temp_file(
103        "beacons.sls", beacon_pillar_file, base_env_pillar_tree_root_dir
104    )
105    try:
106        with top_tempfile, beacon_tempfile:
107            ret = salt_call_cli.run("saltutil.refresh_pillar", wait=True)
108            assert ret.exitcode == 0
109            assert ret.json is True
110            yield
111    finally:
112        # Refresh pillar again to cleaup the temp pillar
113        ret = salt_call_cli.run("saltutil.refresh_pillar", wait=True)
114        assert ret.exitcode == 0
115        assert ret.json is True
116
117
118@attr.s(frozen=True, slots=True)
119class Beacon:
120    name = attr.ib()
121    data = attr.ib()
122
123
124def beacon_instance_ids(value):
125    return str(value)
126
127
128@pytest.fixture(
129    params=[
130        Beacon("ps", [{"processes": {"apache2": "stopped"}}]),
131        Beacon(
132            "watch_apache",
133            [{"processes": {"apache2": "stopped"}}, {"beacon_module": "ps"}],
134        ),
135    ],
136    ids=beacon_instance_ids,
137)
138def beacon_instance(request):
139    return request.param
140
141
142def test_add_and_delete(salt_call_cli, beacon_instance):
143    """
144    Test adding and deleting a beacon
145    """
146    # Add the beacon
147    ret = salt_call_cli.run(
148        "beacons.add", beacon_instance.name, beacon_data=beacon_instance.data
149    )
150    assert ret.exitcode == 0
151    assert ret.json
152    assert ret.json["result"] is True
153
154    # Save beacons
155    ret = salt_call_cli.run("beacons.save")
156    assert ret.exitcode == 0
157    assert ret.json
158    assert ret.json["result"] is True
159
160    # Delete beacon
161    ret = salt_call_cli.run("beacons.delete", beacon_instance.name)
162    assert ret.exitcode == 0
163    assert ret.json
164    assert ret.json["result"] is True
165
166
167@pytest.fixture
168def beacon(beacon_instance, salt_call_cli):
169    ret = salt_call_cli.run(
170        "beacons.add", beacon_instance.name, beacon_data=beacon_instance.data
171    )
172    assert ret.exitcode == 0
173    assert ret.json
174    assert ret.json["result"] is True
175
176    # Save beacons
177    ret = salt_call_cli.run("beacons.save")
178    assert ret.exitcode == 0
179    assert ret.json
180    assert ret.json["result"] is True
181
182    # assert beacon exists
183    ret = salt_call_cli.run("beacons.list", return_yaml=False)
184    assert ret.exitcode == 0
185    assert ret.json
186    assert beacon_instance.name in ret.json
187
188    yield beacon_instance
189
190
191def test_disable(salt_call_cli, beacon):
192    """
193    Test disabling beacons
194    """
195    ret = salt_call_cli.run("beacons.disable")
196    assert ret.exitcode == 0
197    assert ret.json
198    assert ret.json["result"] is True
199
200    # assert beacons are disabled
201    ret = salt_call_cli.run("beacons.list", return_yaml=False)
202    assert ret.exitcode == 0
203    assert ret.json
204    assert ret.json["enabled"] is False
205
206    # disable added beacon
207    ret = salt_call_cli.run("beacons.disable_beacon", beacon.name)
208    assert ret.exitcode == 0
209    assert ret.json
210    assert ret.json["result"] is True
211
212    # assert beacon is disabled
213    ret = salt_call_cli.run("beacons.list", return_yaml=False)
214    assert ret.exitcode == 0
215    assert ret.json
216    assert beacon.name in ret.json
217    for beacon_data in ret.json[beacon.name]:
218        if "enabled" in beacon_data:
219            assert beacon_data["enabled"] is False
220            break
221    else:
222        pytest.fail("Did not find the beacon data with the 'enabled' key")
223
224
225@pytest.fixture
226def disabled_beacon(beacon, salt_call_cli):
227    ret = salt_call_cli.run("beacons.disable")
228    assert ret.exitcode == 0
229    assert ret.json
230    assert ret.json["result"] is True
231    return beacon
232
233
234def test_enable(salt_call_cli, disabled_beacon):
235    """
236    Test enabling beacons
237    """
238    # enable beacons on minion
239    ret = salt_call_cli.run("beacons.enable")
240    assert ret.exitcode == 0
241    assert ret.json
242    assert ret.json["result"] is True
243
244    # assert beacons are enabled
245    ret = salt_call_cli.run("beacons.list", return_yaml=False)
246    assert ret.exitcode == 0
247    assert ret.json
248    assert ret.json["enabled"] is True
249
250
251@pytest.mark.skipif(
252    PRE_PYTEST_SKIP_OR_NOT,
253    reason=(
254        "Skip until https://github.com/saltstack/salt/issues/31516 problems are"
255        " resolved."
256    ),
257)
258def test_enabled_beacons(salt_call_cli, beacon):
259    """
260    Test enabled specific beacon
261    """
262    # enable added beacon
263    ret = salt_call_cli.run("beacons.enable_beacon", beacon.name)
264    assert ret.exitcode == 0
265    assert ret.json
266    assert ret.json["result"] is True
267
268    # assert beacon ps is enabled
269    ret = salt_call_cli.run("beacons.list", return_yaml=False)
270    assert ret.exitcode == 0
271    assert ret.json
272    assert ret.json["enabled"] is True
273    assert beacon.name in ret.json
274    for beacon_data in ret.json[beacon.name]:
275        if "enabled" in beacon_data:
276            assert beacon_data["enabled"] is False
277            break
278    else:
279        pytest.fail("Did not find the beacon data with the 'enabled' key")
280
281
282@pytest.mark.usefixtures("pillar_tree")
283def test_list(salt_call_cli, beacon, inotify_file_path):
284    """
285    Test listing the beacons
286    """
287    # list beacons
288    ret = salt_call_cli.run("beacons.list", return_yaml=False)
289    assert ret.exitcode == 0
290    assert ret.json
291    assert ret.json == {
292        beacon.name: beacon.data,
293        "inotify": [
294            {
295                "files": {
296                    str(inotify_file_path): {"mask": ["open", "create", "close_write"]}
297                }
298            }
299        ],
300    }
301
302
303@pytest.mark.usefixtures("pillar_tree")
304def test_list_only_include_opts(salt_call_cli, beacon):
305    """
306    Test listing the beacons which only exist in opts
307
308    When beacon.save is used to save the running beacons to
309    a file, it uses beacons.list to get that list and should
310    only return those from opts and not pillar.
311
312    In this test, we're making sure we get only get back the
313    beacons that are in opts and not those in pillar.
314    """
315    # list beacons
316    ret = salt_call_cli.run(
317        "beacons.list", return_yaml=False, include_opts=True, include_pillar=False
318    )
319    assert ret.exitcode == 0
320    assert ret.json
321    assert ret.json == {beacon.name: beacon.data}
322
323
324@pytest.mark.usefixtures("pillar_tree", "beacon")
325def test_list_only_include_pillar(salt_call_cli, inotify_file_path):
326    """
327    Test listing the beacons which only exist in pillar
328    """
329    # list beacons
330    ret = salt_call_cli.run(
331        "beacons.list", return_yaml=False, include_opts=False, include_pillar=True
332    )
333    assert ret.exitcode == 0
334    assert ret.json
335    assert ret.json == {
336        "inotify": [
337            {
338                "files": {
339                    str(inotify_file_path): {"mask": ["open", "create", "close_write"]}
340                }
341            }
342        ]
343    }
344
345
346def test_list_available(salt_call_cli):
347    """
348    Test listing the beacons
349    """
350    # list beacons
351    ret = salt_call_cli.run("beacons.list_available", return_yaml=False)
352    assert ret.exitcode == 0
353    assert ret.json
354    assert "ps" in ret.json
355