blob: b6374435f972ad3b1bea2a3aef2e60e54d7151a5 [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.
48 priority, the string name of a priority from constants.Priorities.
49 timeout, the max lifetime of the suite in hours.
50 cros_build_spec, spec used to determine the ChromeOS build to test
51 with a firmware build, e.g., tot, R41 etc.
52 firmware_rw_build_spec, spec used to determine the firmware RW build
53 test with a ChromeOS build.
54 firmware_ro_build_spec, spec used to determine the firmware RO build
55 test with a ChromeOS build.
56 test_source, the source of test code when firmware will be updated in
57 the test. The value can be 'firmware_rw', 'firmware_ro' or 'cros'.
58 job_retry, set to True to enable job-level retry. Default is False.
Xixuan Wu80531932017-10-12 17:26:51 -070059 no_delay, set to True to raise the priority of this task in task.
60 force, set to True to schedule this suite no matter whether there's
61 duplicate jobs before.
Xixuan Wu0c76d5b2017-08-30 16:40:17 -070062 queue, so the suite jobs can start running tests with no waiting.
63 hour, an integer specifying the hour that a nightly run should be
64 triggered, default is set to 21.
65 day, an integer specifying the day of a week that a weekly run should
66 be triggered, default is set to 5 (Saturday).
67 os_type, type of OS, e.g., cros, brillo, android. Default is cros.
68 The argument is required for android/brillo builds.
69 launch_control_branches, comma separated string of launch control
70 branches. The argument is required and only applicable for
71 android/brillo builds.
72 launch_control_targets, comma separated string of build targets for
73 launch control builds. The argument is required and only
74 applicable for android/brillo builds.
75 testbed_dut_count, number of duts to test when using a testbed.
76 tot: The tot manager for checking ToT. If it's None, a new tot_manager
77 instance will be initialized.
78 """
Xixuan Wu5451a662017-10-17 10:57:40 -070079 # Indicate whether there're suites get pushed into taskqueue for this task.
80 self.is_pushed = False
81
Xixuan Wu0c76d5b2017-08-30 16:40:17 -070082 self.name = task_info.name
83 self.suite = task_info.suite
84 self.branch_specs = task_info.branch_specs
85 self.pool = task_info.pool
86 self.num = task_info.num
87 self.priority = task_info.priority
88 self.timeout = task_info.timeout
89 self.cros_build_spec = task_info.cros_build_spec
90 self.firmware_rw_build_spec = task_info.firmware_rw_build_spec
91 self.firmware_ro_build_spec = task_info.firmware_ro_build_spec
92 self.test_source = task_info.test_source
93 self.job_retry = task_info.job_retry
94 self.no_delay = task_info.no_delay
Xixuan Wu80531932017-10-12 17:26:51 -070095 self.force = task_info.force
Xixuan Wu0c76d5b2017-08-30 16:40:17 -070096 self.os_type = task_info.os_type
97 self.testbed_dut_count = task_info.testbed_dut_count
Xixuan Wu008ee832017-10-12 16:59:34 -070098 self.hour = task_info.hour
99 self.day = task_info.day
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700100
101 if task_info.lc_branches:
102 self.launch_control_branches = [
103 t.strip() for t in task_info.lc_branches.split(',')]
104 else:
105 self.launch_control_branches = []
106
107 if task_info.lc_targets:
108 self.launch_control_targets = [
109 t.strip() for t in task_info.lc_targets.split(',')]
110 else:
111 self.launch_control_targets = []
112
113 if task_info.boards:
114 self.boards = [t.strip() for t in task_info.boards.split(',')]
115 else:
116 self.boards = []
117
118 if tot is None:
119 self.tot_manager = tot_manager.TotMilestoneManager()
120 else:
121 self.tot_manager = tot
122
123 self._set_spec_compare_info()
124
125 def schedule(self, launch_control_builds, branch_builds, lab_config,
126 db_client):
127 """Schedule the task by its settings.
128
129 Args:
130 launch_control_builds: the build dict for Android boards, see
131 return value of |get_launch_control_builds|.
132 branch_builds: the build dict for ChromeOS boards, see return
133 value of |get_cros_builds|.
134 lab_config: a config.LabConfig object, to read lab config file.
135 db_client: a cloud_sql_client.CIDBClient object to connect to cidb.
136
137 Raises:
138 SchedulingError: if tasks that should be scheduled fail to schedule.
Xixuan Wu5451a662017-10-17 10:57:40 -0700139
140 Returns:
141 A boolean indicator to show whether there're suites related to this task
142 get pushed into the suites queue.
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700143 """
144 assert lab_config is not None
Xixuan Wu5451a662017-10-17 10:57:40 -0700145 self.is_pushed = False
146
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700147 logging.info('######## Scheduling task %s ########', self.name)
148 if self.os_type == build_lib.OS_TYPE_CROS:
149 if not branch_builds:
150 logging.info('No CrOS build to run, skip running.')
151 else:
152 self._schedule_cros_builds(branch_builds, lab_config, db_client)
153 else:
154 if not launch_control_builds:
155 logging.info('No Android build to run, skip running.')
156 else:
157 self._schedule_launch_control_builds(launch_control_builds)
158
Xixuan Wu5451a662017-10-17 10:57:40 -0700159 return self.is_pushed
160
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700161 def _set_spec_compare_info(self):
162 """Set branch spec compare info for task for further check."""
163 self._bare_branches = []
164 self._version_equal_constraint = False
165 self._version_gte_constraint = False
166 self._version_lte_constraint = False
167
168 if not self.branch_specs:
169 # Any milestone is OK.
170 self._numeric_constraint = version.LooseVersion('0')
171 else:
172 self._numeric_constraint = None
173 for spec in self.branch_specs:
174 if 'tot' in spec.lower():
175 # Convert spec >=tot-1 to >=RXX.
176 tot_str = spec[spec.index('tot'):]
177 spec = spec.replace(
178 tot_str, self.tot_manager.convert_tot_spec(tot_str))
179
180 if spec.startswith('>='):
181 self._numeric_constraint = version.LooseVersion(
182 spec.lstrip('>=R'))
183 self._version_gte_constraint = True
184 elif spec.startswith('<='):
185 self._numeric_constraint = version.LooseVersion(
186 spec.lstrip('<=R'))
187 self._version_lte_constraint = True
188 elif spec.startswith('=='):
189 self._version_equal_constraint = True
190 self._numeric_constraint = version.LooseVersion(
191 spec.lstrip('==R'))
192 else:
193 self._bare_branches.append(spec)
194
195 def _fits_spec(self, branch):
196 """Check if a branch is deemed OK by this task's branch specs.
197
198 Will return whether a branch 'fits' the specifications stored in this task.
199
200 Examples:
201 Assuming tot=R40
202 t = Task('Name', 'suite', ['factory', '>=tot-1'])
203 t._fits_spec('factory') # True
204 t._fits_spec('40') # True
205 t._fits_spec('38') # False
206 t._fits_spec('firmware') # False
207
208 Args:
209 branch: the branch to check.
210
211 Returns:
212 True if branch 'fits' with stored specs, False otherwise.
213 """
214 if branch in build_lib.BARE_BRANCHES:
215 return branch in self._bare_branches
216
217 if self._numeric_constraint:
218 if self._version_equal_constraint:
219 return version.LooseVersion(branch) == self._numeric_constraint
220 elif self._version_gte_constraint:
221 return version.LooseVersion(branch) >= self._numeric_constraint
222 elif self._version_lte_constraint:
223 return version.LooseVersion(branch) <= self._numeric_constraint
224 else:
225 return version.LooseVersion(branch) >= self._numeric_constraint
226 else:
227 return False
228
229 def _get_latest_firmware_build_from_db(self, spec, board, db_client):
230 """Get the latest firmware build from CIDB database.
231
232 Args:
233 spec: a string build spec for RO or RW firmware, eg. firmware,
234 cros. For RO firmware, the value can also be released_ro_X.
235 board: the board against which this task will run suite job.
236 db_client: a cloud_sql_client.CIDBClient object to connect to cidb.
237
238 Returns:
239 the latest firmware build.
240
241 Raises:
242 MySQLdb.OperationalError: if connection operations are not valid.
243 """
244 build_type = 'release' if spec == 'cros' else spec
245 # TODO(xixuan): not all firmware are saved in cidb.
246 build = db_client.get_latest_passed_builds('%s-%s' % (board, build_type))
247
248 if build is None:
249 return None
250 else:
251 latest_build = str(build_lib.CrOSBuild(
252 board, build_type, build.milestone, build.platform))
253 logging.debug('latest firmware build for %s-%s: %s', board,
254 build_type, latest_build)
255 return latest_build
256
257 def _get_latest_firmware_build_from_lab_config(self, spec, board,
258 lab_config):
259 """Get latest firmware from lab config file.
260
261 Args:
262 spec: a string build spec for RO or RW firmware, eg. firmware,
263 cros. For RO firmware, the value can also be released_ro_X.
264 board: a string board against which this task will run suite job.
265 lab_config: a config.LabConfig object, to read lab config file.
266
267 Returns:
268 A string latest firmware build.
269
270 Raises:
271 ValueError: if no firmware build list is found in lab config file.
272 """
273 index = int(spec[len(_RELEASE_RO_FIRMWARE_SPEC):])
274 released_ro_builds = lab_config.get_firmware_ro_build_list(
275 'RELEASED_RO_BUILDS_%s' % board).split(',')
276 if len(released_ro_builds) < index:
277 raise ValueError('No %dth ro_builds in the lab_config firmware '
278 'list %r' % (index, released_ro_builds))
279
280 logging.debug('Get ro_build: %s', released_ro_builds[index - 1])
281 return released_ro_builds[index - 1]
282
283 def _get_firmware_build(self, spec, board, lab_config, db_client):
284 """Get the firmware build name to test with ChromeOS build.
285
286 Args:
287 spec: a string build spec for RO or RW firmware, eg. firmware,
288 cros. For RO firmware, the value can also be released_ro_X.
289 board: a string board against which this task will run suite job.
290 lab_config: a config.LabConfig object, to read lab config file.
291 db_client: a cloud_sql_client.CIDBClient object to connect to cidb.
292
293 Returns:
294 A string firmware build name.
295
296 Raises:
297 ValueError: if failing to get firmware from lab config file;
298 MySQLdb.OperationalError: if DB connection is not available.
299 """
300 if not spec or spec == 'stable':
301 # TODO(crbug.com/577316): Query stable RO firmware.
302 logging.debug('%s RO firmware build is not supported.', spec)
303 return None
304
305 try:
306 if spec.startswith(_RELEASE_RO_FIRMWARE_SPEC):
307 # For RO firmware whose value is released_ro_X, where X is the index of
308 # the list defined in lab config file:
309 # CROS/RELEASED_RO_BUILDS_[board].
310 # For example, for spec 'released_ro_2', and lab config file
311 # CROS/RELEASED_RO_BUILDS_veyron_jerry: build1,build2,
312 # return firmare RO build should be 'build2'.
313 return self._get_latest_firmware_build_from_lab_config(
314 spec, board, lab_config)
315 else:
316 return self._get_latest_firmware_build_from_db(spec, board, db_client)
317 except ValueError as e:
318 logging.warning('Failed to get firmware from lab config file'
319 'for spec %s, board %s: %s', spec, board, str(e))
320 return None
321 except MySQLdb.OperationalError as e:
322 logging.warning('Failed to get firmware from cidb for spec %s, '
323 'board %s: %s', spec, board, str(e))
324 return None
325
C Shapiro7f24a002017-12-05 14:25:09 -0700326 def _push_suite(
327 self,
328 board=None,
329 model=None,
330 cros_build=None,
331 firmware_rw_build=None,
332 firmware_ro_build=None,
333 test_source_build=None,
334 launch_control_build=None,
335 run_prod_code=False):
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700336 """Schedule suite job for the task by pushing suites to SuiteQueue.
337
338 Args:
339 board: the board against which this suite job run.
C Shapiro7f24a002017-12-05 14:25:09 -0700340 model: the model name for unibuild.
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700341 cros_build: the CrOS build of this suite job.
342 firmware_rw_build: Firmware RW build to run this suite job with.
343 firmware_ro_build: Firmware RO build to run this suite job with.
344 test_source_build: Test source build, used for server-side
345 packaging of this suite job.
346 launch_control_build: the launch control build of this suite job.
347 run_prod_code: If True, the suite will run the test code that lives
348 in prod aka the test code currently on the lab servers. If
349 False, the control files and test code for this suite run will
350 be retrieved from the build artifacts. Default is False.
351 """
352 android_build = None
353 testbed_build = None
354
355 if self.testbed_dut_count:
356 launch_control_build = '%s#%d' % (launch_control_build,
357 self.testbed_dut_count)
358 test_source_build = launch_control_build
359 board = '%s-%d' % (board, self.testbed_dut_count)
360
361 if launch_control_build:
362 if not self.testbed_dut_count:
363 android_build = launch_control_build
364 else:
365 testbed_build = launch_control_build
366
367 suite_job_parameters = {
368 'suite': self.suite,
369 'board': board,
C Shapiro7f24a002017-12-05 14:25:09 -0700370 'model': model,
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700371 build_lib.BuildVersionKey.CROS_VERSION: cros_build,
372 build_lib.BuildVersionKey.FW_RW_VERSION: firmware_rw_build,
373 build_lib.BuildVersionKey.FW_RO_VERSION: firmware_ro_build,
374 build_lib.BuildVersionKey.ANDROID_BUILD_VERSION: android_build,
375 build_lib.BuildVersionKey.TESTBED_BUILD_VERSION: testbed_build,
376 'num': self.num,
377 'pool': self.pool,
378 'priority': self.priority,
379 'timeout': self.timeout,
380 'timeout_mins': _JOB_MAX_RUNTIME_MINS_DEFAULT,
381 'max_runtime_mins': _JOB_MAX_RUNTIME_MINS_DEFAULT,
Xixuan Wu2ba72652017-09-15 15:49:42 -0700382 'no_wait_for_results': not self.job_retry,
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700383 'test_source_build': test_source_build,
384 'job_retry': self.job_retry,
385 'no_delay': self.no_delay,
Xixuan Wu80531932017-10-12 17:26:51 -0700386 'force': self.force,
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700387 'run_prod_code': run_prod_code,
388 }
389
390 task_executor.push(task_executor.SUITES_QUEUE, **suite_job_parameters)
391 logging.info('Pushing task %r into taskqueue', suite_job_parameters)
Xixuan Wu5451a662017-10-17 10:57:40 -0700392 self.is_pushed = True
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700393
394 def _schedule_cros_builds(self, branch_builds, lab_config, db_client):
395 """Schedule tasks with branch builds.
396
397 Args:
398 branch_builds: the build dict for ChromeOS boards, see return
399 value of |build_lib.get_cros_builds_since_date_from_db|.
400 lab_config: a config.LabConfig object, to read lab config file.
401 db_client: a cloud_sql_client.CIDBClient object to connect to cidb.
402 """
C Shapiro7f24a002017-12-05 14:25:09 -0700403 models_by_board = lab_config.get_cros_model_map() if lab_config else {}
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700404 for (board, build_type,
405 milestone), manifest in branch_builds.iteritems():
406 cros_build = str(build_lib.CrOSBuild(board, build_type, milestone,
407 manifest))
408 logging.info('Running %s on %s', self.name, cros_build)
409 if self.boards and board not in self.boards:
410 logging.debug('Board %s is not in supported board list: %s',
411 board, self.boards)
412 continue
413
414 # Check the fitness of the build's branch for task
415 branch_build_spec = _pick_branch(build_type, milestone)
416 if not self._fits_spec(branch_build_spec):
417 logging.debug("branch_build spec %s doesn't fit this task's "
418 "requirement: %s", branch_build_spec, self.branch_specs)
419 continue
420
421 firmware_rw_build = None
422 firmware_ro_build = None
423 if self.firmware_rw_build_spec or self.firmware_ro_build_spec:
424 firmware_rw_build = self._get_firmware_build(
425 self.firmware_rw_build_spec, board, lab_config, db_client)
426 firmware_ro_build = self._get_firmware_build(
427 self.firmware_ro_build_spec, board, lab_config, db_client)
428
429 if not firmware_ro_build and self.firmware_ro_build_spec:
430 logging.debug('No firmware ro build to run, skip running')
431 continue
432
433 if self.test_source == build_lib.BuildType.FIRMWARE_RW:
434 test_source_build = firmware_rw_build
435 else:
436 # Default test source build to CrOS build if it's not specified and
437 # run_prod_code is set to False, which is the case in current suite
438 # scheduler settings, where run_prod_code is always False.
439 test_source_build = cros_build
440
C Shapiro7f24a002017-12-05 14:25:09 -0700441 models = models_by_board.get(board, [None])
442
443 for model in models:
444 self._push_suite(board=board,
445 model=model,
446 cros_build=cros_build,
447 firmware_rw_build=firmware_rw_build,
448 firmware_ro_build=firmware_ro_build,
449 test_source_build=test_source_build)
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700450
451 def _schedule_launch_control_builds(self, launch_control_builds):
452 """Schedule tasks with launch control builds.
453
454 Args:
455 launch_control_builds: the build dict for Android boards.
456 """
457 for board, launch_control_build in launch_control_builds.iteritems():
458 logging.debug('Running %s on %s', self.name, board)
459 if self.boards and board not in self.boards:
460 logging.debug('Board %s is not in supported board list: %s',
461 board, self.boards)
462 continue
463
464 for android_build in launch_control_build:
465 if not any([branch in android_build
466 for branch in self.launch_control_branches]):
467 logging.debug('Branch %s is not required to run for task '
468 '%s', android_build, self.name)
469 continue
470
471 self._push_suite(board=board,
472 test_source_build=android_build,
473 launch_control_build=android_build)
474
475
476def _pick_branch(build_type, milestone):
477 """Select branch based on build type.
478
479 If the build_type is a bare branch, return build_type as the build spec.
480 If the build_type is a normal CrOS branch, return milestone as the build
481 spec.
482
483 Args:
484 build_type: a string builder name, like 'release'.
485 milestone: a string milestone, like '55'.
486
487 Returns:
488 A string milestone if build_type represents CrOS build, otherwise
489 return build_type.
490 """
491 return build_type if build_type in build_lib.BARE_BRANCHES else milestone