• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import copy
2import json
3import logging
4import re
5
6import common
7
8from autotest_lib.client.common_lib import autotemp
9from autotest_lib.client.common_lib import global_config
10
11
12# Try importing the essential bug reporting libraries. Chromite and gdata_lib
13# are useless unless they can import gdata too.
14try:
15    __import__('chromite')
16    __import__('gdata')
17except ImportError, e:
18    fundamental_libs = False
19    logging.debug('Will not be able to generate link '
20                  'to the buildbot page when filing bugs. %s', e)
21else:
22    from chromite.lib import cros_build_lib, gs
23    fundamental_libs = True
24
25
26# Number of times to retry if a gs command fails. Defaults to 10,
27# which is far too long given that we already wait on these files
28# before starting HWTests.
29_GS_RETRIES = 1
30
31
32_HTTP_ERROR_THRESHOLD = 400
33BUG_CONFIG_SECTION = 'BUG_REPORTING'
34
35# global configurations needed for build artifacts
36_gs_domain = global_config.global_config.get_config_value(
37    BUG_CONFIG_SECTION, 'gs_domain', default='')
38_chromeos_image_archive = global_config.global_config.get_config_value(
39    BUG_CONFIG_SECTION, 'chromeos_image_archive', default='')
40_arg_prefix = global_config.global_config.get_config_value(
41    BUG_CONFIG_SECTION, 'arg_prefix', default='')
42
43
44# global configurations needed for results log
45_retrieve_logs_cgi = global_config.global_config.get_config_value(
46    BUG_CONFIG_SECTION, 'retrieve_logs_cgi', default='')
47_generic_results_bin = global_config.global_config.get_config_value(
48    BUG_CONFIG_SECTION, 'generic_results_bin', default='')
49_debug_dir = global_config.global_config.get_config_value(
50    BUG_CONFIG_SECTION, 'debug_dir', default='')
51
52
53# Template for the url used to generate the link to the job
54_job_view = global_config.global_config.get_config_value(
55    BUG_CONFIG_SECTION, 'job_view', default='')
56
57
58# gs prefix to perform file like operations (gs://)
59_gs_file_prefix = global_config.global_config.get_config_value(
60    BUG_CONFIG_SECTION, 'gs_file_prefix', default='')
61
62
63# global configurations needed for buildbot stages link
64_buildbot_builders = global_config.global_config.get_config_value(
65    BUG_CONFIG_SECTION, 'buildbot_builders', default='')
66_build_prefix = global_config.global_config.get_config_value(
67    BUG_CONFIG_SECTION, 'build_prefix', default='')
68
69_CRBUG_URL = global_config.global_config.get_config_value(
70    BUG_CONFIG_SECTION, 'crbug_url')
71
72
73WMATRIX_RETRY_URL = global_config.global_config.get_config_value(
74    BUG_CONFIG_SECTION, 'wmatrix_retry_url')
75WMATRIX_TEST_HISTORY_URL = global_config.global_config.get_config_value(
76    BUG_CONFIG_SECTION, 'wmatrix_test_history_url')
77
78
79class InvalidBugTemplateException(Exception):
80    """Exception raised when a bug template is not valid, e.g., missing value
81    for essential attributes.
82    """
83    pass
84
85
86class BugTemplate(object):
87    """Wrapper class to merge a suite and test bug templates, and do validation.
88    """
89
90    # Names of expected attributes.
91    EXPECTED_BUG_TEMPLATE_ATTRIBUTES = ['owner', 'labels', 'status', 'title',
92                                        'cc', 'summary', 'components']
93    LIST_ATTRIBUTES = ['cc', 'labels']
94    EMAIL_ATTRIBUTES = ['owner', 'cc']
95
96    EMAIL_REGEX = re.compile(r'[^@]+@[^@]+\.[^@]+')
97
98
99    def __init__(self, bug_template):
100        """Initialize a BugTemplate object.
101
102        @param bug_template: initial bug template, e.g., bug template from suite
103                             control file.
104        """
105        self.bug_template = self.cleanup_bug_template(bug_template)
106
107
108    @classmethod
109    def validate_bug_template(cls, bug_template):
110        """Verify if a bug template has value for all essential attributes.
111
112        @param bug_template: bug template to be verified.
113        @raise InvalidBugTemplateException: raised when a bug template
114                is invalid, e.g., has missing essential attribute, or any given
115                template is not a dictionary.
116        """
117        if not type(bug_template) is dict:
118            raise InvalidBugTemplateException('Bug template must be a '
119                                              'dictionary.')
120
121        unexpected_keys = []
122        for key, value in bug_template.iteritems():
123            if not key in cls.EXPECTED_BUG_TEMPLATE_ATTRIBUTES:
124                raise InvalidBugTemplateException('Key %s is not expected in '
125                                                  'bug template.' % key)
126            if (key in cls.LIST_ATTRIBUTES and
127                not isinstance(value, list)):
128                raise InvalidBugTemplateException('Value for %s must be a list.'
129                                                  % key)
130            if key in cls.EMAIL_ATTRIBUTES:
131                emails = value if isinstance(value, list) else [value]
132                for email in emails:
133                    if not email or not cls.EMAIL_REGEX.match(email):
134                        raise InvalidBugTemplateException(
135                                'Invalid email address: %s.' % email)
136
137
138    @classmethod
139    def cleanup_bug_template(cls, bug_template):
140        """Remove empty entries in given bug template.
141
142        @param bug_template: bug template to be verified.
143
144        @return: A cleaned up bug template.
145        @raise InvalidBugTemplateException: raised when a bug template
146                is not a dictionary.
147        """
148        if not type(bug_template) is dict:
149            raise InvalidBugTemplateException('Bug template must be a '
150                                              'dictionary.')
151        template = copy.deepcopy(bug_template)
152        # If owner or cc is set but the value is empty or None, remove it from
153        # the template.
154        for email_attribute in cls.EMAIL_ATTRIBUTES:
155            if email_attribute in template:
156                value = template[email_attribute]
157                if isinstance(value, list):
158                    template[email_attribute] = [email for email in value
159                                                 if email]
160                if not template[email_attribute]:
161                    del(template[email_attribute])
162        return template
163
164
165    def finalize_bug_template(self, test_template):
166        """Merge test and suite bug templates.
167
168        @param test_template: Bug template from test control file.
169        @return: Merged bug template.
170
171        @raise InvalidBugTemplateException: raised when the merged template is
172                invalid, e.g., has missing essential attribute, or any given
173                template is not a dictionary.
174        """
175        test_template = self.cleanup_bug_template(test_template)
176        self.validate_bug_template(self.bug_template)
177        self.validate_bug_template(test_template)
178
179        merged_template = test_template
180        merged_template.update((k, v) for k, v in self.bug_template.iteritems()
181                               if k not in merged_template)
182
183        # test_template wins for common keys, unless values are list that can be
184        # merged.
185        for key in set(merged_template.keys()).intersection(
186                                                    self.bug_template.keys()):
187            if (type(merged_template[key]) is list and
188                type(self.bug_template[key]) is list):
189                merged_template[key] = (merged_template[key] +
190                                        self.bug_template[key])
191            elif not merged_template[key]:
192                merged_template[key] = self.bug_template[key]
193        self.validate_bug_template(merged_template)
194        return merged_template
195
196
197def link_build_artifacts(build):
198    """Returns a url to build artifacts on google storage.
199
200    @param build: A string, e.g. stout32-release/R30-4433.0.0
201
202    @returns: A url to build artifacts on google storage.
203
204    """
205    return (_gs_domain + _arg_prefix +
206            _chromeos_image_archive + build)
207
208
209def link_job(job_id, instance_server=None):
210    """Returns an url to the job on cautotest.
211
212    @param job_id: A string, representing the job id.
213    @param instance_server: The instance server.
214        Eg: cautotest, cautotest-cq, localhost.
215
216    @returns: An url to the job on cautotest.
217
218    """
219    if not job_id:
220        return 'Job did not run, or was aborted prematurely'
221    if not instance_server:
222        instance_server = global_config.global_config.get_config_value(
223            'SERVER', 'hostname', default='localhost')
224    if 'cautotest' in instance_server:
225        instance_server += '.corp.google.com'
226    return _job_view % (instance_server, job_id)
227
228
229def _base_results_log(job_id, result_owner, hostname):
230    """Returns the base url of the job's results.
231
232    @param job_id: A string, representing the job id.
233    @param result_owner: A string, representing the onwer of the job.
234    @param hostname: A string, representing the host on which
235                     the job has run.
236
237    @returns: The base url of the job's results.
238
239    """
240    if job_id and result_owner and hostname:
241        path_to_object = '%s-%s/%s' % (job_id, result_owner,
242                                       hostname)
243        return (_retrieve_logs_cgi + _generic_results_bin +
244                path_to_object)
245
246
247def link_result_logs(job_id, result_owner, hostname):
248    """Returns a url to test logs on google storage.
249
250    @param job_id: A string, representing the job id.
251    @param result_owner: A string, representing the owner of the job.
252    @param hostname: A string, representing the host on which the
253                     jot has run.
254
255    @returns: A url to test logs on google storage.
256
257    """
258    base_results = _base_results_log(job_id, result_owner, hostname)
259    if base_results:
260        return '%s/%s' % (base_results, _debug_dir)
261    return ('Could not generate results log: the job with id %s, '
262            'scheduled by: %s on host: %s did not run' %
263            (job_id, result_owner, hostname))
264
265
266def link_status_log(job_id, result_owner, hostname):
267    """Returns an url to status log of the job.
268
269    @param job_id: A string, representing the job id.
270    @param result_owner: A string, representing the owner of the job.
271    @param hostname: A string, representing the host on which the
272                     jot has run.
273
274    @returns: A url to status log of the job.
275
276    """
277    base_results = _base_results_log(job_id, result_owner, hostname)
278    if base_results:
279        return '%s/%s' % (base_results, 'status.log')
280    return 'NA'
281
282
283def _get_metadata_dict(build):
284    """
285    Get a dictionary of metadata related to this failure.
286
287    Metadata.json is created in the HWTest Archiving stage, if this file
288    isn't found the call to Cat will timeout after the number of retries
289    specified in the GSContext object. If metadata.json exists we parse
290    a json string of it's contents into a dictionary, which we return.
291
292    @param build: A string, e.g. stout32-release/R30-4433.0.0
293
294    @returns: A dictionary with the contents of metadata.json.
295
296    """
297    if not fundamental_libs:
298        return
299    try:
300        tempdir = autotemp.tempdir()
301        gs_context = gs.GSContext(retries=_GS_RETRIES,
302                                  cache_dir=tempdir.name)
303        gs_cmd = '%s%s%s/metadata.json' % (_gs_file_prefix,
304                                           _chromeos_image_archive,
305                                           build)
306        return json.loads(gs_context.Cat(gs_cmd))
307    except (cros_build_lib.RunCommandError, gs.GSContextException) as e:
308        logging.debug(e)
309    finally:
310        tempdir.clean()
311
312
313def link_buildbot_stages(build):
314    """
315    Link to the buildbot page associated with this run of HWTests.
316
317    @param build: A string, e.g. stout32-release/R30-4433.0.0
318
319    @return: A link to the buildbot stages page, or 'NA' if we cannot glean
320             enough information from metadata.json (or it doesn't exist).
321    """
322    metadata = _get_metadata_dict(build)
323    if (metadata and
324        metadata.get('builder-name') and
325        metadata.get('build-number')):
326
327        return ('%s%s/builds/%s' %
328                    (_buildbot_builders,
329                     metadata.get('builder-name'),
330                     metadata.get('build-number'))).replace(' ', '%20')
331    return 'NA'
332
333
334def link_retry_url(test_name):
335    """Link to the wmatrix retry stats page for this test.
336
337    @param test_name: Test we want to search the retry stats page for.
338
339    @return: A link to the wmatrix retry stats dashboard for this test.
340    """
341    return WMATRIX_RETRY_URL % test_name
342
343
344def link_test_history(test_name):
345  """Link to the wmatrix test history page for this test.
346
347  @param test_name: Test we want to search the test history for.
348
349  @return: A link to the wmatrix test history page for this test.
350  """
351  return WMATRIX_TEST_HISTORY_URL % test_name
352
353
354def link_crbug(bug_id):
355    """Generate a bug link for the given bug_id.
356
357    @param bug_id: The id of the bug.
358    @return: A link, eg: https://crbug.com/<bug_id>.
359    """
360    return _CRBUG_URL % (bug_id,)
361