Xixuan Wu | 303a519 | 2017-08-29 11:10:42 -0700 | [diff] [blame] | 1 | # 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 of a basic event.""" |
| 6 | |
| 7 | import datetime |
| 8 | import logging |
Xinan Lin | 028f958 | 2019-12-11 10:55:33 -0800 | [diff] [blame] | 9 | import re |
Xixuan Wu | 303a519 | 2017-08-29 11:10:42 -0700 | [diff] [blame] | 10 | |
| 11 | import build_lib |
| 12 | import constants |
| 13 | import datastore_client |
Xixuan Wu | a5a2944 | 2017-10-11 11:03:02 -0700 | [diff] [blame] | 14 | import global_config |
Xixuan Wu | 303a519 | 2017-08-29 11:10:42 -0700 | [diff] [blame] | 15 | import task |
Xinan Lin | ea1efcb | 2019-12-30 23:46:42 -0800 | [diff] [blame] | 16 | import time_converter |
Xixuan Wu | 303a519 | 2017-08-29 11:10:42 -0700 | [diff] [blame] | 17 | |
| 18 | # The suffix for section |
| 19 | _SECTION_SUFFIX = '_params' |
| 20 | |
| 21 | |
| 22 | class BaseEvent(object): |
| 23 | """Basic class for a suite scheduler event.""" |
| 24 | |
| 25 | # The default keyword and priority for base event. Will be overwritten by |
| 26 | # its subclass. |
| 27 | KEYWORD = 'base' |
| 28 | PRIORITY = constants.Priorities.DEFAULT |
| 29 | |
| 30 | # The interval hours between consequent rounds of events. Default is 6. |
| 31 | LAST_EXEC_INTERVAL = 6 |
| 32 | |
| 33 | # The number of days between each event to trigger. Default is 1. |
| 34 | DAYS_INTERVAL = 1 |
| 35 | |
| 36 | # The max lifetime of suites kicked off by this event. |
| 37 | TIMEOUT = 24 # Hours |
| 38 | |
| 39 | def __init__(self, event_settings, last_exec_utc, target_exec_utc): |
| 40 | """Initialize a base event. |
| 41 | |
| 42 | Args: |
| 43 | event_settings: a config_reader.EventSettings object, indicating |
| 44 | the event settings. |
| 45 | last_exec_utc: The utc datetime.datetime timestamp of the last |
| 46 | execution of the event. |
| 47 | target_exec_utc: The utc datetime.datetime timestamp of the next |
| 48 | execution of the event. |
| 49 | """ |
| 50 | self._update_with_settings(event_settings) |
| 51 | self._datastore_client = None |
| 52 | self.task_list = [] |
| 53 | |
| 54 | # Processing executing time. |
| 55 | self.target_exec_utc = target_exec_utc |
| 56 | self._set_last_exec(last_exec_utc) |
| 57 | self._set_should_handle() |
| 58 | |
| 59 | logging.info('%s Event created:\ntarget_exec_time (utc): %s\n' |
| 60 | 'last_exec_time (utc): %s\nshould_handle: %s', |
| 61 | self.keyword, self.target_exec_utc, self.last_exec_utc, |
| 62 | self.should_handle) |
| 63 | |
| 64 | def set_task_list(self, task_list): |
| 65 | """Update task list with given input. |
| 66 | |
| 67 | Args: |
| 68 | task_list: a new task list for this event. |
| 69 | """ |
| 70 | self.task_list = list(task_list) |
| 71 | |
| 72 | def filter_tasks(self): |
| 73 | """Filter tasks from original task list. |
| 74 | |
| 75 | This could be overwritten by subclass of BaseEvent, like Nightly event, or |
| 76 | be directly used by event types like NewBuild. |
| 77 | """ |
| 78 | self.task_list = list(self.task_list) |
| 79 | |
Jack Neus | 2cbdc43 | 2022-06-24 20:21:30 +0000 | [diff] [blame] | 80 | def get_cros_builds(self, lab_config, build_client, stable_rubik_milestones=None): |
Xixuan Wu | 303a519 | 2017-08-29 11:10:42 -0700 | [diff] [blame] | 81 | """Get CrOS builds to run on for this event. |
| 82 | |
| 83 | Args: |
Xixuan Wu | 6fb1627 | 2017-10-19 13:16:00 -0700 | [diff] [blame] | 84 | lab_config: a config_reader.LabConfig object, to read lab configs. |
Xixuan Wu | c681901 | 2019-05-23 11:34:59 -0700 | [diff] [blame] | 85 | build_client: a rest_client.BuildBucketBigqueryClient object, to |
| 86 | connect Buildbucket Bigquery. |
Jack Neus | 2cbdc43 | 2022-06-24 20:21:30 +0000 | [diff] [blame] | 87 | stable_rubik_milestones: map of build target to stable Rubik milestone, |
| 88 | used to determine where we should use Rubik builds instead of Leagcy. |
Xixuan Wu | 303a519 | 2017-08-29 11:10:42 -0700 | [diff] [blame] | 89 | |
| 90 | Returns: |
Craig Bergstrom | 58263d3 | 2018-04-26 14:11:35 -0600 | [diff] [blame] | 91 | A two-tuples of dicts containing cros builds, see return from |
Xinan Lin | ea1efcb | 2019-12-30 23:46:42 -0800 | [diff] [blame] | 92 | |build_lib.get_cros_builds|. |
Xixuan Wu | 303a519 | 2017-08-29 11:10:42 -0700 | [diff] [blame] | 93 | """ |
Xinan Lin | 3330d67 | 2020-03-03 14:52:36 -0800 | [diff] [blame] | 94 | # delay_minutes ensures that most of the target builds were inserted to BQ. |
| 95 | delay_minutes = datetime.timedelta(minutes=constants.BaseEvent.DELAY_MINUTES) |
Xinan Lin | 71eeeb0 | 2020-03-10 17:37:12 -0700 | [diff] [blame] | 96 | return build_lib.get_cros_builds(build_client, |
| 97 | lab_config.get_cros_board_list(), |
| 98 | self.since_date - delay_minutes, |
Xinan Lin | cfa4101 | 2020-04-08 13:22:03 -0700 | [diff] [blame] | 99 | self.target_exec_utc - delay_minutes, |
Jack Neus | 8f0edb4 | 2022-03-17 20:21:39 +0000 | [diff] [blame] | 100 | self.keyword, |
Jack Neus | 2cbdc43 | 2022-06-24 20:21:30 +0000 | [diff] [blame] | 101 | stable_rubik_milestones=stable_rubik_milestones) |
Xixuan Wu | 303a519 | 2017-08-29 11:10:42 -0700 | [diff] [blame] | 102 | |
Jack Neus | 8f0edb4 | 2022-03-17 20:21:39 +0000 | [diff] [blame] | 103 | def get_recent_cros_builds(self, lab_config, build_client, delta_days=1, |
Jack Neus | 2cbdc43 | 2022-06-24 20:21:30 +0000 | [diff] [blame] | 104 | stable_rubik_milestones=None): |
Garry Wang | 4e29b51 | 2021-08-26 19:32:59 -0700 | [diff] [blame] | 105 | """Get most recent CrOS builds based on a given delta. |
Garry Wang | dce7757 | 2021-07-18 19:33:35 -0700 | [diff] [blame] | 106 | |
| 107 | Multi-DUTs testing can be triggered by new build of primary board, |
| 108 | however it may not necessary all secondary boards also have a new |
| 109 | build within the same time frame(from last exec to target exec), so |
| 110 | we need to determine builds for secondary boards based on recent build |
| 111 | info, and one day window should be good enough to at least contain one |
| 112 | build for every boards. |
| 113 | |
| 114 | Args: |
| 115 | lab_config: a config_reader.LabConfig object, to read lab configs. |
| 116 | build_client: a rest_client.BuildBucketBigqueryClient object, to |
| 117 | connect Buildbucket Bigquery. |
Garry Wang | 4e29b51 | 2021-08-26 19:32:59 -0700 | [diff] [blame] | 118 | delta_days: a int, to indicate time range of days(now - delta_days) |
| 119 | we want to get builds. |
Jack Neus | 2cbdc43 | 2022-06-24 20:21:30 +0000 | [diff] [blame] | 120 | stable_rubik_milestones: map of build target to stable Rubik milestone, |
| 121 | used to determine where we should use Rubik builds instead of Leagcy. |
Garry Wang | dce7757 | 2021-07-18 19:33:35 -0700 | [diff] [blame] | 122 | |
| 123 | Returns: |
| 124 | A two-tuples of dicts containing cros builds, see return from |
| 125 | |build_lib.get_cros_builds|. |
| 126 | """ |
| 127 | delay_minutes = datetime.timedelta(minutes=constants.BaseEvent.DELAY_MINUTES) |
Garry Wang | 4e29b51 | 2021-08-26 19:32:59 -0700 | [diff] [blame] | 128 | time_delta = datetime.timedelta(days=delta_days) |
| 129 | since_date = self.target_exec_utc - time_delta |
Garry Wang | dce7757 | 2021-07-18 19:33:35 -0700 | [diff] [blame] | 130 | return build_lib.get_cros_builds(build_client, |
| 131 | lab_config.get_cros_board_list(), |
| 132 | since_date - delay_minutes, |
| 133 | self.target_exec_utc - delay_minutes, |
Jack Neus | 8f0edb4 | 2022-03-17 20:21:39 +0000 | [diff] [blame] | 134 | self.keyword, |
Jack Neus | 2cbdc43 | 2022-06-24 20:21:30 +0000 | [diff] [blame] | 135 | stable_rubik_milestones=stable_rubik_milestones) |
Garry Wang | dce7757 | 2021-07-18 19:33:35 -0700 | [diff] [blame] | 136 | |
Xixuan Wu | 303a519 | 2017-08-29 11:10:42 -0700 | [diff] [blame] | 137 | def get_launch_control_builds(self, lab_config, android_client): |
| 138 | """Get launch control builds to run on for this event. |
| 139 | |
| 140 | Args: |
| 141 | lab_config: a config_reader.LabConfig object, to read lab configs. |
| 142 | android_client: a rest_client.AndroidBuildRestClient object to call |
| 143 | android build API. |
| 144 | |
| 145 | Returns: |
| 146 | a dict containing launch control builds for Android boards. See return |
| 147 | value of |build_lib.get_launch_control_builds_by_branch_targets|. |
| 148 | """ |
| 149 | return build_lib.get_launch_control_builds_by_branch_targets( |
| 150 | android_client, lab_config.get_android_board_list(), |
| 151 | self.launch_control_branch_targets) |
| 152 | |
Xinan Lin | 028f958 | 2019-12-11 10:55:33 -0800 | [diff] [blame] | 153 | def get_firmware_builds(self, build_client): |
| 154 | """Get the latest firmware builds for all boards. |
| 155 | |
| 156 | Args: |
| 157 | build_client: a rest_client.BuildBucketBigqueryClient object, to |
| 158 | connect Buildbucket Bigquery. |
| 159 | |
| 160 | Returns: |
| 161 | A dict of artifact link for the firmware. The key is |
| 162 | ([cros|firmware], board). |
| 163 | """ |
| 164 | firmware_builds = build_client.get_latest_passed_firmware_builds() |
| 165 | if not firmware_builds: |
| 166 | return None |
| 167 | firmware_build_dict = {} |
| 168 | ARTIFACT_PATTERN = r'gs://chromeos-image-archive/(?P<firmware_build>.+)' |
| 169 | for spec, board, artifact in firmware_builds: |
Sean McAllister | 4bc30fe | 2021-04-23 18:44:46 -0600 | [diff] [blame] | 170 | artifact = artifact or '' |
Xinan Lin | 028f958 | 2019-12-11 10:55:33 -0800 | [diff] [blame] | 171 | match = re.match(ARTIFACT_PATTERN, artifact) |
| 172 | if not match: |
| 173 | logging.debug('Artifact path of firmware is not valid: %s, %s, %s', |
| 174 | spec, board, artifact) |
| 175 | continue |
| 176 | firmware_build = match.group('firmware_build') |
| 177 | logging.debug('latest firmware build of (%s, %s): %s', |
| 178 | spec, board, firmware_build) |
| 179 | firmware_build_dict[(spec, board)] = firmware_build |
| 180 | return firmware_build_dict |
| 181 | |
Craig Bergstrom | 58263d3 | 2018-04-26 14:11:35 -0600 | [diff] [blame] | 182 | def process_tasks(self, launch_control_builds, cros_builds_tuple, |
Garry Wang | 4e29b51 | 2021-08-26 19:32:59 -0700 | [diff] [blame] | 183 | firmware_builds, configs, recent_cros_builds_tuple): |
Xixuan Wu | 303a519 | 2017-08-29 11:10:42 -0700 | [diff] [blame] | 184 | """Schedule tasks in task_list. |
| 185 | |
| 186 | Args: |
| 187 | launch_control_builds: the build dict for Android boards, see |
| 188 | return value of |get_launch_control_builds|. |
Craig Bergstrom | 58263d3 | 2018-04-26 14:11:35 -0600 | [diff] [blame] | 189 | cros_builds_tuple: a two-tuple of build dicts for ChromeOS boards, |
| 190 | see return value of |get_cros_builds|. |
Xinan Lin | 028f958 | 2019-12-11 10:55:33 -0800 | [diff] [blame] | 191 | firmware_builds: a dict of firmware artifact, see return value of |
| 192 | |get_firmware_builds|. |
Xixuan Wu | f4a4c88 | 2019-03-15 14:48:26 -0700 | [diff] [blame] | 193 | configs: a config_reader.Configs object, to contain several config |
| 194 | readers. |
Garry Wang | 4e29b51 | 2021-08-26 19:32:59 -0700 | [diff] [blame] | 195 | recent_cros_builds_tuple: Same as cros_builds_tuple, but contains |
| 196 | build info from a wider range(e.g. 3 days). |
Xixuan Wu | 303a519 | 2017-08-29 11:10:42 -0700 | [diff] [blame] | 197 | |
| 198 | Returns: |
| 199 | A list of finished tasks' names. |
| 200 | """ |
Xixuan Wu | 5451a66 | 2017-10-17 10:57:40 -0700 | [diff] [blame] | 201 | scheduled_tasks = [] |
Xixuan Wu | 303a519 | 2017-08-29 11:10:42 -0700 | [diff] [blame] | 202 | for per_task in self.task_list: |
| 203 | try: |
Garry Wang | dce7757 | 2021-07-18 19:33:35 -0700 | [diff] [blame] | 204 | scheduled = False |
| 205 | if per_task.is_multi_dut_testing: |
| 206 | scheduled = per_task.schedule_multi_duts( |
| 207 | cros_builds_tuple, |
Garry Wang | 4e29b51 | 2021-08-26 19:32:59 -0700 | [diff] [blame] | 208 | recent_cros_builds_tuple, |
Garry Wang | dce7757 | 2021-07-18 19:33:35 -0700 | [diff] [blame] | 209 | configs |
| 210 | ) |
| 211 | else: |
| 212 | scheduled = per_task.schedule( |
| 213 | launch_control_builds, |
| 214 | cros_builds_tuple, |
| 215 | firmware_builds, |
| 216 | configs |
| 217 | ) |
| 218 | if scheduled: |
Xixuan Wu | 5451a66 | 2017-10-17 10:57:40 -0700 | [diff] [blame] | 219 | scheduled_tasks.append(per_task.name) |
| 220 | else: |
| 221 | logging.debug('No suites are scheduled in suites queue for ' |
| 222 | 'task %s', per_task.name) |
Xixuan Wu | 303a519 | 2017-08-29 11:10:42 -0700 | [diff] [blame] | 223 | except task.SchedulingError: |
| 224 | logging.exception('Failed to schedule task: %s', per_task.name) |
| 225 | continue |
| 226 | |
Xixuan Wu | 5451a66 | 2017-10-17 10:57:40 -0700 | [diff] [blame] | 227 | return scheduled_tasks |
Xixuan Wu | 303a519 | 2017-08-29 11:10:42 -0700 | [diff] [blame] | 228 | |
| 229 | def finish(self): |
| 230 | """Execute all actions that once an event is finished.""" |
| 231 | self.datastore_client.set_last_execute_time( |
| 232 | self.keyword, self.target_exec_utc) |
| 233 | |
| 234 | def _update_with_settings(self, event_settings): |
| 235 | """Update event with given settings from config file. |
| 236 | |
| 237 | Args: |
| 238 | event_settings: a config_reader.EventSettings object, indicating |
| 239 | the event settings. |
| 240 | """ |
| 241 | self.always_handle = event_settings.always_handle is not None |
| 242 | if self.always_handle: |
| 243 | self.always_handle = event_settings.always_handle |
| 244 | |
| 245 | def _set_last_exec(self, last_exec_utc): |
| 246 | """Process and set last execute time. |
| 247 | |
| 248 | Set last_exec_utc. If last_exec_utc is too old or None, set it as |
| 249 | target_exec_utc. |
| 250 | |
| 251 | Args: |
| 252 | last_exec_utc: the utc datetime.datetime timestamp of last execute |
| 253 | time. None means no last_exec_utc saved for this event in |
| 254 | datastore. |
| 255 | """ |
| 256 | if last_exec_utc is not None: |
| 257 | self.last_exec_utc = last_exec_utc |
| 258 | |
| 259 | # If this TimedEvent has expired for a period of time, run its |
| 260 | # tasks on next available timepoint. |
| 261 | if (self.target_exec_utc - self.last_exec_utc > datetime.timedelta( |
| 262 | hours=self.LAST_EXEC_INTERVAL)): |
| 263 | self.last_exec_utc = self.target_exec_utc |
| 264 | else: |
| 265 | self.last_exec_utc = self.target_exec_utc |
| 266 | |
| 267 | def _set_should_handle(self): |
| 268 | """Update it's the time for the event to be handled.""" |
Xixuan Wu | a5a2944 | 2017-10-11 11:03:02 -0700 | [diff] [blame] | 269 | if self.always_handle or global_config.GAE_TESTING: |
Xixuan Wu | 303a519 | 2017-08-29 11:10:42 -0700 | [diff] [blame] | 270 | self.should_handle = True |
| 271 | else: |
| 272 | if self.last_exec_utc and self.last_exec_utc == self.target_exec_utc: |
| 273 | self.should_handle = False |
| 274 | else: |
| 275 | self.should_handle = True |
| 276 | |
| 277 | @classmethod |
| 278 | def section_name(cls): |
| 279 | """Getter for the section name of the event. |
| 280 | |
| 281 | Returns: |
| 282 | A string representing section name, refers to a section in config file |
| 283 | that contains this event's params. |
| 284 | """ |
| 285 | return cls.KEYWORD + _SECTION_SUFFIX |
| 286 | |
| 287 | @property |
| 288 | def datastore_client(self): |
| 289 | """Getter for private |self._datastore_client| property.""" |
| 290 | if self._datastore_client is None: |
| 291 | self._datastore_client = ( |
| 292 | datastore_client.LastExecutionRecordStore()) |
| 293 | |
| 294 | return self._datastore_client |
| 295 | |
| 296 | @property |
| 297 | def keyword(self): |
| 298 | """Getter for private |self.KEYWORD| property.""" |
| 299 | return self.KEYWORD |
| 300 | |
| 301 | @property |
| 302 | def launch_control_branch_targets(self): |
| 303 | """Get a dict of branch:targets for launch controls from all tasks.""" |
| 304 | branches = {} |
| 305 | for per_task in self.task_list: |
| 306 | for branch in per_task.launch_control_branches: |
| 307 | branches.setdefault(branch, []).extend( |
| 308 | per_task.launch_control_targets) |
| 309 | |
| 310 | return branches |