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