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