forked from saltstack/salt
-
Notifications
You must be signed in to change notification settings - Fork 1
/
test_minion.py
325 lines (291 loc) · 16.4 KB
/
test_minion.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# -*- coding: utf-8 -*-
'''
:codeauthor: Mike Place <mp@saltstack.com>
'''
# Import python libs
from __future__ import absolute_import
import copy
import os
# Import Salt Testing libs
from tests.support.unit import TestCase, skipIf
from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch, MagicMock
from tests.support.mixins import AdaptedConfigurationTestCaseMixin
from tests.support.helpers import skip_if_not_root
# Import salt libs
import salt.minion
import salt.utils.event as event
from salt.exceptions import SaltSystemExit, SaltMasterUnresolvableError
import salt.syspaths
import tornado
import tornado.testing
from salt.ext.six.moves import range
__opts__ = {}
@skipIf(NO_MOCK, NO_MOCK_REASON)
class MinionTestCase(TestCase, AdaptedConfigurationTestCaseMixin):
def test_invalid_master_address(self):
with patch.dict(__opts__, {'ipv6': False, 'master': float('127.0'), 'master_port': '4555', 'retry_dns': False}):
self.assertRaises(SaltSystemExit, salt.minion.resolve_dns, __opts__)
def test_source_int_name_local(self):
'''
test when file_client local and
source_interface_name is set
'''
interfaces = {'bond0.1234': {'hwaddr': '01:01:01:d0:d0:d0',
'up': True, 'inet':
[{'broadcast': '111.1.111.255',
'netmask': '111.1.0.0',
'label': 'bond0',
'address': '111.1.0.1'}]}}
with patch.dict(__opts__, {'ipv6': False, 'master': '127.0.0.1',
'master_port': '4555', 'file_client': 'local',
'source_interface_name': 'bond0.1234',
'source_ret_port': 49017,
'source_publish_port': 49018}), \
patch('salt.utils.network.interfaces',
MagicMock(return_value=interfaces)):
assert salt.minion.resolve_dns(__opts__) == {'master_ip': '127.0.0.1',
'source_ip': '111.1.0.1',
'source_ret_port': 49017,
'source_publish_port': 49018,
'master_uri': 'tcp://127.0.0.1:4555'}
def test_source_int_name_remote(self):
'''
test when file_client remote and
source_interface_name is set and
interface is down
'''
interfaces = {'bond0.1234': {'hwaddr': '01:01:01:d0:d0:d0',
'up': False, 'inet':
[{'broadcast': '111.1.111.255',
'netmask': '111.1.0.0',
'label': 'bond0',
'address': '111.1.0.1'}]}}
with patch.dict(__opts__, {'ipv6': False, 'master': '127.0.0.1',
'master_port': '4555', 'file_client': 'remote',
'source_interface_name': 'bond0.1234',
'source_ret_port': 49017,
'source_publish_port': 49018}), \
patch('salt.utils.network.interfaces',
MagicMock(return_value=interfaces)):
assert salt.minion.resolve_dns(__opts__) == {'master_ip': '127.0.0.1',
'source_ret_port': 49017,
'source_publish_port': 49018,
'master_uri': 'tcp://127.0.0.1:4555'}
def test_source_address(self):
'''
test when source_address is set
'''
interfaces = {'bond0.1234': {'hwaddr': '01:01:01:d0:d0:d0',
'up': False, 'inet':
[{'broadcast': '111.1.111.255',
'netmask': '111.1.0.0',
'label': 'bond0',
'address': '111.1.0.1'}]}}
with patch.dict(__opts__, {'ipv6': False, 'master': '127.0.0.1',
'master_port': '4555', 'file_client': 'local',
'source_interface_name': '',
'source_address': '111.1.0.1',
'source_ret_port': 49017,
'source_publish_port': 49018}), \
patch('salt.utils.network.interfaces',
MagicMock(return_value=interfaces)):
assert salt.minion.resolve_dns(__opts__) == {'source_publish_port': 49018,
'source_ret_port': 49017,
'master_uri': 'tcp://127.0.0.1:4555',
'source_ip': '111.1.0.1',
'master_ip': '127.0.0.1'}
# Tests for _handle_decoded_payload in the salt.minion.Minion() class: 3
def test_handle_decoded_payload_jid_match_in_jid_queue(self):
'''
Tests that the _handle_decoded_payload function returns when a jid is given that is already present
in the jid_queue.
Note: This test doesn't contain all of the patch decorators above the function like the other tests
for _handle_decoded_payload below. This is essential to this test as the call to the function must
return None BEFORE any of the processes are spun up because we should be avoiding firing duplicate
jobs.
'''
mock_opts = salt.config.DEFAULT_MINION_OPTS
mock_data = {'fun': 'foo.bar',
'jid': 123}
mock_jid_queue = [123]
minion = salt.minion.Minion(mock_opts, jid_queue=copy.copy(mock_jid_queue), io_loop=tornado.ioloop.IOLoop())
try:
ret = minion._handle_decoded_payload(mock_data).result()
self.assertEqual(minion.jid_queue, mock_jid_queue)
self.assertIsNone(ret)
finally:
minion.destroy()
def test_handle_decoded_payload_jid_queue_addition(self):
'''
Tests that the _handle_decoded_payload function adds a jid to the minion's jid_queue when the new
jid isn't already present in the jid_queue.
'''
with patch('salt.minion.Minion.ctx', MagicMock(return_value={})), \
patch('salt.utils.process.SignalHandlingMultiprocessingProcess.start', MagicMock(return_value=True)), \
patch('salt.utils.process.SignalHandlingMultiprocessingProcess.join', MagicMock(return_value=True)):
mock_jid = 11111
mock_opts = salt.config.DEFAULT_MINION_OPTS
mock_data = {'fun': 'foo.bar',
'jid': mock_jid}
mock_jid_queue = [123, 456]
minion = salt.minion.Minion(mock_opts, jid_queue=copy.copy(mock_jid_queue), io_loop=tornado.ioloop.IOLoop())
try:
# Assert that the minion's jid_queue attribute matches the mock_jid_queue as a baseline
# This can help debug any test failures if the _handle_decoded_payload call fails.
self.assertEqual(minion.jid_queue, mock_jid_queue)
# Call the _handle_decoded_payload function and update the mock_jid_queue to include the new
# mock_jid. The mock_jid should have been added to the jid_queue since the mock_jid wasn't
# previously included. The minion's jid_queue attribute and the mock_jid_queue should be equal.
minion._handle_decoded_payload(mock_data).result()
mock_jid_queue.append(mock_jid)
self.assertEqual(minion.jid_queue, mock_jid_queue)
finally:
minion.destroy()
def test_handle_decoded_payload_jid_queue_reduced_minion_jid_queue_hwm(self):
'''
Tests that the _handle_decoded_payload function removes a jid from the minion's jid_queue when the
minion's jid_queue high water mark (minion_jid_queue_hwm) is hit.
'''
with patch('salt.minion.Minion.ctx', MagicMock(return_value={})), \
patch('salt.utils.process.SignalHandlingMultiprocessingProcess.start', MagicMock(return_value=True)), \
patch('salt.utils.process.SignalHandlingMultiprocessingProcess.join', MagicMock(return_value=True)):
mock_opts = salt.config.DEFAULT_MINION_OPTS
mock_opts['minion_jid_queue_hwm'] = 2
mock_data = {'fun': 'foo.bar',
'jid': 789}
mock_jid_queue = [123, 456]
minion = salt.minion.Minion(mock_opts, jid_queue=copy.copy(mock_jid_queue), io_loop=tornado.ioloop.IOLoop())
try:
# Assert that the minion's jid_queue attribute matches the mock_jid_queue as a baseline
# This can help debug any test failures if the _handle_decoded_payload call fails.
self.assertEqual(minion.jid_queue, mock_jid_queue)
# Call the _handle_decoded_payload function and check that the queue is smaller by one item
# and contains the new jid
minion._handle_decoded_payload(mock_data).result()
self.assertEqual(len(minion.jid_queue), 2)
self.assertEqual(minion.jid_queue, [456, 789])
finally:
minion.destroy()
def test_process_count_max(self):
'''
Tests that the _handle_decoded_payload function does not spawn more than the configured amount of processes,
as per process_count_max.
'''
with patch('salt.minion.Minion.ctx', MagicMock(return_value={})), \
patch('salt.utils.process.SignalHandlingMultiprocessingProcess.start', MagicMock(return_value=True)), \
patch('salt.utils.process.SignalHandlingMultiprocessingProcess.join', MagicMock(return_value=True)), \
patch('salt.utils.minion.running', MagicMock(return_value=[])), \
patch('tornado.gen.sleep', MagicMock(return_value=tornado.concurrent.Future())):
process_count_max = 10
mock_opts = salt.config.DEFAULT_MINION_OPTS
mock_opts['minion_jid_queue_hwm'] = 100
mock_opts["process_count_max"] = process_count_max
io_loop = tornado.ioloop.IOLoop()
minion = salt.minion.Minion(mock_opts, jid_queue=[], io_loop=io_loop)
try:
# mock gen.sleep to throw a special Exception when called, so that we detect it
class SleepCalledEception(Exception):
"""Thrown when sleep is called"""
pass
tornado.gen.sleep.return_value.set_exception(SleepCalledEception())
# up until process_count_max: gen.sleep does not get called, processes are started normally
for i in range(process_count_max):
mock_data = {'fun': 'foo.bar',
'jid': i}
io_loop.run_sync(lambda data=mock_data: minion._handle_decoded_payload(data))
self.assertEqual(salt.utils.process.SignalHandlingMultiprocessingProcess.start.call_count, i + 1)
self.assertEqual(len(minion.jid_queue), i + 1)
salt.utils.minion.running.return_value += [i]
# above process_count_max: gen.sleep does get called, JIDs are created but no new processes are started
mock_data = {'fun': 'foo.bar',
'jid': process_count_max + 1}
self.assertRaises(SleepCalledEception,
lambda: io_loop.run_sync(lambda: minion._handle_decoded_payload(mock_data)))
self.assertEqual(salt.utils.process.SignalHandlingMultiprocessingProcess.start.call_count,
process_count_max)
self.assertEqual(len(minion.jid_queue), process_count_max + 1)
finally:
minion.destroy()
def test_beacons_before_connect(self):
'''
Tests that the 'beacons_before_connect' option causes the beacons to be initialized before connect.
'''
with patch('salt.minion.Minion.ctx', MagicMock(return_value={})), \
patch('salt.minion.Minion.sync_connect_master', MagicMock(side_effect=RuntimeError('stop execution'))), \
patch('salt.utils.process.SignalHandlingMultiprocessingProcess.start', MagicMock(return_value=True)), \
patch('salt.utils.process.SignalHandlingMultiprocessingProcess.join', MagicMock(return_value=True)):
mock_opts = self.get_config('minion', from_scratch=True)
mock_opts['beacons_before_connect'] = True
io_loop = tornado.ioloop.IOLoop()
io_loop.make_current()
minion = salt.minion.Minion(mock_opts, io_loop=io_loop)
try:
try:
minion.tune_in(start=True)
except RuntimeError:
pass
# Make sure beacons are initialized but the sheduler is not
self.assertTrue('beacons' in minion.periodic_callbacks)
self.assertTrue('schedule' not in minion.periodic_callbacks)
finally:
minion.destroy()
def test_scheduler_before_connect(self):
'''
Tests that the 'scheduler_before_connect' option causes the scheduler to be initialized before connect.
'''
with patch('salt.minion.Minion.ctx', MagicMock(return_value={})), \
patch('salt.minion.Minion.sync_connect_master', MagicMock(side_effect=RuntimeError('stop execution'))), \
patch('salt.utils.process.SignalHandlingMultiprocessingProcess.start', MagicMock(return_value=True)), \
patch('salt.utils.process.SignalHandlingMultiprocessingProcess.join', MagicMock(return_value=True)):
mock_opts = self.get_config('minion', from_scratch=True)
mock_opts['scheduler_before_connect'] = True
io_loop = tornado.ioloop.IOLoop()
io_loop.make_current()
minion = salt.minion.Minion(mock_opts, io_loop=io_loop)
try:
try:
minion.tune_in(start=True)
except RuntimeError:
pass
# Make sure the scheduler is initialized but the beacons are not
self.assertTrue('schedule' in minion.periodic_callbacks)
self.assertTrue('beacons' not in minion.periodic_callbacks)
finally:
minion.destroy()
def test_minion_retry_dns_count(self):
'''
Tests that the resolve_dns will retry dns look ups for a maximum of
3 times before raising a SaltMasterUnresolvableError exception.
'''
with patch.dict(__opts__, {'ipv6': False, 'master': 'dummy',
'master_port': '4555',
'retry_dns': 1, 'retry_dns_count': 3}):
self.assertRaises(SaltMasterUnresolvableError,
salt.minion.resolve_dns, __opts__)
@skipIf(NO_MOCK, NO_MOCK_REASON)
class MinionAsyncTestCase(TestCase, AdaptedConfigurationTestCaseMixin, tornado.testing.AsyncTestCase):
@skip_if_not_root
def test_sock_path_len(self):
'''
This tests whether or not a larger hash causes the sock path to exceed
the system's max sock path length. See the below link for more
information.
https://github.com/saltstack/salt/issues/12172#issuecomment-43903643
'''
opts = {
'id': 'salt-testing',
'hash_type': 'sha512',
'sock_dir': os.path.join(salt.syspaths.SOCK_DIR, 'minion'),
'extension_modules': ''
}
with patch.dict(__opts__, opts):
try:
event_publisher = event.AsyncEventPublisher(__opts__)
result = True
except ValueError:
# There are rare cases where we operate a closed socket, especially in containers.
# In this case, don't fail the test because we'll catch it down the road.
result = True
except SaltSystemExit:
result = False
self.assertTrue(result)