Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / third_party / chromite / cbuildbot / stages / report_stages.py
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.
4
5 """Module containing the report stages."""
6
7 from __future__ import print_function
8
9 import logging
10 import os
11 import sys
12
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
29
30
31 def WriteBasicMetadata(builder_run):
32   """Writes basic metadata that should be known at start of execution.
33
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.
37
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.
41
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.
44
45   Args:
46     builder_run: The BuilderRun instance for this build.
47   """
48   start_time = results_lib.Results.start_time
49   start_time_stamp = cros_build_lib.UserDateTimeFormat(timeval=start_time)
50
51   metadata = {
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'],
60       'time': {
61           'start': start_time_stamp,
62       },
63       'master_build_id': builder_run.options.master_build_id,
64   }
65
66   builder_run.attrs.metadata.UpdateWithDict(metadata)
67
68
69 class BuildStartStage(generic_stages.BuilderStage):
70   """The first stage to run.
71
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.
74   """
75   @failures_lib.SetFailureType(failures_lib.InfrastructureFailure)
76   def PerformStage(self):
77     WriteBasicMetadata(self._run)
78     d = self._run.attrs.metadata.GetDict()
79
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.
84     if 'build_id' in d:
85       logging.info('Already have build_id %s, not inserting an entry.',
86                    d['build_id'])
87       return
88
89     if cidb.CIDBConnectionFactory.IsCIDBSetup():
90       db_type = cidb.CIDBConnectionFactory.GetCIDBConnectionType()
91       db = cidb.CIDBConnectionFactory.GetCIDBConnectionForBuilder()
92       if db:
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'],
97              waterfall=waterfall,
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,
103                                                  'db_type': db_type})
104         logging.info('Inserted build_id %s into cidb database.', build_id)
105
106
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})
116         return
117
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))
122
123
124 class BuildReexecutionFinishedStage(generic_stages.BuilderStage,
125                                     generic_stages.ArchivingStageMixin):
126   """The first stage to run after the final cbuildbot reexecution.
127
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).
132
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.
136
137   Where possible, metadata that is already known at this time should be
138   written at this time rather than in ReportStage.
139   """
140   @failures_lib.SetFailureType(failures_lib.InfrastructureFailure)
141   def PerformStage(self):
142     config = self._run.config
143     build_root = self._build_root
144
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']]
150
151     sdk_verinfo = cros_build_lib.LoadKeyValueFile(
152         os.path.join(build_root, constants.SDK_VERSION_FILE),
153         ignore_missing=True)
154
155     verinfo = self._run.GetVersionInfo(build_root)
156     platform_tag = getattr(self._run.attrs, 'release_tag')
157     if not platform_tag:
158       platform_tag = verinfo.VersionString()
159
160     version = {
161             'full': self._run.GetVersion(),
162             'milestone': verinfo.chrome_branch,
163             'platform': platform_tag,
164     }
165
166     metadata = {
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'],
172
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>'),
176     }
177
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())
184
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
189     # version to uprev).
190     logging.info("Metadata 'version' being written: %s", version)
191     self._run.attrs.metadata.UpdateKeyDictWithDict('version', version)
192
193     # Ensure that all boards and child config boards have a per-board
194     # metadata subdict.
195     for b in config['boards']:
196       self._run.attrs.metadata.UpdateBoardDictWithDict(b, {})
197
198     for cc in child_configs:
199       for b in cc['boards']:
200         self._run.attrs.metadata.UpdateBoardDictWithDict(b, {})
201
202     # Upload build metadata (and write it to database if necessary)
203     self.UploadMetadata(filename=constants.PARTIAL_METADATA_JSON)
204
205     # Write child-per-build and board-per-build rows to database
206     if cidb.CIDBConnectionFactory.IsCIDBSetup():
207       db = cidb.CIDBConnectionFactory.GetCIDBConnectionForBuilder()
208       if db:
209         build_id = self._run.attrs.metadata.GetValue('build_id')
210         # TODO(akeshet): replace this with a GetValue call once crbug.com/406522
211         # is resolved
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)
215           if board_metadata:
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'])
219
220
221 class ReportStage(generic_stages.BuilderStage,
222                   generic_stages.ArchivingStageMixin):
223   """Summarize all the builds."""
224
225   _HTML_HEAD = """<html>
226 <head>
227  <title>Archive Index: %(board)s / %(version)s</title>
228 </head>
229 <body>
230 <h2>Artifacts Index: %(board)s / %(version)s (%(config)s config)</h2>"""
231
232   def __init__(self, builder_run, sync_instance, completion_instance, **kwargs):
233     super(ReportStage, self).__init__(builder_run, **kwargs)
234
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
240
241   def _UpdateRunStreak(self, builder_run, final_status):
242     """Update the streak counter for this builder, if applicable, and notify.
243
244     Update the pass/fail streak counter for the builder.  If the new
245     streak should trigger a notification email then send it now.
246
247     Args:
248       builder_run: BuilderRun for this run.
249       final_status: Final status string for this run.
250     """
251
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):
264         cros_build_lib.Info(
265             'Builder failed %i consecutive times, sending health alert email '
266             'to %s.',
267             -streak_value,
268             builder_run.config.health_alert_recipients)
269
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'})
276
277   def _UpdateStreakCounter(self, final_status, counter_name,
278                            dry_run=False):
279     """Update the given streak counter based on the final status of build.
280
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.
284
285     Args:
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
289                     build config.
290       dry_run: Pretend to update counter only. Default: False.
291
292     Returns:
293       The new value of the streak counter.
294     """
295     gs_ctx = gs.GSContext(dry_run=dry_run)
296     counter_url = os.path.join(constants.MANIFEST_VERSIONS_GS_URL,
297                                constants.STREAK_COUNTERS,
298                                counter_name)
299     gs_counter = gs.GSCounter(gs_ctx, counter_url)
300
301     if final_status == constants.FINAL_STATUS_PASSED:
302       streak_value = gs_counter.StreakIncrement()
303     else:
304       streak_value = gs_counter.StreakDecrement()
305
306     return streak_value
307
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())
312
313   def _UploadMetadataForRun(self, final_status):
314     """Upload metadata.json for this entire run.
315
316     Args:
317       final_status: Final status string for this run.
318     """
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()
324
325   def _UploadArchiveIndex(self, builder_run):
326     """Upload an HTML index for the artifacts at remote archive location.
327
328     If there are no artifacts in the archive then do nothing.
329
330     Args:
331       builder_run: BuilderRun object for this run.
332
333     Returns:
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.
338     """
339     archive = builder_run.GetArchive()
340     archive_path = archive.archive_path
341
342     config = builder_run.config
343     boards = config.boards
344     if boards:
345       board_names = ' '.join(boards)
346     else:
347       boards = [None]
348       board_names = '<no board>'
349
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)
357
358     else:
359       # Prepare html head.
360       head_data = {
361           'board': board_names,
362           'config': config.name,
363           'version': builder_run.GetVersion(),
364       }
365       head = self._HTML_HEAD % head_data
366
367       files = osutils.ReadFile(uploaded).splitlines() + [
368           '.|Google Storage Index',
369           '..|',
370       ]
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,
376                                  head=head)
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)
381
382   def GetReportMetadata(self, config=None, stage=None, final_status=None,
383                         sync_instance=None, completion_instance=None):
384     """Generate ReportStage metadata.
385
386    Args:
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.
398
399     Returns:
400       A JSON-able dictionary representation of the metadata object.
401     """
402     builder_run = self._run
403     config = config or builder_run.config
404
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
409                              sync_instance.pool)
410
411     get_statuses_from_slaves = (
412         config['master'] and
413         completion_instance and
414         isinstance(completion_instance,
415                    completion_stages.MasterSlaveSyncCompletionStage)
416     )
417
418     return metadata_lib.CBuildbotMetadata.GetReportMetadataDict(
419         builder_run, get_changes_from_pool,
420         get_statuses_from_slaves, config, stage, final_status, sync_instance,
421         completion_instance)
422
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()
430
431     if results_lib.Results.BuildSucceededSoFar():
432       final_status = constants.FINAL_STATUS_PASSED
433     else:
434       final_status = constants.FINAL_STATUS_FAILED
435
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)
440
441     # Iterate through each builder run, whether there is just the main one
442     # or multiple child builder runs.
443     archive_urls = {}
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)
452       if run_archive_urls:
453         archive_urls.update(run_archive_urls)
454         # Also update the LATEST files, since this run did archive something.
455
456         archive = builder_run.GetArchive()
457         # Check if the builder_run is tied to any boards and if so get all
458         # upload urls.
459         upload_urls = self._GetUploadUrls('LATEST-*', builder_run=builder_run)
460         archive = builder_run.GetArchive()
461
462         archive.UpdateLatestMarkers(builder_run.manifest_branch,
463                                     builder_run.debug,
464                                     upload_urls=upload_urls)
465
466     version = getattr(self._run.attrs, 'release_tag', '')
467     results_lib.Results.Report(sys.stdout, archive_urls=archive_urls,
468                                current_version=version)
469
470     if cidb.CIDBConnectionFactory.IsCIDBSetup():
471       db = cidb.CIDBConnectionFactory.GetCIDBConnectionForBuilder()
472       if db:
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
478         else:
479           status_for_db = manifest_version.BuilderStatus.STATUS_FAILED
480
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)
491
492
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,
497                                   boards=self._boards,
498                                   debug=self._run.options.debug)