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