1 # Copyright (c) 2013 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.
5 """Module containing the report stages."""
7 from __future__ import print_function
13 from chromite.cbuildbot import commands
14 from chromite.cbuildbot import constants
15 from chromite.cbuildbot import failures_lib
16 from chromite.cbuildbot import manifest_version
17 from chromite.cbuildbot import metadata_lib
18 from chromite.cbuildbot import results_lib
19 from chromite.cbuildbot import tree_status
20 from chromite.cbuildbot.stages import completion_stages
21 from chromite.cbuildbot.stages import generic_stages
22 from chromite.cbuildbot.stages import sync_stages
23 from chromite.lib import alerts
24 from chromite.lib import cidb
25 from chromite.lib import cros_build_lib
26 from chromite.lib import gs
27 from chromite.lib import osutils
28 from chromite.lib import toolchain
31 def WriteBasicMetadata(builder_run):
32 """Writes basic metadata that should be known at start of execution.
34 This method writes to |build_run|'s metadata instance the basic metadata
35 values that should be known at the beginning of the first cbuildbot
36 execution, prior to any reexecutions.
38 In particular, this method does not write any metadata values that depend
39 on the builder config, as the config may be modified by patches that are
40 applied before the final reexectuion.
42 This method is safe to run more than once (for instance, once per cbuildbot
43 execution) because it will write the same data each time.
46 builder_run: The BuilderRun instance for this build.
48 start_time = results_lib.Results.start_time
49 start_time_stamp = cros_build_lib.UserDateTimeFormat(timeval=start_time)
52 # Data for this build.
53 'bot-hostname': cros_build_lib.GetHostName(fully_qualified=True),
54 'build-number': builder_run.buildnumber,
55 'builder-name': os.environ.get('BUILDBOT_BUILDERNAME', ''),
56 'buildbot-url': os.environ.get('BUILDBOT_BUILDBOTURL', ''),
57 'buildbot-master-name':
58 os.environ.get('BUILDBOT_MASTERNAME', ''),
59 'bot-config': builder_run.config['name'],
61 'start': start_time_stamp,
63 'master_build_id': builder_run.options.master_build_id,
66 builder_run.attrs.metadata.UpdateWithDict(metadata)
69 class BuildStartStage(generic_stages.BuilderStage):
70 """The first stage to run.
72 This stage writes a few basic metadata values that are known at the start of
73 build, and inserts the build into the database, if appropriate.
75 @failures_lib.SetFailureType(failures_lib.InfrastructureFailure)
76 def PerformStage(self):
77 WriteBasicMetadata(self._run)
78 d = self._run.attrs.metadata.GetDict()
80 # BuildStartStage should only run once per build. But just in case it
81 # is somehow running a second time, we do not want to insert an additional
82 # database entry. Detect if a database entry has been inserted already
83 # and if so quit the stage.
85 logging.info('Already have build_id %s, not inserting an entry.',
89 if cidb.CIDBConnectionFactory.IsCIDBSetup():
90 db_type = cidb.CIDBConnectionFactory.GetCIDBConnectionType()
91 db = cidb.CIDBConnectionFactory.GetCIDBConnectionForBuilder()
93 waterfall = d['buildbot-master-name']
94 assert waterfall in ('chromeos', 'chromiumos', 'chromiumos.tryserver')
95 build_id = db.InsertBuild(
96 builder_name=d['builder-name'],
98 build_number=d['build-number'],
99 build_config=d['bot-config'],
100 bot_hostname=d['bot-hostname'],
101 master_build_id=d['master_build_id'])
102 self._run.attrs.metadata.UpdateWithDict({'build_id': build_id,
104 logging.info('Inserted build_id %s into cidb database.', build_id)
107 def HandleSkip(self):
108 """Ensure that re-executions use the same db instance as initial db."""
109 metadata_dict = self._run.attrs.metadata.GetDict()
110 if 'build_id' in metadata_dict:
111 db_type = cidb.CIDBConnectionFactory.GetCIDBConnectionType()
112 if not 'db_type' in metadata_dict:
113 # This will only execute while this CL is in the commit queue. After
114 # this CL lands, this block can be removed.
115 self._run.attrs.metadata.UpdateWithDict({'db_type': db_type})
118 if db_type != metadata_dict['db_type']:
119 cidb.CIDBConnectionFactory.InvalidateCIDBSetup()
120 raise AssertionError('Invalid attempt to switch from database %s to '
121 '%s.' % (metadata_dict['db_type'], db_type))
124 class BuildReexecutionFinishedStage(generic_stages.BuilderStage,
125 generic_stages.ArchivingStageMixin):
126 """The first stage to run after the final cbuildbot reexecution.
128 This stage is the first stage run after the final cbuildbot
129 bootstrap/reexecution. By the time this stage is run, the sync stages
130 are complete and version numbers of chromeos are known (though chrome
131 version may not be known until SyncChrome).
133 This stage writes metadata values that are first known after the final
134 reexecution (such as those that come from the config). This stage also
135 updates the build's cidb entry if appropriate.
137 Where possible, metadata that is already known at this time should be
138 written at this time rather than in ReportStage.
140 @failures_lib.SetFailureType(failures_lib.InfrastructureFailure)
141 def PerformStage(self):
142 config = self._run.config
143 build_root = self._build_root
145 # Flat list of all child config boards. Since child configs
146 # are not allowed to have children, it is not necessary to search
147 # deeper than one generation.
148 child_configs = [{'name': c['name'], 'boards' : c['boards']}
149 for c in config['child_configs']]
151 sdk_verinfo = cros_build_lib.LoadKeyValueFile(
152 os.path.join(build_root, constants.SDK_VERSION_FILE),
155 verinfo = self._run.GetVersionInfo(build_root)
156 platform_tag = getattr(self._run.attrs, 'release_tag')
158 platform_tag = verinfo.VersionString()
161 'full': self._run.GetVersion(),
162 'milestone': verinfo.chrome_branch,
163 'platform': platform_tag,
167 # Version of the metadata format.
168 'metadata-version': '2',
169 'boards': config['boards'],
170 'child-configs': child_configs,
171 'build_type': config['build_type'],
173 # Data for the toolchain used.
174 'sdk-version': sdk_verinfo.get('SDK_LATEST_VERSION', '<unknown>'),
175 'toolchain-url': sdk_verinfo.get('TC_PATH', '<unknown>'),
178 if len(config['boards']) == 1:
179 toolchains = toolchain.GetToolchainsForBoard(config['boards'][0],
180 buildroot=build_root)
181 metadata['toolchain-tuple'] = (
182 toolchain.FilterToolchains(toolchains, 'default', True).keys() +
183 toolchain.FilterToolchains(toolchains, 'default', False).keys())
185 logging.info('Metadata being written: %s', metadata)
186 self._run.attrs.metadata.UpdateWithDict(metadata)
187 # Update 'version' separately to avoid overwriting the existing
188 # entries in it (e.g. PFQ builders may have written the Chrome
190 logging.info("Metadata 'version' being written: %s", version)
191 self._run.attrs.metadata.UpdateKeyDictWithDict('version', version)
193 # Ensure that all boards and child config boards have a per-board
195 for b in config['boards']:
196 self._run.attrs.metadata.UpdateBoardDictWithDict(b, {})
198 for cc in child_configs:
199 for b in cc['boards']:
200 self._run.attrs.metadata.UpdateBoardDictWithDict(b, {})
202 # Upload build metadata (and write it to database if necessary)
203 self.UploadMetadata(filename=constants.PARTIAL_METADATA_JSON)
205 # Write child-per-build and board-per-build rows to database
206 if cidb.CIDBConnectionFactory.IsCIDBSetup():
207 db = cidb.CIDBConnectionFactory.GetCIDBConnectionForBuilder()
209 build_id = self._run.attrs.metadata.GetValue('build_id')
210 # TODO(akeshet): replace this with a GetValue call once crbug.com/406522
212 per_board_dict = self._run.attrs.metadata.GetDict()['board-metadata']
213 for board, board_metadata in per_board_dict.items():
214 db.InsertBoardPerBuild(build_id, board)
216 db.UpdateBoardPerBuildMetadata(build_id, board, board_metadata)
217 for child_config in self._run.attrs.metadata.GetValue('child-configs'):
218 db.InsertChildConfigPerBuild(build_id, child_config['name'])
221 class ReportStage(generic_stages.BuilderStage,
222 generic_stages.ArchivingStageMixin):
223 """Summarize all the builds."""
225 _HTML_HEAD = """<html>
227 <title>Archive Index: %(board)s / %(version)s</title>
230 <h2>Artifacts Index: %(board)s / %(version)s (%(config)s config)</h2>"""
232 def __init__(self, builder_run, sync_instance, completion_instance, **kwargs):
233 super(ReportStage, self).__init__(builder_run, **kwargs)
235 # TODO(mtennant): All these should be retrieved from builder_run instead.
236 # Or, more correctly, the info currently retrieved from these stages should
237 # be stored and retrieved from builder_run instead.
238 self._sync_instance = sync_instance
239 self._completion_instance = completion_instance
241 def _UpdateRunStreak(self, builder_run, final_status):
242 """Update the streak counter for this builder, if applicable, and notify.
244 Update the pass/fail streak counter for the builder. If the new
245 streak should trigger a notification email then send it now.
248 builder_run: BuilderRun for this run.
249 final_status: Final status string for this run.
252 # Exclude tryjobs from streak counting.
253 if not builder_run.options.remote_trybot and not builder_run.options.local:
254 streak_value = self._UpdateStreakCounter(
255 final_status=final_status, counter_name=builder_run.config.name,
256 dry_run=self._run.debug)
257 verb = 'passed' if streak_value > 0 else 'failed'
258 cros_build_lib.Info('Builder %s has %s %s time(s) in a row.',
259 builder_run.config.name, verb, abs(streak_value))
260 # See if updated streak should trigger a notification email.
261 if (builder_run.config.health_alert_recipients and
262 builder_run.config.health_threshold > 0 and
263 streak_value <= -builder_run.config.health_threshold):
265 'Builder failed %i consecutive times, sending health alert email '
268 builder_run.config.health_alert_recipients)
270 if not self._run.debug:
271 alerts.SendEmail('%s health alert' % builder_run.config.name,
272 tree_status.GetHealthAlertRecipients(builder_run),
273 message=self._HealthAlertMessage(-streak_value),
274 smtp_server=constants.GOLO_SMTP_SERVER,
275 extra_fields={'X-cbuildbot-alert': 'cq-health'})
277 def _UpdateStreakCounter(self, final_status, counter_name,
279 """Update the given streak counter based on the final status of build.
281 A streak counter counts the number of consecutive passes or failures of
282 a particular builder. Consecutive passes are indicated by a positive value,
283 consecutive failures by a negative value.
286 final_status: String indicating final status of build,
287 constants.FINAL_STATUS_PASSED indicating success.
288 counter_name: Name of counter to increment, typically the name of the
290 dry_run: Pretend to update counter only. Default: False.
293 The new value of the streak counter.
295 gs_ctx = gs.GSContext(dry_run=dry_run)
296 counter_url = os.path.join(constants.MANIFEST_VERSIONS_GS_URL,
297 constants.STREAK_COUNTERS,
299 gs_counter = gs.GSCounter(gs_ctx, counter_url)
301 if final_status == constants.FINAL_STATUS_PASSED:
302 streak_value = gs_counter.StreakIncrement()
304 streak_value = gs_counter.StreakDecrement()
308 def _HealthAlertMessage(self, fail_count):
309 """Returns the body of a health alert email message."""
310 return 'The builder named %s has failed %i consecutive times. See %s' % (
311 self._run.config['name'], fail_count, self.ConstructDashboardURL())
313 def _UploadMetadataForRun(self, final_status):
314 """Upload metadata.json for this entire run.
317 final_status: Final status string for this run.
319 self._run.attrs.metadata.UpdateWithDict(
320 self.GetReportMetadata(final_status=final_status,
321 sync_instance=self._sync_instance,
322 completion_instance=self._completion_instance))
323 self.UploadMetadata()
325 def _UploadArchiveIndex(self, builder_run):
326 """Upload an HTML index for the artifacts at remote archive location.
328 If there are no artifacts in the archive then do nothing.
331 builder_run: BuilderRun object for this run.
334 If an index file is uploaded then a dict is returned where each value
335 is the same (the URL for the uploaded HTML index) and the keys are
336 the boards it applies to, including None if applicable. If no index
337 file is uploaded then this returns None.
339 archive = builder_run.GetArchive()
340 archive_path = archive.archive_path
342 config = builder_run.config
343 boards = config.boards
345 board_names = ' '.join(boards)
348 board_names = '<no board>'
350 # See if there are any artifacts found for this run.
351 uploaded = os.path.join(archive_path, commands.UPLOADED_LIST_FILENAME)
352 if not os.path.exists(uploaded):
353 # UPLOADED doesn't exist. Normal if Archive stage never ran, which
354 # is possibly normal. Regardless, no archive index is needed.
355 logging.info('No archived artifacts found for %s run (%s)',
356 builder_run.config.name, board_names)
361 'board': board_names,
362 'config': config.name,
363 'version': builder_run.GetVersion(),
365 head = self._HTML_HEAD % head_data
367 files = osutils.ReadFile(uploaded).splitlines() + [
368 '.|Google Storage Index',
371 index = os.path.join(archive_path, 'index.html')
372 # TODO (sbasi) crbug.com/362776: Rework the way we do uploading to
373 # multiple buckets. Currently this can only be done in the Archive Stage
374 # therefore index.html will only end up in the normal Chrome OS bucket.
375 commands.GenerateHtmlIndex(index, files, url_base=archive.download_url,
377 commands.UploadArchivedFile(
378 archive_path, [archive.upload_url], os.path.basename(index),
379 debug=self._run.debug, acl=self.acl)
380 return dict((b, archive.download_url + '/index.html') for b in boards)
382 def GetReportMetadata(self, config=None, stage=None, final_status=None,
383 sync_instance=None, completion_instance=None):
384 """Generate ReportStage metadata.
387 config: The build config for this run. Defaults to self._run.config.
388 stage: The stage name that this metadata file is being uploaded for.
389 final_status: Whether the build passed or failed. If None, the build
390 will be treated as still running.
391 sync_instance: The stage instance that was used for syncing the source
392 code. This should be a derivative of SyncStage. If None, the list of
393 commit queue patches will not be included in the metadata.
394 completion_instance: The stage instance that was used to wait for slave
395 completion. Used to add slave build information to master builder's
396 metadata. If None, no such status information will be included. It not
397 None, this should be a derivative of MasterSlaveSyncCompletionStage.
400 A JSON-able dictionary representation of the metadata object.
402 builder_run = self._run
403 config = config or builder_run.config
405 commit_queue_stages = (sync_stages.CommitQueueSyncStage,
406 sync_stages.PreCQSyncStage)
407 get_changes_from_pool = (sync_instance and
408 isinstance(sync_instance, commit_queue_stages) and
411 get_statuses_from_slaves = (
413 completion_instance and
414 isinstance(completion_instance,
415 completion_stages.MasterSlaveSyncCompletionStage)
418 return metadata_lib.CBuildbotMetadata.GetReportMetadataDict(
419 builder_run, get_changes_from_pool,
420 get_statuses_from_slaves, config, stage, final_status, sync_instance,
423 def PerformStage(self):
424 # Make sure local archive directory is prepared, if it was not already.
425 # TODO(mtennant): It is not clear how this happens, but a CQ master run
426 # that never sees an open tree somehow reaches Report stage without a
427 # set up archive directory.
428 if not os.path.exists(self.archive_path):
429 self.archive.SetupArchivePath()
431 if results_lib.Results.BuildSucceededSoFar():
432 final_status = constants.FINAL_STATUS_PASSED
434 final_status = constants.FINAL_STATUS_FAILED
436 # Upload metadata, and update the pass/fail streak counter for the main
437 # run only. These aren't needed for the child builder runs.
438 self._UploadMetadataForRun(final_status)
439 self._UpdateRunStreak(self._run, final_status)
441 # Iterate through each builder run, whether there is just the main one
442 # or multiple child builder runs.
444 for builder_run in self._run.GetUngroupedBuilderRuns():
445 # Generate an index for archived artifacts if there are any. All the
446 # archived artifacts for one run/config are in one location, so the index
447 # is only specific to each run/config. In theory multiple boards could
448 # share that archive, but in practice it is usually one board. A
449 # run/config without a board will also usually not have artifacts to
450 # archive, but that restriction is not assumed here.
451 run_archive_urls = self._UploadArchiveIndex(builder_run)
453 archive_urls.update(run_archive_urls)
454 # Also update the LATEST files, since this run did archive something.
456 archive = builder_run.GetArchive()
457 # Check if the builder_run is tied to any boards and if so get all
459 upload_urls = self._GetUploadUrls('LATEST-*', builder_run=builder_run)
460 archive = builder_run.GetArchive()
462 archive.UpdateLatestMarkers(builder_run.manifest_branch,
464 upload_urls=upload_urls)
466 version = getattr(self._run.attrs, 'release_tag', '')
467 results_lib.Results.Report(sys.stdout, archive_urls=archive_urls,
468 current_version=version)
470 if cidb.CIDBConnectionFactory.IsCIDBSetup():
471 db = cidb.CIDBConnectionFactory.GetCIDBConnectionForBuilder()
473 build_id = self._run.attrs.metadata.GetValue('build_id')
474 # TODO(akeshet): Eliminate this status string translate once
475 # these differing status strings are merged, crbug.com/318930
476 if final_status == constants.FINAL_STATUS_PASSED:
477 status_for_db = manifest_version.BuilderStatus.STATUS_PASSED
479 status_for_db = manifest_version.BuilderStatus.STATUS_FAILED
481 # TODO(akeshet): Consider uploading the status pickle to the database,
482 # (by specifying that argument to FinishBuild), or come up with a
483 # pickle-free mechanism to describe failure details in database.
484 # TODO(akeshet): Find a clearer way to get the "primary upload url" for
485 # the metadata.json file. One alternative is _GetUploadUrls(...)[0].
486 # Today it seems that element 0 of its return list is the primary upload
487 # url, but there is no guarantee or unit test coverage of that.
488 metadata_url = os.path.join(self.upload_url, constants.METADATA_JSON)
489 db.FinishBuild(build_id, status=status_for_db,
490 metadata_url=metadata_url)
493 class RefreshPackageStatusStage(generic_stages.BuilderStage):
494 """Stage for refreshing Portage package status in online spreadsheet."""
495 def PerformStage(self):
496 commands.RefreshPackageStatus(buildroot=self._build_root,
498 debug=self._run.options.debug)