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