blob: 034cd3888e013deb4f3c625fc7f78f26454cb1dc [file] [log] [blame]
showarde39ebe92009-06-18 23:14:48 +00001#!/usr/bin/python
2
Prashanth B09338992014-06-22 12:44:10 -07003import cPickle
showard42d44982009-10-12 20:34:03 +00004import os, unittest
showarde39ebe92009-06-18 23:14:48 +00005import common
showardac5b0002009-10-19 18:34:00 +00006from autotest_lib.client.common_lib import global_config
Prashanth B09338992014-06-22 12:44:10 -07007from autotest_lib.client.common_lib import utils
showard42d44982009-10-12 20:34:03 +00008from autotest_lib.client.common_lib.test_utils import mock
showard2aafd902009-10-14 16:20:14 +00009from autotest_lib.scheduler import drone_manager, drone_utility, drones
Simran Basi882f15b2013-10-29 14:59:34 -070010from autotest_lib.scheduler import scheduler_config, site_drone_manager
Prashanth B09338992014-06-22 12:44:10 -070011from autotest_lib.scheduler import thread_lib
12from autotest_lib.scheduler import pidfile_monitor
13from autotest_lib.server.hosts import ssh_host
14
showarde39ebe92009-06-18 23:14:48 +000015
16class MockDrone(drones._AbstractDrone):
showard9bb960b2009-11-19 01:02:11 +000017 def __init__(self, name, active_processes=0, max_processes=10,
18 allowed_users=None):
showarde39ebe92009-06-18 23:14:48 +000019 super(MockDrone, self).__init__()
20 self.name = name
jamesren76fcf192010-04-21 20:39:50 +000021 self.hostname = name
showarde39ebe92009-06-18 23:14:48 +000022 self.active_processes = active_processes
23 self.max_processes = max_processes
showard9bb960b2009-11-19 01:02:11 +000024 self.allowed_users = allowed_users
showard42d44982009-10-12 20:34:03 +000025 # maps method names list of tuples containing method arguments
26 self._recorded_calls = {'queue_call': [],
27 'send_file_to': []}
28
29
30 def queue_call(self, method, *args, **kwargs):
31 self._recorded_calls['queue_call'].append((method, args, kwargs))
32
33
showard2aafd902009-10-14 16:20:14 +000034 def call(self, method, *args, **kwargs):
35 # don't bother differentiating between call() and queue_call()
36 return self.queue_call(method, *args, **kwargs)
37
38
showard42d44982009-10-12 20:34:03 +000039 def send_file_to(self, drone, source_path, destination_path,
40 can_fail=False):
41 self._recorded_calls['send_file_to'].append(
42 (drone, source_path, destination_path))
43
44
45 # method for use by tests
46 def _check_for_recorded_call(self, method_name, arguments):
47 recorded_arg_list = self._recorded_calls[method_name]
48 was_called = arguments in recorded_arg_list
49 if not was_called:
50 print 'Recorded args:', recorded_arg_list
51 print 'Expected:', arguments
52 return was_called
53
54
55 def was_call_queued(self, method, *args, **kwargs):
56 return self._check_for_recorded_call('queue_call',
57 (method, args, kwargs))
58
59
60 def was_file_sent(self, drone, source_path, destination_path):
61 return self._check_for_recorded_call('send_file_to',
62 (drone, source_path,
63 destination_path))
showarde39ebe92009-06-18 23:14:48 +000064
65
66class DroneManager(unittest.TestCase):
showard42d44982009-10-12 20:34:03 +000067 _DRONE_INSTALL_DIR = '/drone/install/dir'
showardc75fded2009-10-14 16:20:02 +000068 _DRONE_RESULTS_DIR = os.path.join(_DRONE_INSTALL_DIR, 'results')
showard42d44982009-10-12 20:34:03 +000069 _RESULTS_DIR = '/results/dir'
70 _SOURCE_PATH = 'source/path'
71 _DESTINATION_PATH = 'destination/path'
showard2aafd902009-10-14 16:20:14 +000072 _WORKING_DIRECTORY = 'working/directory'
showard9bb960b2009-11-19 01:02:11 +000073 _USERNAME = 'my_user'
showard42d44982009-10-12 20:34:03 +000074
showarde39ebe92009-06-18 23:14:48 +000075 def setUp(self):
showard42d44982009-10-12 20:34:03 +000076 self.god = mock.mock_god()
77 self.god.stub_with(drones, 'AUTOTEST_INSTALL_DIR',
78 self._DRONE_INSTALL_DIR)
showarde39ebe92009-06-18 23:14:48 +000079 self.manager = drone_manager.DroneManager()
showard42d44982009-10-12 20:34:03 +000080 self.god.stub_with(self.manager, '_results_dir', self._RESULTS_DIR)
81
showard2aafd902009-10-14 16:20:14 +000082 # we don't want this to ever actually get called
83 self.god.stub_function(drones, 'get_drone')
showardac5b0002009-10-19 18:34:00 +000084 # we don't want the DroneManager to go messing with global config
85 def do_nothing():
86 pass
87 self.god.stub_with(self.manager, 'refresh_drone_configs', do_nothing)
showard2aafd902009-10-14 16:20:14 +000088
showard42d44982009-10-12 20:34:03 +000089 # set up some dummy drones
showard202343e2009-10-14 16:20:24 +000090 self.mock_drone = MockDrone('mock_drone')
showard42d44982009-10-12 20:34:03 +000091 self.manager._drones[self.mock_drone.name] = self.mock_drone
92 self.results_drone = MockDrone('results_drone', 0, 10)
93 self.manager._results_drone = self.results_drone
94
95 self.mock_drone_process = drone_manager.Process(self.mock_drone.name, 0)
96
97
98 def tearDown(self):
99 self.god.unstub_all()
showarde39ebe92009-06-18 23:14:48 +0000100
101
102 def _test_choose_drone_for_execution_helper(self, processes_info_list,
103 requested_processes):
104 for index, process_info in enumerate(processes_info_list):
105 active_processes, max_processes = process_info
106 self.manager._enqueue_drone(MockDrone(index, active_processes,
107 max_processes))
108
showard9bb960b2009-11-19 01:02:11 +0000109 return self.manager._choose_drone_for_execution(requested_processes,
jamesren76fcf192010-04-21 20:39:50 +0000110 self._USERNAME, None)
showarde39ebe92009-06-18 23:14:48 +0000111
112
113 def test_choose_drone_for_execution(self):
114 drone = self._test_choose_drone_for_execution_helper([(1, 2), (0, 2)],
115 1)
116 self.assertEquals(drone.name, 1)
117
118
119 def test_choose_drone_for_execution_some_full(self):
120 drone = self._test_choose_drone_for_execution_helper([(0, 1), (1, 3)],
121 2)
122 self.assertEquals(drone.name, 1)
123
124
125 def test_choose_drone_for_execution_all_full(self):
126 drone = self._test_choose_drone_for_execution_helper([(2, 1), (3, 2)],
127 1)
128 self.assertEquals(drone.name, 1)
129
130
jamesren37b50452010-03-25 20:38:56 +0000131 def test_choose_drone_for_execution_all_full_same_percentage_capacity(self):
132 drone = self._test_choose_drone_for_execution_helper([(5, 3), (10, 6)],
133 1)
134 self.assertEquals(drone.name, 1)
135
136
showard9bb960b2009-11-19 01:02:11 +0000137 def test_user_restrictions(self):
138 # this drone is restricted to a different user
139 self.manager._enqueue_drone(MockDrone(1, max_processes=10,
140 allowed_users=['fakeuser']))
141 # this drone is allowed but has lower capacity
142 self.manager._enqueue_drone(MockDrone(2, max_processes=2,
143 allowed_users=[self._USERNAME]))
144
145 self.assertEquals(2,
jamesren76fcf192010-04-21 20:39:50 +0000146 self.manager.max_runnable_processes(self._USERNAME,
147 None))
showard9bb960b2009-11-19 01:02:11 +0000148 drone = self.manager._choose_drone_for_execution(
jamesren76fcf192010-04-21 20:39:50 +0000149 1, username=self._USERNAME, drone_hostnames_allowed=None)
showard9bb960b2009-11-19 01:02:11 +0000150 self.assertEquals(drone.name, 2)
151
152
jamesren37b50452010-03-25 20:38:56 +0000153 def test_user_restrictions_with_full_drone(self):
154 # this drone is restricted to a different user
155 self.manager._enqueue_drone(MockDrone(1, max_processes=10,
156 allowed_users=['fakeuser']))
157 # this drone is allowed but is full
158 self.manager._enqueue_drone(MockDrone(2, active_processes=3,
159 max_processes=2,
160 allowed_users=[self._USERNAME]))
161
162 self.assertEquals(0,
jamesren76fcf192010-04-21 20:39:50 +0000163 self.manager.max_runnable_processes(self._USERNAME,
164 None))
jamesren37b50452010-03-25 20:38:56 +0000165 drone = self.manager._choose_drone_for_execution(
jamesren76fcf192010-04-21 20:39:50 +0000166 1, username=self._USERNAME, drone_hostnames_allowed=None)
jamesren37b50452010-03-25 20:38:56 +0000167 self.assertEquals(drone.name, 2)
168
169
jamesren76fcf192010-04-21 20:39:50 +0000170 def _setup_test_drone_restrictions(self, active_processes=0):
171 self.manager._enqueue_drone(MockDrone(
172 1, active_processes=active_processes, max_processes=10))
173 self.manager._enqueue_drone(MockDrone(
174 2, active_processes=active_processes, max_processes=5))
175 self.manager._enqueue_drone(MockDrone(
176 3, active_processes=active_processes, max_processes=2))
177
178
179 def test_drone_restrictions_allow_any(self):
180 self._setup_test_drone_restrictions()
181 self.assertEquals(10,
182 self.manager.max_runnable_processes(self._USERNAME,
183 None))
184 drone = self.manager._choose_drone_for_execution(
185 1, username=self._USERNAME, drone_hostnames_allowed=None)
186 self.assertEqual(drone.name, 1)
187
188
189 def test_drone_restrictions_under_capacity(self):
190 self._setup_test_drone_restrictions()
191 drone_hostnames_allowed = (2, 3)
192 self.assertEquals(
193 5, self.manager.max_runnable_processes(self._USERNAME,
194 drone_hostnames_allowed))
195 drone = self.manager._choose_drone_for_execution(
196 1, username=self._USERNAME,
197 drone_hostnames_allowed=drone_hostnames_allowed)
198
199 self.assertEqual(drone.name, 2)
200
201
202 def test_drone_restrictions_over_capacity(self):
203 self._setup_test_drone_restrictions(active_processes=6)
204 drone_hostnames_allowed = (2, 3)
205 self.assertEquals(
206 0, self.manager.max_runnable_processes(self._USERNAME,
207 drone_hostnames_allowed))
208 drone = self.manager._choose_drone_for_execution(
209 7, username=self._USERNAME,
210 drone_hostnames_allowed=drone_hostnames_allowed)
211 self.assertEqual(drone.name, 2)
212
213
214 def test_drone_restrictions_allow_none(self):
215 self._setup_test_drone_restrictions()
216 drone_hostnames_allowed = ()
217 self.assertEquals(
218 0, self.manager.max_runnable_processes(self._USERNAME,
219 drone_hostnames_allowed))
220 drone = self.manager._choose_drone_for_execution(
221 1, username=self._USERNAME,
222 drone_hostnames_allowed=drone_hostnames_allowed)
223 self.assertEqual(drone, None)
224
225
showard2aafd902009-10-14 16:20:14 +0000226 def test_initialize(self):
showard2aafd902009-10-14 16:20:14 +0000227 results_hostname = 'results_repo'
showardac5b0002009-10-19 18:34:00 +0000228 results_install_dir = '/results/install'
229 global_config.global_config.override_config_value(
230 scheduler_config.CONFIG_SECTION,
231 'results_host_installation_directory', results_install_dir)
showard2aafd902009-10-14 16:20:14 +0000232
showard2aafd902009-10-14 16:20:14 +0000233 (drones.get_drone.expect_call(self.mock_drone.name)
234 .and_return(self.mock_drone))
showard202343e2009-10-14 16:20:24 +0000235
236 results_drone = MockDrone('results_drone')
237 self.god.stub_function(results_drone, 'set_autotest_install_dir')
238 drones.get_drone.expect_call(results_hostname).and_return(results_drone)
showardac5b0002009-10-19 18:34:00 +0000239 results_drone.set_autotest_install_dir.expect_call(results_install_dir)
showard2aafd902009-10-14 16:20:14 +0000240
241 self.manager.initialize(base_results_dir=self._RESULTS_DIR,
242 drone_hostnames=[self.mock_drone.name],
243 results_repository_hostname=results_hostname)
244
245 self.assert_(self.mock_drone.was_call_queued(
246 'initialize', self._DRONE_RESULTS_DIR + '/'))
showardac5b0002009-10-19 18:34:00 +0000247 self.god.check_playback()
showard2aafd902009-10-14 16:20:14 +0000248
249
showard42d44982009-10-12 20:34:03 +0000250 def test_execute_command(self):
251 self.manager._enqueue_drone(self.mock_drone)
252
showard42d44982009-10-12 20:34:03 +0000253 pidfile_name = 'my_pidfile'
254 log_file = 'log_file'
255
256 pidfile_id = self.manager.execute_command(
257 command=['test', drone_manager.WORKING_DIRECTORY],
showard2aafd902009-10-14 16:20:14 +0000258 working_directory=self._WORKING_DIRECTORY,
showard42d44982009-10-12 20:34:03 +0000259 pidfile_name=pidfile_name,
showard418785b2009-11-23 20:19:59 +0000260 num_processes=1,
showard42d44982009-10-12 20:34:03 +0000261 log_file=log_file)
262
showardc75fded2009-10-14 16:20:02 +0000263 full_working_directory = os.path.join(self._DRONE_RESULTS_DIR,
showard2aafd902009-10-14 16:20:14 +0000264 self._WORKING_DIRECTORY)
showard42d44982009-10-12 20:34:03 +0000265 self.assertEquals(pidfile_id.path,
266 os.path.join(full_working_directory, pidfile_name))
267 self.assert_(self.mock_drone.was_call_queued(
268 'execute_command', ['test', full_working_directory],
269 full_working_directory,
showardc75fded2009-10-14 16:20:02 +0000270 os.path.join(self._DRONE_RESULTS_DIR, log_file), pidfile_name))
showard42d44982009-10-12 20:34:03 +0000271
272
showard2aafd902009-10-14 16:20:14 +0000273 def test_attach_file_to_execution(self):
274 self.manager._enqueue_drone(self.mock_drone)
275
276 contents = 'my\ncontents'
277 attached_path = self.manager.attach_file_to_execution(
278 self._WORKING_DIRECTORY, contents)
279 self.manager.execute_command(command=['test'],
280 working_directory=self._WORKING_DIRECTORY,
showard418785b2009-11-23 20:19:59 +0000281 pidfile_name='mypidfile',
jamesren76fcf192010-04-21 20:39:50 +0000282 num_processes=1,
283 drone_hostnames_allowed=None)
showard2aafd902009-10-14 16:20:14 +0000284
285 self.assert_(self.mock_drone.was_call_queued(
286 'write_to_file',
287 os.path.join(self._DRONE_RESULTS_DIR, attached_path),
288 contents))
289
290
showard42d44982009-10-12 20:34:03 +0000291 def test_copy_results_on_drone(self):
292 self.manager.copy_results_on_drone(self.mock_drone_process,
293 self._SOURCE_PATH,
294 self._DESTINATION_PATH)
295 self.assert_(self.mock_drone.was_call_queued(
296 'copy_file_or_directory',
showardc75fded2009-10-14 16:20:02 +0000297 os.path.join(self._DRONE_RESULTS_DIR, self._SOURCE_PATH),
298 os.path.join(self._DRONE_RESULTS_DIR, self._DESTINATION_PATH)))
showard42d44982009-10-12 20:34:03 +0000299
300
301 def test_copy_to_results_repository(self):
Simran Basi882f15b2013-10-29 14:59:34 -0700302 site_drone_manager.ENABLE_ARCHIVING = True
showard42d44982009-10-12 20:34:03 +0000303 self.manager.copy_to_results_repository(self.mock_drone_process,
304 self._SOURCE_PATH)
305 self.assert_(self.mock_drone.was_file_sent(
306 self.results_drone,
showardc75fded2009-10-14 16:20:02 +0000307 os.path.join(self._DRONE_RESULTS_DIR, self._SOURCE_PATH),
showard42d44982009-10-12 20:34:03 +0000308 os.path.join(self._RESULTS_DIR, self._SOURCE_PATH)))
309
310
311 def test_write_lines_to_file(self):
312 file_path = 'file/path'
313 lines = ['line1', 'line2']
314 written_data = 'line1\nline2\n'
315
316 # write to results repository
317 self.manager.write_lines_to_file(file_path, lines)
318 self.assert_(self.results_drone.was_call_queued(
319 'write_to_file', os.path.join(self._RESULTS_DIR, file_path),
320 written_data))
321
322 # write to a drone
323 self.manager.write_lines_to_file(
324 file_path, lines, paired_with_process=self.mock_drone_process)
325 self.assert_(self.mock_drone.was_call_queued(
326 'write_to_file',
showardc75fded2009-10-14 16:20:02 +0000327 os.path.join(self._DRONE_RESULTS_DIR, file_path), written_data))
showard42d44982009-10-12 20:34:03 +0000328
329
showardd3496242009-12-10 21:41:43 +0000330 def test_pidfile_expiration(self):
331 self.god.stub_with(self.manager, '_get_max_pidfile_refreshes',
332 lambda: 0)
333 pidfile_id = self.manager.get_pidfile_id_from('tag', 'name')
334 self.manager.register_pidfile(pidfile_id)
335 self.manager._drop_old_pidfiles()
336 self.manager._drop_old_pidfiles()
337 self.assertFalse(self.manager._registered_pidfile_info)
338
339
Prashanth B09338992014-06-22 12:44:10 -0700340class AsyncDroneManager(unittest.TestCase):
341 _DRONE_INSTALL_DIR = '/drone/install/dir'
342 _RESULTS_DIR = '/results/dir'
343
344
345 def create_remote_drone(self, hostname, mock_host):
346 """Create and initialize a Remote Drone.
347
348 @return: A remote drone instance.
349 """
350 drones.drone_utility.create_host.expect_call(hostname).and_return(
351 mock_host)
352 mock_host.is_up.expect_call().and_return(True)
353 return drones._RemoteDrone(hostname)
354
355
356 def create_fake_pidfile_info(self, tag='tag', name='name'):
357 pidfile_id = self.manager.get_pidfile_id_from(tag, name)
358 self.manager.register_pidfile(pidfile_id)
359 return self.manager._registered_pidfile_info
360
361
362 def setUp(self):
363 self.god = mock.mock_god()
364 self.god.stub_with(drones, 'AUTOTEST_INSTALL_DIR',
365 self._DRONE_INSTALL_DIR)
366 self.manager = drone_manager.DroneManager()
367 self.god.stub_with(self.manager, '_results_dir', self._RESULTS_DIR)
368
369 # we don't want this to ever actually get called
370 self.god.stub_function(drones, 'get_drone')
371 # we don't want the DroneManager to go messing with global config
372 def do_nothing():
373 pass
374 self.god.stub_with(self.manager, 'refresh_drone_configs', do_nothing)
375
376 # set up some dummy drones with different mock hosts.
377 mock_host1 = self.god.create_mock_class(ssh_host.SSHHost,
378 'mock SSHHost 1')
379 mock_host2 = self.god.create_mock_class(ssh_host.SSHHost,
380 'mock SSHHost 2')
381 self.god.stub_function(drones.drone_utility, 'create_host')
382 self.mock_drone_1 = self.create_remote_drone(
383 'fakehostname1', mock_host1)
384 self.mock_drone_2 = self.create_remote_drone(
385 'fakehostname2', mock_host2)
386 for mock_drone in [self.mock_drone_1, self.mock_drone_2]:
387 self.manager._drones[mock_drone.hostname] = mock_drone
388
389 self.results_drone = MockDrone('results_drone', 0, 10)
390 self.manager._results_drone = self.results_drone
391 self.drone_utility_path = 'mock-drone-utility-path'
392 self.mock_return = {'results': ['mock results'],
393 'warnings': []}
394
395
396 def tearDown(self):
397 self.god.unstub_all()
398
399 def test_trigger_refresh(self):
400 """Test drone manager trigger refresh."""
401 self.god.stub_with(drones._RemoteDrone, '_drone_utility_path',
402 self.drone_utility_path)
403
404 # Create some fake pidfiles and confirm that a refresh call is
405 # executed on each drone host, with the same pidfile paths. Then
406 # check that each drone gets a key in the returned results dictionary.
407 for i in range(0, 1):
408 pidfile_info = self.create_fake_pidfile_info(
409 'tag%s' % i, 'name%s' %i)
410 pidfile_paths = [pidfile.path for pidfile in pidfile_info.keys()]
411 refresh_call = drone_utility.call('refresh', pidfile_paths)
412 expected_results = {}
413 for drone in self.manager.get_drones():
414 mock_result = utils.CmdResult(
415 stdout=cPickle.dumps(self.mock_return))
416 drone._host.run.expect_call(
417 'python %s' % self.drone_utility_path,
418 stdin=cPickle.dumps([refresh_call]), stdout_tee=None,
419 connect_timeout=mock.is_instance_comparator(int)
420 ).and_return(mock_result)
421 expected_results[drone] = self.mock_return['results']
422
423 self.manager.trigger_refresh()
424 self.assertTrue(self.manager._refresh_task_queue.get_results() ==
425 expected_results)
426 self.god.check_playback()
427
428
429 def test_sync_refresh(self):
430 """Test drone manager sync refresh."""
431
432 # Insert some drone_utility results into the results queue, then
433 # check that get_results returns it in the right format, and that
434 # the rest of sync_refresh populates the right datastructures for
435 # correct handling of agents. Also confirm that this method of
436 # syncing is sufficient for the monitor to pick up the exit status
437 # of the process in the same way it would in handle_agents.
438 pidfile_path = 'results/hosts/host_id/job_id-name/.autoserv_execute'
439 pidfiles = {pidfile_path: '123\n12\n0\n'}
440 drone_utility_results = {
441 'pidfiles': pidfiles,
442 'autoserv_processes':{},
443 'all_processes':{},
444 'parse_processes':{},
445 'pidfiles_second_read':pidfiles,
446 }
447 # Our manager instance isn't the drone manager singletone that the
448 # pidfile_monitor will use by default, becuase setUp doesn't call
449 # drone_manager.instance().
450 self.god.stub_with(pidfile_monitor, '_drone_manager', self.manager)
451 monitor = pidfile_monitor.PidfileRunMonitor()
452 monitor.pidfile_id = drone_manager.PidfileId(pidfile_path)
453 self.manager.register_pidfile(monitor.pidfile_id)
454 self.assertTrue(monitor._state.exit_status == None)
455
456 self.manager._refresh_task_queue.results_queue.put(
457 thread_lib.ThreadedTaskQueue.result(
458 self.mock_drone_1, [drone_utility_results]))
459 self.manager.sync_refresh()
460 pidfiles = self.manager._pidfiles
461 pidfile_id = pidfiles.keys()[0]
462 pidfile_contents = pidfiles[pidfile_id]
463
464 self.assertTrue(
465 pidfile_id.path == pidfile_path and
466 pidfile_contents.process.pid == 123 and
467 pidfile_contents.process.hostname ==
468 self.mock_drone_1.hostname and
469 pidfile_contents.exit_status == 12 and
470 pidfile_contents.num_tests_failed == 0)
471 self.assertTrue(monitor.exit_code() == 12)
472 self.god.check_playback()
473
474
showarde39ebe92009-06-18 23:14:48 +0000475if __name__ == '__main__':
476 unittest.main()