blob: fd5b28461f58d3e5c99e55ebc1d31f49014733a1 [file] [log] [blame]
Xixuan Wu0c76d5b2017-08-30 16:40:17 -07001# Copyright 2017 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Module for tasks triggered by suite scheduler."""
6# pylint: disable=g-bad-import-order
7
8from distutils import version
9import MySQLdb
10import logging
11
12import build_lib
13import task_executor
Xixuan Wu0c76d5b2017-08-30 16:40:17 -070014import tot_manager
15
16# The max lifetime of a suite scheduled by suite scheduler
17_JOB_MAX_RUNTIME_MINS_DEFAULT = 72 * 60
18
19# One kind of formats for RO firmware spec.
20_RELEASE_RO_FIRMWARE_SPEC = 'released_ro_'
21
22
23class SchedulingError(Exception):
24 """Raised to indicate a failure in scheduling a task."""
25
26
27class Task(object):
28 """Represents an entry from the suite_scheduler config file.
29
30 Each entry from the suite_scheduler config file maps one-to-one to a
31 Task. Each instance has enough information to schedule itself.
32 """
33
34 def __init__(self, task_info, tot=None):
35 """Initialize a task instance.
36
37 Args:
38 task_info: a config_reader.TaskInfo object, which includes:
39 name, name of this task, e.g. 'NightlyPower'
40 suite, the name of the suite to run, e.g. 'graphics_per-day'
41 branch_specs, a pre-vetted iterable of branch specifiers,
42 e.g. ['>=R18', 'factory']
43 pool, the pool of machines to schedule tasks. Default is None.
44 num, the number of devices to shard the test suite. It could
45 be an Integer or None. By default it's None.
46 boards, a comma separated list of boards to run this task on. Default
47 is None, which allows this task to run on all boards.
Po-Hsien Wang6d589732018-05-15 17:19:34 -070048 exclude_boards, a comma separated list of boards not to run this task
49 on. Default is None, which allows this task to run on all boards.
Xixuan Wu0c76d5b2017-08-30 16:40:17 -070050 priority, the string name of a priority from constants.Priorities.
51 timeout, the max lifetime of the suite in hours.
52 cros_build_spec, spec used to determine the ChromeOS build to test
53 with a firmware build, e.g., tot, R41 etc.
54 firmware_rw_build_spec, spec used to determine the firmware RW build
55 test with a ChromeOS build.
56 firmware_ro_build_spec, spec used to determine the firmware RO build
57 test with a ChromeOS build.
58 test_source, the source of test code when firmware will be updated in
59 the test. The value can be 'firmware_rw', 'firmware_ro' or 'cros'.
60 job_retry, set to True to enable job-level retry. Default is False.
Xixuan Wu80531932017-10-12 17:26:51 -070061 no_delay, set to True to raise the priority of this task in task.
62 force, set to True to schedule this suite no matter whether there's
63 duplicate jobs before.
Xixuan Wu0c76d5b2017-08-30 16:40:17 -070064 queue, so the suite jobs can start running tests with no waiting.
65 hour, an integer specifying the hour that a nightly run should be
66 triggered, default is set to 21.
67 day, an integer specifying the day of a week that a weekly run should
68 be triggered, default is set to 5 (Saturday).
69 os_type, type of OS, e.g., cros, brillo, android. Default is cros.
70 The argument is required for android/brillo builds.
71 launch_control_branches, comma separated string of launch control
72 branches. The argument is required and only applicable for
73 android/brillo builds.
74 launch_control_targets, comma separated string of build targets for
75 launch control builds. The argument is required and only
76 applicable for android/brillo builds.
77 testbed_dut_count, number of duts to test when using a testbed.
78 tot: The tot manager for checking ToT. If it's None, a new tot_manager
79 instance will be initialized.
80 """
Xixuan Wu5451a662017-10-17 10:57:40 -070081 # Indicate whether there're suites get pushed into taskqueue for this task.
82 self.is_pushed = False
83
Xixuan Wu0c76d5b2017-08-30 16:40:17 -070084 self.name = task_info.name
85 self.suite = task_info.suite
86 self.branch_specs = task_info.branch_specs
87 self.pool = task_info.pool
88 self.num = task_info.num
89 self.priority = task_info.priority
90 self.timeout = task_info.timeout
91 self.cros_build_spec = task_info.cros_build_spec
92 self.firmware_rw_build_spec = task_info.firmware_rw_build_spec
93 self.firmware_ro_build_spec = task_info.firmware_ro_build_spec
94 self.test_source = task_info.test_source
95 self.job_retry = task_info.job_retry
96 self.no_delay = task_info.no_delay
Xixuan Wu80531932017-10-12 17:26:51 -070097 self.force = task_info.force
Xixuan Wu0c76d5b2017-08-30 16:40:17 -070098 self.os_type = task_info.os_type
99 self.testbed_dut_count = task_info.testbed_dut_count
Xixuan Wu008ee832017-10-12 16:59:34 -0700100 self.hour = task_info.hour
101 self.day = task_info.day
Craig Bergstrom58263d32018-04-26 14:11:35 -0600102 self.only_hwtest_sanity_required = task_info.only_hwtest_sanity_required
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700103
104 if task_info.lc_branches:
105 self.launch_control_branches = [
106 t.strip() for t in task_info.lc_branches.split(',')]
107 else:
108 self.launch_control_branches = []
109
110 if task_info.lc_targets:
111 self.launch_control_targets = [
112 t.strip() for t in task_info.lc_targets.split(',')]
113 else:
114 self.launch_control_targets = []
115
116 if task_info.boards:
117 self.boards = [t.strip() for t in task_info.boards.split(',')]
118 else:
119 self.boards = []
120
Po-Hsien Wang6d589732018-05-15 17:19:34 -0700121 if task_info.exclude_boards:
122 self.exclude_boards = [
123 t.strip() for t in task_info.exclude_boards.split(',')]
124 else:
125 self.exclude_boards = []
126
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700127 if tot is None:
128 self.tot_manager = tot_manager.TotMilestoneManager()
129 else:
130 self.tot_manager = tot
131
132 self._set_spec_compare_info()
133
Craig Bergstrom58263d32018-04-26 14:11:35 -0600134 def schedule(self, launch_control_builds, cros_builds_tuple, lab_config,
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700135 db_client):
136 """Schedule the task by its settings.
137
138 Args:
139 launch_control_builds: the build dict for Android boards, see
140 return value of |get_launch_control_builds|.
Craig Bergstrom58263d32018-04-26 14:11:35 -0600141 cros_builds_tuple: the two-tuple of build dicts for ChromeOS boards,
142 see return value of |get_cros_builds|.
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700143 lab_config: a config.LabConfig object, to read lab config file.
144 db_client: a cloud_sql_client.CIDBClient object to connect to cidb.
145
146 Raises:
147 SchedulingError: if tasks that should be scheduled fail to schedule.
Xixuan Wu5451a662017-10-17 10:57:40 -0700148
149 Returns:
150 A boolean indicator to show whether there're suites related to this task
151 get pushed into the suites queue.
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700152 """
153 assert lab_config is not None
Xixuan Wu5451a662017-10-17 10:57:40 -0700154 self.is_pushed = False
155
Craig Bergstrom58263d32018-04-26 14:11:35 -0600156 branch_builds, relaxed_builds = cros_builds_tuple
157 builds_dict = branch_builds
158 if self.only_hwtest_sanity_required:
159 builds_dict = relaxed_builds
160
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700161 logging.info('######## Scheduling task %s ########', self.name)
162 if self.os_type == build_lib.OS_TYPE_CROS:
Craig Bergstrom58263d32018-04-26 14:11:35 -0600163 if not builds_dict:
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700164 logging.info('No CrOS build to run, skip running.')
165 else:
Craig Bergstrom58263d32018-04-26 14:11:35 -0600166 self._schedule_cros_builds(builds_dict, lab_config, db_client)
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700167 else:
168 if not launch_control_builds:
169 logging.info('No Android build to run, skip running.')
170 else:
171 self._schedule_launch_control_builds(launch_control_builds)
172
Xixuan Wu5451a662017-10-17 10:57:40 -0700173 return self.is_pushed
174
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700175 def _set_spec_compare_info(self):
176 """Set branch spec compare info for task for further check."""
177 self._bare_branches = []
178 self._version_equal_constraint = False
179 self._version_gte_constraint = False
180 self._version_lte_constraint = False
181
182 if not self.branch_specs:
183 # Any milestone is OK.
184 self._numeric_constraint = version.LooseVersion('0')
185 else:
186 self._numeric_constraint = None
187 for spec in self.branch_specs:
188 if 'tot' in spec.lower():
189 # Convert spec >=tot-1 to >=RXX.
190 tot_str = spec[spec.index('tot'):]
191 spec = spec.replace(
192 tot_str, self.tot_manager.convert_tot_spec(tot_str))
193
194 if spec.startswith('>='):
195 self._numeric_constraint = version.LooseVersion(
196 spec.lstrip('>=R'))
197 self._version_gte_constraint = True
198 elif spec.startswith('<='):
199 self._numeric_constraint = version.LooseVersion(
200 spec.lstrip('<=R'))
201 self._version_lte_constraint = True
202 elif spec.startswith('=='):
203 self._version_equal_constraint = True
204 self._numeric_constraint = version.LooseVersion(
205 spec.lstrip('==R'))
206 else:
207 self._bare_branches.append(spec)
208
209 def _fits_spec(self, branch):
210 """Check if a branch is deemed OK by this task's branch specs.
211
212 Will return whether a branch 'fits' the specifications stored in this task.
213
214 Examples:
215 Assuming tot=R40
216 t = Task('Name', 'suite', ['factory', '>=tot-1'])
217 t._fits_spec('factory') # True
218 t._fits_spec('40') # True
219 t._fits_spec('38') # False
220 t._fits_spec('firmware') # False
221
222 Args:
223 branch: the branch to check.
224
225 Returns:
226 True if branch 'fits' with stored specs, False otherwise.
227 """
228 if branch in build_lib.BARE_BRANCHES:
229 return branch in self._bare_branches
230
231 if self._numeric_constraint:
232 if self._version_equal_constraint:
233 return version.LooseVersion(branch) == self._numeric_constraint
234 elif self._version_gte_constraint:
235 return version.LooseVersion(branch) >= self._numeric_constraint
236 elif self._version_lte_constraint:
237 return version.LooseVersion(branch) <= self._numeric_constraint
238 else:
239 return version.LooseVersion(branch) >= self._numeric_constraint
240 else:
241 return False
242
243 def _get_latest_firmware_build_from_db(self, spec, board, db_client):
244 """Get the latest firmware build from CIDB database.
245
246 Args:
247 spec: a string build spec for RO or RW firmware, eg. firmware,
248 cros. For RO firmware, the value can also be released_ro_X.
249 board: the board against which this task will run suite job.
250 db_client: a cloud_sql_client.CIDBClient object to connect to cidb.
251
252 Returns:
253 the latest firmware build.
254
255 Raises:
256 MySQLdb.OperationalError: if connection operations are not valid.
257 """
258 build_type = 'release' if spec == 'cros' else spec
259 # TODO(xixuan): not all firmware are saved in cidb.
260 build = db_client.get_latest_passed_builds('%s-%s' % (board, build_type))
261
262 if build is None:
263 return None
264 else:
265 latest_build = str(build_lib.CrOSBuild(
266 board, build_type, build.milestone, build.platform))
267 logging.debug('latest firmware build for %s-%s: %s', board,
268 build_type, latest_build)
269 return latest_build
270
271 def _get_latest_firmware_build_from_lab_config(self, spec, board,
272 lab_config):
273 """Get latest firmware from lab config file.
274
275 Args:
276 spec: a string build spec for RO or RW firmware, eg. firmware,
277 cros. For RO firmware, the value can also be released_ro_X.
278 board: a string board against which this task will run suite job.
279 lab_config: a config.LabConfig object, to read lab config file.
280
281 Returns:
282 A string latest firmware build.
283
284 Raises:
285 ValueError: if no firmware build list is found in lab config file.
286 """
287 index = int(spec[len(_RELEASE_RO_FIRMWARE_SPEC):])
288 released_ro_builds = lab_config.get_firmware_ro_build_list(
289 'RELEASED_RO_BUILDS_%s' % board).split(',')
290 if len(released_ro_builds) < index:
291 raise ValueError('No %dth ro_builds in the lab_config firmware '
292 'list %r' % (index, released_ro_builds))
293
294 logging.debug('Get ro_build: %s', released_ro_builds[index - 1])
295 return released_ro_builds[index - 1]
296
297 def _get_firmware_build(self, spec, board, lab_config, db_client):
298 """Get the firmware build name to test with ChromeOS build.
299
300 Args:
301 spec: a string build spec for RO or RW firmware, eg. firmware,
302 cros. For RO firmware, the value can also be released_ro_X.
303 board: a string board against which this task will run suite job.
304 lab_config: a config.LabConfig object, to read lab config file.
305 db_client: a cloud_sql_client.CIDBClient object to connect to cidb.
306
307 Returns:
308 A string firmware build name.
309
310 Raises:
311 ValueError: if failing to get firmware from lab config file;
312 MySQLdb.OperationalError: if DB connection is not available.
313 """
314 if not spec or spec == 'stable':
315 # TODO(crbug.com/577316): Query stable RO firmware.
316 logging.debug('%s RO firmware build is not supported.', spec)
317 return None
318
319 try:
320 if spec.startswith(_RELEASE_RO_FIRMWARE_SPEC):
321 # For RO firmware whose value is released_ro_X, where X is the index of
322 # the list defined in lab config file:
323 # CROS/RELEASED_RO_BUILDS_[board].
324 # For example, for spec 'released_ro_2', and lab config file
325 # CROS/RELEASED_RO_BUILDS_veyron_jerry: build1,build2,
Craig Bergstrom58263d32018-04-26 14:11:35 -0600326 # return firmware RO build should be 'build2'.
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700327 return self._get_latest_firmware_build_from_lab_config(
328 spec, board, lab_config)
329 else:
330 return self._get_latest_firmware_build_from_db(spec, board, db_client)
331 except ValueError as e:
332 logging.warning('Failed to get firmware from lab config file'
333 'for spec %s, board %s: %s', spec, board, str(e))
334 return None
335 except MySQLdb.OperationalError as e:
336 logging.warning('Failed to get firmware from cidb for spec %s, '
337 'board %s: %s', spec, board, str(e))
338 return None
339
C Shapiro7f24a002017-12-05 14:25:09 -0700340 def _push_suite(
341 self,
342 board=None,
343 model=None,
344 cros_build=None,
345 firmware_rw_build=None,
346 firmware_ro_build=None,
347 test_source_build=None,
348 launch_control_build=None,
349 run_prod_code=False):
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700350 """Schedule suite job for the task by pushing suites to SuiteQueue.
351
352 Args:
353 board: the board against which this suite job run.
C Shapiro7f24a002017-12-05 14:25:09 -0700354 model: the model name for unibuild.
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700355 cros_build: the CrOS build of this suite job.
356 firmware_rw_build: Firmware RW build to run this suite job with.
357 firmware_ro_build: Firmware RO build to run this suite job with.
358 test_source_build: Test source build, used for server-side
359 packaging of this suite job.
360 launch_control_build: the launch control build of this suite job.
361 run_prod_code: If True, the suite will run the test code that lives
362 in prod aka the test code currently on the lab servers. If
363 False, the control files and test code for this suite run will
364 be retrieved from the build artifacts. Default is False.
365 """
366 android_build = None
367 testbed_build = None
368
369 if self.testbed_dut_count:
370 launch_control_build = '%s#%d' % (launch_control_build,
371 self.testbed_dut_count)
372 test_source_build = launch_control_build
373 board = '%s-%d' % (board, self.testbed_dut_count)
374
375 if launch_control_build:
376 if not self.testbed_dut_count:
377 android_build = launch_control_build
378 else:
379 testbed_build = launch_control_build
380
381 suite_job_parameters = {
382 'suite': self.suite,
383 'board': board,
C Shapiro7f24a002017-12-05 14:25:09 -0700384 'model': model,
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700385 build_lib.BuildVersionKey.CROS_VERSION: cros_build,
386 build_lib.BuildVersionKey.FW_RW_VERSION: firmware_rw_build,
387 build_lib.BuildVersionKey.FW_RO_VERSION: firmware_ro_build,
388 build_lib.BuildVersionKey.ANDROID_BUILD_VERSION: android_build,
389 build_lib.BuildVersionKey.TESTBED_BUILD_VERSION: testbed_build,
390 'num': self.num,
391 'pool': self.pool,
392 'priority': self.priority,
393 'timeout': self.timeout,
394 'timeout_mins': _JOB_MAX_RUNTIME_MINS_DEFAULT,
395 'max_runtime_mins': _JOB_MAX_RUNTIME_MINS_DEFAULT,
Xixuan Wu2ba72652017-09-15 15:49:42 -0700396 'no_wait_for_results': not self.job_retry,
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700397 'test_source_build': test_source_build,
398 'job_retry': self.job_retry,
399 'no_delay': self.no_delay,
Xixuan Wu80531932017-10-12 17:26:51 -0700400 'force': self.force,
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700401 'run_prod_code': run_prod_code,
402 }
403
404 task_executor.push(task_executor.SUITES_QUEUE, **suite_job_parameters)
405 logging.info('Pushing task %r into taskqueue', suite_job_parameters)
Xixuan Wu5451a662017-10-17 10:57:40 -0700406 self.is_pushed = True
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700407
Craig Bergstrom58263d32018-04-26 14:11:35 -0600408 def _schedule_cros_builds(self, build_dict, lab_config, db_client):
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700409 """Schedule tasks with branch builds.
410
411 Args:
Craig Bergstrom58263d32018-04-26 14:11:35 -0600412 build_dict: the build dict for ChromeOS boards, see return
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700413 value of |build_lib.get_cros_builds_since_date_from_db|.
414 lab_config: a config.LabConfig object, to read lab config file.
415 db_client: a cloud_sql_client.CIDBClient object to connect to cidb.
416 """
C Shapiro7f24a002017-12-05 14:25:09 -0700417 models_by_board = lab_config.get_cros_model_map() if lab_config else {}
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700418 for (board, build_type,
Craig Bergstrom58263d32018-04-26 14:11:35 -0600419 milestone), manifest in build_dict.iteritems():
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700420 cros_build = str(build_lib.CrOSBuild(board, build_type, milestone,
421 manifest))
422 logging.info('Running %s on %s', self.name, cros_build)
Po-Hsien Wang6d589732018-05-15 17:19:34 -0700423 if self.exclude_boards and board in self.exclude_boards:
424 logging.debug('Board %s is in excluded board list: %s',
425 board, self.exclude_boards)
426 continue
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700427 if self.boards and board not in self.boards:
428 logging.debug('Board %s is not in supported board list: %s',
429 board, self.boards)
430 continue
431
432 # Check the fitness of the build's branch for task
433 branch_build_spec = _pick_branch(build_type, milestone)
434 if not self._fits_spec(branch_build_spec):
435 logging.debug("branch_build spec %s doesn't fit this task's "
436 "requirement: %s", branch_build_spec, self.branch_specs)
437 continue
438
439 firmware_rw_build = None
440 firmware_ro_build = None
441 if self.firmware_rw_build_spec or self.firmware_ro_build_spec:
442 firmware_rw_build = self._get_firmware_build(
443 self.firmware_rw_build_spec, board, lab_config, db_client)
444 firmware_ro_build = self._get_firmware_build(
445 self.firmware_ro_build_spec, board, lab_config, db_client)
446
447 if not firmware_ro_build and self.firmware_ro_build_spec:
448 logging.debug('No firmware ro build to run, skip running')
449 continue
450
451 if self.test_source == build_lib.BuildType.FIRMWARE_RW:
452 test_source_build = firmware_rw_build
453 else:
454 # Default test source build to CrOS build if it's not specified and
455 # run_prod_code is set to False, which is the case in current suite
456 # scheduler settings, where run_prod_code is always False.
457 test_source_build = cros_build
458
C Shapiro7f24a002017-12-05 14:25:09 -0700459 models = models_by_board.get(board, [None])
460
461 for model in models:
462 self._push_suite(board=board,
463 model=model,
464 cros_build=cros_build,
465 firmware_rw_build=firmware_rw_build,
466 firmware_ro_build=firmware_ro_build,
467 test_source_build=test_source_build)
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700468
469 def _schedule_launch_control_builds(self, launch_control_builds):
470 """Schedule tasks with launch control builds.
471
472 Args:
473 launch_control_builds: the build dict for Android boards.
474 """
475 for board, launch_control_build in launch_control_builds.iteritems():
476 logging.debug('Running %s on %s', self.name, board)
Po-Hsien Wang6d589732018-05-15 17:19:34 -0700477 if self.exclude_boards and board in self.exclude_boards:
478 logging.debug('Board %s is in excluded board list: %s',
479 board, self.exclude_boards)
480 continue
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700481 if self.boards and board not in self.boards:
482 logging.debug('Board %s is not in supported board list: %s',
483 board, self.boards)
484 continue
485
486 for android_build in launch_control_build:
487 if not any([branch in android_build
488 for branch in self.launch_control_branches]):
489 logging.debug('Branch %s is not required to run for task '
490 '%s', android_build, self.name)
491 continue
492
493 self._push_suite(board=board,
494 test_source_build=android_build,
495 launch_control_build=android_build)
496
497
498def _pick_branch(build_type, milestone):
499 """Select branch based on build type.
500
501 If the build_type is a bare branch, return build_type as the build spec.
502 If the build_type is a normal CrOS branch, return milestone as the build
503 spec.
504
505 Args:
506 build_type: a string builder name, like 'release'.
507 milestone: a string milestone, like '55'.
508
509 Returns:
510 A string milestone if build_type represents CrOS build, otherwise
511 return build_type.
512 """
513 return build_type if build_type in build_lib.BARE_BRANCHES else milestone