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