blob: da376144632817175779543e5512e1b86159a7d8 [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
326 def _push_suite(self, board=None, cros_build=None, firmware_rw_build=None,
327 firmware_ro_build=None, test_source_build=None,
328 launch_control_build=None, run_prod_code=False):
329 """Schedule suite job for the task by pushing suites to SuiteQueue.
330
331 Args:
332 board: the board against which this suite job run.
333 cros_build: the CrOS build of this suite job.
334 firmware_rw_build: Firmware RW build to run this suite job with.
335 firmware_ro_build: Firmware RO build to run this suite job with.
336 test_source_build: Test source build, used for server-side
337 packaging of this suite job.
338 launch_control_build: the launch control build of this suite job.
339 run_prod_code: If True, the suite will run the test code that lives
340 in prod aka the test code currently on the lab servers. If
341 False, the control files and test code for this suite run will
342 be retrieved from the build artifacts. Default is False.
343 """
344 android_build = None
345 testbed_build = None
346
347 if self.testbed_dut_count:
348 launch_control_build = '%s#%d' % (launch_control_build,
349 self.testbed_dut_count)
350 test_source_build = launch_control_build
351 board = '%s-%d' % (board, self.testbed_dut_count)
352
353 if launch_control_build:
354 if not self.testbed_dut_count:
355 android_build = launch_control_build
356 else:
357 testbed_build = launch_control_build
358
359 suite_job_parameters = {
360 'suite': self.suite,
361 'board': board,
362 build_lib.BuildVersionKey.CROS_VERSION: cros_build,
363 build_lib.BuildVersionKey.FW_RW_VERSION: firmware_rw_build,
364 build_lib.BuildVersionKey.FW_RO_VERSION: firmware_ro_build,
365 build_lib.BuildVersionKey.ANDROID_BUILD_VERSION: android_build,
366 build_lib.BuildVersionKey.TESTBED_BUILD_VERSION: testbed_build,
367 'num': self.num,
368 'pool': self.pool,
369 'priority': self.priority,
370 'timeout': self.timeout,
371 'timeout_mins': _JOB_MAX_RUNTIME_MINS_DEFAULT,
372 'max_runtime_mins': _JOB_MAX_RUNTIME_MINS_DEFAULT,
Xixuan Wu2ba72652017-09-15 15:49:42 -0700373 'no_wait_for_results': not self.job_retry,
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700374 'test_source_build': test_source_build,
375 'job_retry': self.job_retry,
376 'no_delay': self.no_delay,
Xixuan Wu80531932017-10-12 17:26:51 -0700377 'force': self.force,
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700378 'run_prod_code': run_prod_code,
379 }
380
381 task_executor.push(task_executor.SUITES_QUEUE, **suite_job_parameters)
382 logging.info('Pushing task %r into taskqueue', suite_job_parameters)
Xixuan Wu5451a662017-10-17 10:57:40 -0700383 self.is_pushed = True
Xixuan Wu0c76d5b2017-08-30 16:40:17 -0700384
385 def _schedule_cros_builds(self, branch_builds, lab_config, db_client):
386 """Schedule tasks with branch builds.
387
388 Args:
389 branch_builds: the build dict for ChromeOS boards, see return
390 value of |build_lib.get_cros_builds_since_date_from_db|.
391 lab_config: a config.LabConfig object, to read lab config file.
392 db_client: a cloud_sql_client.CIDBClient object to connect to cidb.
393 """
394 for (board, build_type,
395 milestone), manifest in branch_builds.iteritems():
396 cros_build = str(build_lib.CrOSBuild(board, build_type, milestone,
397 manifest))
398 logging.info('Running %s on %s', self.name, cros_build)
399 if self.boards and board not in self.boards:
400 logging.debug('Board %s is not in supported board list: %s',
401 board, self.boards)
402 continue
403
404 # Check the fitness of the build's branch for task
405 branch_build_spec = _pick_branch(build_type, milestone)
406 if not self._fits_spec(branch_build_spec):
407 logging.debug("branch_build spec %s doesn't fit this task's "
408 "requirement: %s", branch_build_spec, self.branch_specs)
409 continue
410
411 firmware_rw_build = None
412 firmware_ro_build = None
413 if self.firmware_rw_build_spec or self.firmware_ro_build_spec:
414 firmware_rw_build = self._get_firmware_build(
415 self.firmware_rw_build_spec, board, lab_config, db_client)
416 firmware_ro_build = self._get_firmware_build(
417 self.firmware_ro_build_spec, board, lab_config, db_client)
418
419 if not firmware_ro_build and self.firmware_ro_build_spec:
420 logging.debug('No firmware ro build to run, skip running')
421 continue
422
423 if self.test_source == build_lib.BuildType.FIRMWARE_RW:
424 test_source_build = firmware_rw_build
425 else:
426 # Default test source build to CrOS build if it's not specified and
427 # run_prod_code is set to False, which is the case in current suite
428 # scheduler settings, where run_prod_code is always False.
429 test_source_build = cros_build
430
431 self._push_suite(board=board,
432 cros_build=cros_build,
433 firmware_rw_build=firmware_rw_build,
434 firmware_ro_build=firmware_ro_build,
435 test_source_build=test_source_build)
436
437 def _schedule_launch_control_builds(self, launch_control_builds):
438 """Schedule tasks with launch control builds.
439
440 Args:
441 launch_control_builds: the build dict for Android boards.
442 """
443 for board, launch_control_build in launch_control_builds.iteritems():
444 logging.debug('Running %s on %s', self.name, board)
445 if self.boards and board not in self.boards:
446 logging.debug('Board %s is not in supported board list: %s',
447 board, self.boards)
448 continue
449
450 for android_build in launch_control_build:
451 if not any([branch in android_build
452 for branch in self.launch_control_branches]):
453 logging.debug('Branch %s is not required to run for task '
454 '%s', android_build, self.name)
455 continue
456
457 self._push_suite(board=board,
458 test_source_build=android_build,
459 launch_control_build=android_build)
460
461
462def _pick_branch(build_type, milestone):
463 """Select branch based on build type.
464
465 If the build_type is a bare branch, return build_type as the build spec.
466 If the build_type is a normal CrOS branch, return milestone as the build
467 spec.
468
469 Args:
470 build_type: a string builder name, like 'release'.
471 milestone: a string milestone, like '55'.
472
473 Returns:
474 A string milestone if build_type represents CrOS build, otherwise
475 return build_type.
476 """
477 return build_type if build_type in build_lib.BARE_BRANCHES else milestone