1*9c5db199SXin Li# Lint as: python2, python3 2*9c5db199SXin Li# Copyright (c) 2012 The Chromium OS Authors. All rights reserved. 3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 4*9c5db199SXin Li# found in the LICENSE file. 5*9c5db199SXin Li 6*9c5db199SXin Li 7*9c5db199SXin Lifrom __future__ import absolute_import 8*9c5db199SXin Lifrom __future__ import division 9*9c5db199SXin Lifrom __future__ import print_function 10*9c5db199SXin Li 11*9c5db199SXin Liimport random 12*9c5db199SXin Liimport re 13*9c5db199SXin Liimport six 14*9c5db199SXin Li 15*9c5db199SXin Liimport common 16*9c5db199SXin Li 17*9c5db199SXin Lifrom autotest_lib.client.common_lib import global_config 18*9c5db199SXin Li 19*9c5db199SXin Li_CONFIG = global_config.global_config 20*9c5db199SXin Li 21*9c5db199SXin Li# comments injected into the control file. 22*9c5db199SXin Li_INJECT_BEGIN = '# INJECT_BEGIN - DO NOT DELETE THIS LINE' 23*9c5db199SXin Li_INJECT_END = '# INJECT_END - DO NOT DELETE LINE' 24*9c5db199SXin Li 25*9c5db199SXin Li 26*9c5db199SXin Li# The regex for an injected line in the control file with the format: 27*9c5db199SXin Li# varable_name=varable_value 28*9c5db199SXin Li_INJECT_VAR_RE = re.compile('^[_A-Za-z]\w*=.+$') 29*9c5db199SXin Li 30*9c5db199SXin Li 31*9c5db199SXin Lidef image_url_pattern(): 32*9c5db199SXin Li """Returns image_url_pattern from global_config.""" 33*9c5db199SXin Li return _CONFIG.get_config_value('CROS', 'image_url_pattern', type=str) 34*9c5db199SXin Li 35*9c5db199SXin Li 36*9c5db199SXin Lidef firmware_url_pattern(): 37*9c5db199SXin Li """Returns firmware_url_pattern from global_config.""" 38*9c5db199SXin Li return _CONFIG.get_config_value('CROS', 'firmware_url_pattern', type=str) 39*9c5db199SXin Li 40*9c5db199SXin Li 41*9c5db199SXin Lidef factory_image_url_pattern(): 42*9c5db199SXin Li """Returns path to factory image after it's been staged.""" 43*9c5db199SXin Li return _CONFIG.get_config_value('CROS', 'factory_image_url_pattern', 44*9c5db199SXin Li type=str) 45*9c5db199SXin Li 46*9c5db199SXin Li 47*9c5db199SXin Lidef sharding_factor(): 48*9c5db199SXin Li """Returns sharding_factor from global_config.""" 49*9c5db199SXin Li return _CONFIG.get_config_value('CROS', 'sharding_factor', type=int) 50*9c5db199SXin Li 51*9c5db199SXin Li 52*9c5db199SXin Lidef infrastructure_user(): 53*9c5db199SXin Li """Returns infrastructure_user from global_config.""" 54*9c5db199SXin Li return _CONFIG.get_config_value('CROS', 'infrastructure_user', type=str) 55*9c5db199SXin Li 56*9c5db199SXin Li 57*9c5db199SXin Lidef package_url_pattern(is_launch_control_build=False): 58*9c5db199SXin Li """Returns package_url_pattern from global_config. 59*9c5db199SXin Li 60*9c5db199SXin Li @param is_launch_control_build: True if the package url is for Launch 61*9c5db199SXin Li Control build. Default is False. 62*9c5db199SXin Li """ 63*9c5db199SXin Li if is_launch_control_build: 64*9c5db199SXin Li return _CONFIG.get_config_value('ANDROID', 'package_url_pattern', 65*9c5db199SXin Li type=str) 66*9c5db199SXin Li else: 67*9c5db199SXin Li return _CONFIG.get_config_value('CROS', 'package_url_pattern', type=str) 68*9c5db199SXin Li 69*9c5db199SXin Li 70*9c5db199SXin Lidef try_job_timeout_mins(): 71*9c5db199SXin Li """Returns try_job_timeout_mins from global_config.""" 72*9c5db199SXin Li return _CONFIG.get_config_value('SCHEDULER', 'try_job_timeout_mins', 73*9c5db199SXin Li type=int, default=4*60) 74*9c5db199SXin Li 75*9c5db199SXin Li 76*9c5db199SXin Lidef get_package_url(devserver_url, build): 77*9c5db199SXin Li """Returns the package url from the |devserver_url| and |build|. 78*9c5db199SXin Li 79*9c5db199SXin Li @param devserver_url: a string specifying the host to contact e.g. 80*9c5db199SXin Li http://my_host:9090. 81*9c5db199SXin Li @param build: the build/image string to use e.g. mario-release/R19-123.0.1. 82*9c5db199SXin Li @return the url where you can find the packages for the build. 83*9c5db199SXin Li """ 84*9c5db199SXin Li return package_url_pattern() % (devserver_url, build) 85*9c5db199SXin Li 86*9c5db199SXin Li 87*9c5db199SXin Lidef get_devserver_build_from_package_url(package_url, 88*9c5db199SXin Li is_launch_control_build=False): 89*9c5db199SXin Li """The inverse method of get_package_url. 90*9c5db199SXin Li 91*9c5db199SXin Li @param package_url: a string specifying the package url. 92*9c5db199SXin Li @param is_launch_control_build: True if the package url is for Launch 93*9c5db199SXin Li Control build. Default is False. 94*9c5db199SXin Li 95*9c5db199SXin Li @return tuple containing the devserver_url, build. 96*9c5db199SXin Li """ 97*9c5db199SXin Li pattern = package_url_pattern(is_launch_control_build) 98*9c5db199SXin Li re_pattern = pattern.replace('%s', '(\S+)') 99*9c5db199SXin Li 100*9c5db199SXin Li devserver_build_tuple = re.search(re_pattern, package_url).groups() 101*9c5db199SXin Li 102*9c5db199SXin Li # TODO(beeps): This is a temporary hack around the fact that all 103*9c5db199SXin Li # job_repo_urls in the database currently contain 'archive'. Remove 104*9c5db199SXin Li # when all hosts have been reimaged at least once. Ref: crbug.com/214373. 105*9c5db199SXin Li return (devserver_build_tuple[0], 106*9c5db199SXin Li devserver_build_tuple[1].replace('archive/', '')) 107*9c5db199SXin Li 108*9c5db199SXin Li 109*9c5db199SXin Lidef get_build_from_image(image): 110*9c5db199SXin Li """Get the build name from the image string. 111*9c5db199SXin Li 112*9c5db199SXin Li @param image: A string of image, can be the build name or a url to the 113*9c5db199SXin Li build, e.g., 114*9c5db199SXin Li http://devserver/update/alex-release/R27-3837.0.0 115*9c5db199SXin Li 116*9c5db199SXin Li @return: Name of the build. Return None if fail to parse build name. 117*9c5db199SXin Li """ 118*9c5db199SXin Li if not image.startswith('http://'): 119*9c5db199SXin Li return image 120*9c5db199SXin Li else: 121*9c5db199SXin Li match = re.match('.*/([^/]+/R\d+-[^/]+)', image) 122*9c5db199SXin Li if match: 123*9c5db199SXin Li return match.group(1) 124*9c5db199SXin Li 125*9c5db199SXin Li 126*9c5db199SXin Lidef get_random_best_host(afe, host_list, require_usable_hosts=True): 127*9c5db199SXin Li """ 128*9c5db199SXin Li Randomly choose the 'best' host from host_list, using fresh status. 129*9c5db199SXin Li 130*9c5db199SXin Li Hit the AFE to get latest status for the listed hosts. Then apply 131*9c5db199SXin Li the following heuristic to pick the 'best' set: 132*9c5db199SXin Li 133*9c5db199SXin Li Remove unusable hosts (not tools.is_usable()), then 134*9c5db199SXin Li 'Ready' > 'Running, Cleaning, Verifying, etc' 135*9c5db199SXin Li 136*9c5db199SXin Li If any 'Ready' hosts exist, return a random choice. If not, randomly 137*9c5db199SXin Li choose from the next tier. If there are none of those either, None. 138*9c5db199SXin Li 139*9c5db199SXin Li @param afe: autotest front end that holds the hosts being managed. 140*9c5db199SXin Li @param host_list: an iterable of Host objects, per server/frontend.py 141*9c5db199SXin Li @param require_usable_hosts: only return hosts currently in a usable 142*9c5db199SXin Li state. 143*9c5db199SXin Li @return a Host object, or None if no appropriate host is found. 144*9c5db199SXin Li """ 145*9c5db199SXin Li if not host_list: 146*9c5db199SXin Li return None 147*9c5db199SXin Li hostnames = [host.hostname for host in host_list] 148*9c5db199SXin Li updated_hosts = afe.get_hosts(hostnames=hostnames) 149*9c5db199SXin Li usable_hosts = [host for host in updated_hosts if is_usable(host)] 150*9c5db199SXin Li ready_hosts = [host for host in usable_hosts if host.status == 'Ready'] 151*9c5db199SXin Li unusable_hosts = [h for h in updated_hosts if not is_usable(h)] 152*9c5db199SXin Li if ready_hosts: 153*9c5db199SXin Li return random.choice(ready_hosts) 154*9c5db199SXin Li if usable_hosts: 155*9c5db199SXin Li return random.choice(usable_hosts) 156*9c5db199SXin Li if not require_usable_hosts and unusable_hosts: 157*9c5db199SXin Li return random.choice(unusable_hosts) 158*9c5db199SXin Li return None 159*9c5db199SXin Li 160*9c5db199SXin Li 161*9c5db199SXin Lidef remove_legacy_injection(control_file_in): 162*9c5db199SXin Li """ 163*9c5db199SXin Li Removes the legacy injection part from a control file. 164*9c5db199SXin Li 165*9c5db199SXin Li @param control_file_in: the contents of a control file to munge. 166*9c5db199SXin Li 167*9c5db199SXin Li @return The modified control file string. 168*9c5db199SXin Li """ 169*9c5db199SXin Li if not control_file_in: 170*9c5db199SXin Li return control_file_in 171*9c5db199SXin Li 172*9c5db199SXin Li new_lines = [] 173*9c5db199SXin Li lines = control_file_in.strip().splitlines() 174*9c5db199SXin Li remove_done = False 175*9c5db199SXin Li for line in lines: 176*9c5db199SXin Li if remove_done: 177*9c5db199SXin Li new_lines.append(line) 178*9c5db199SXin Li else: 179*9c5db199SXin Li if not _INJECT_VAR_RE.match(line): 180*9c5db199SXin Li remove_done = True 181*9c5db199SXin Li new_lines.append(line) 182*9c5db199SXin Li return '\n'.join(new_lines) 183*9c5db199SXin Li 184*9c5db199SXin Li 185*9c5db199SXin Lidef remove_injection(control_file_in): 186*9c5db199SXin Li """ 187*9c5db199SXin Li Removes the injection part from a control file. 188*9c5db199SXin Li 189*9c5db199SXin Li @param control_file_in: the contents of a control file to munge. 190*9c5db199SXin Li 191*9c5db199SXin Li @return The modified control file string. 192*9c5db199SXin Li """ 193*9c5db199SXin Li if not control_file_in: 194*9c5db199SXin Li return control_file_in 195*9c5db199SXin Li 196*9c5db199SXin Li start = control_file_in.find(_INJECT_BEGIN) 197*9c5db199SXin Li if start >=0: 198*9c5db199SXin Li end = control_file_in.find(_INJECT_END, start) 199*9c5db199SXin Li if start < 0 or end < 0: 200*9c5db199SXin Li return remove_legacy_injection(control_file_in) 201*9c5db199SXin Li 202*9c5db199SXin Li end += len(_INJECT_END) 203*9c5db199SXin Li ch = control_file_in[end] 204*9c5db199SXin Li total_length = len(control_file_in) 205*9c5db199SXin Li while end <= total_length and ( 206*9c5db199SXin Li ch == '\n' or ch == ' ' or ch == '\t'): 207*9c5db199SXin Li end += 1 208*9c5db199SXin Li if end < total_length: 209*9c5db199SXin Li ch = control_file_in[end] 210*9c5db199SXin Li return control_file_in[:start] + control_file_in[end:] 211*9c5db199SXin Li 212*9c5db199SXin Li 213*9c5db199SXin Lidef inject_vars(vars, control_file_in): 214*9c5db199SXin Li """ 215*9c5db199SXin Li Inject the contents of |vars| into |control_file_in|. 216*9c5db199SXin Li 217*9c5db199SXin Li @param vars: a dict to shoehorn into the provided control file string. 218*9c5db199SXin Li @param control_file_in: the contents of a control file to munge. 219*9c5db199SXin Li @return the modified control file string. 220*9c5db199SXin Li """ 221*9c5db199SXin Li control_file = '' 222*9c5db199SXin Li control_file += _INJECT_BEGIN + '\n' 223*9c5db199SXin Li for key, value in six.iteritems(vars): 224*9c5db199SXin Li # None gets injected as 'None' without this check; same for digits. 225*9c5db199SXin Li if isinstance(value, str): 226*9c5db199SXin Li control_file += "%s=%s\n" % (key, repr(value)) 227*9c5db199SXin Li else: 228*9c5db199SXin Li control_file += "%s=%r\n" % (key, value) 229*9c5db199SXin Li 230*9c5db199SXin Li args_dict_str = "%s=%s\n" % ('args_dict', repr(vars)) 231*9c5db199SXin Li return control_file + args_dict_str + _INJECT_END + '\n' + control_file_in 232*9c5db199SXin Li 233*9c5db199SXin Li 234*9c5db199SXin Lidef is_usable(host): 235*9c5db199SXin Li """ 236*9c5db199SXin Li Given a host, determine if the host is usable right now. 237*9c5db199SXin Li 238*9c5db199SXin Li @param host: Host instance (as in server/frontend.py) 239*9c5db199SXin Li @return True if host is alive and not incorrectly locked. Else, False. 240*9c5db199SXin Li """ 241*9c5db199SXin Li return alive(host) and not incorrectly_locked(host) 242*9c5db199SXin Li 243*9c5db199SXin Li 244*9c5db199SXin Lidef alive(host): 245*9c5db199SXin Li """ 246*9c5db199SXin Li Given a host, determine if the host is alive. 247*9c5db199SXin Li 248*9c5db199SXin Li @param host: Host instance (as in server/frontend.py) 249*9c5db199SXin Li @return True if host is not under, or in need of, repair. Else, False. 250*9c5db199SXin Li """ 251*9c5db199SXin Li return host.status not in ['Repair Failed', 'Repairing'] 252*9c5db199SXin Li 253*9c5db199SXin Li 254*9c5db199SXin Lidef incorrectly_locked(host): 255*9c5db199SXin Li """ 256*9c5db199SXin Li Given a host, determine if the host is locked by some user. 257*9c5db199SXin Li 258*9c5db199SXin Li If the host is unlocked, or locked by the test infrastructure, 259*9c5db199SXin Li this will return False. There is only one system user defined as part 260*9c5db199SXin Li of the test infrastructure and is listed in global_config.ini under the 261*9c5db199SXin Li [CROS] section in the 'infrastructure_user' field. 262*9c5db199SXin Li 263*9c5db199SXin Li @param host: Host instance (as in server/frontend.py) 264*9c5db199SXin Li @return False if the host is not locked, or locked by the infra. 265*9c5db199SXin Li True if the host is locked by the infra user. 266*9c5db199SXin Li """ 267*9c5db199SXin Li return (host.locked and host.locked_by != infrastructure_user()) 268*9c5db199SXin Li 269*9c5db199SXin Li 270*9c5db199SXin Lidef _testname_to_keyval_key(testname): 271*9c5db199SXin Li """Make a test name acceptable as a keyval key. 272*9c5db199SXin Li 273*9c5db199SXin Li @param testname Test name that must be converted. 274*9c5db199SXin Li @return A string with selected bad characters replaced 275*9c5db199SXin Li with allowable characters. 276*9c5db199SXin Li """ 277*9c5db199SXin Li # Characters for keys in autotest keyvals are restricted; in 278*9c5db199SXin Li # particular, '/' isn't allowed. Alas, in the case of an 279*9c5db199SXin Li # aborted job, the test name will be a path that includes '/' 280*9c5db199SXin Li # characters. We want to file bugs for aborted jobs, so we 281*9c5db199SXin Li # apply a transform here to avoid trouble. 282*9c5db199SXin Li return testname.replace('/', '_') 283*9c5db199SXin Li 284*9c5db199SXin Li 285*9c5db199SXin Li_BUG_ID_KEYVAL = '-Bug_Id' 286*9c5db199SXin Li_BUG_COUNT_KEYVAL = '-Bug_Count' 287*9c5db199SXin Li 288*9c5db199SXin Li 289*9c5db199SXin Lidef create_bug_keyvals(job_id, testname, bug_info): 290*9c5db199SXin Li """Create keyvals to record a bug filed against a test failure. 291*9c5db199SXin Li 292*9c5db199SXin Li @param testname Name of the test for which to record a bug. 293*9c5db199SXin Li @param bug_info Pair with the id of the bug and the count of 294*9c5db199SXin Li the number of times the bug has been seen. 295*9c5db199SXin Li @param job_id The afe job id of job which the test is associated to. 296*9c5db199SXin Li job_id will be a part of the key. 297*9c5db199SXin Li @return Keyvals to be recorded for the given test. 298*9c5db199SXin Li """ 299*9c5db199SXin Li testname = _testname_to_keyval_key(testname) 300*9c5db199SXin Li keyval_base = '%s_%s' % (job_id, testname) if job_id else testname 301*9c5db199SXin Li return { 302*9c5db199SXin Li keyval_base + _BUG_ID_KEYVAL: bug_info[0], 303*9c5db199SXin Li keyval_base + _BUG_COUNT_KEYVAL: bug_info[1] 304*9c5db199SXin Li } 305*9c5db199SXin Li 306*9c5db199SXin Li 307*9c5db199SXin Lidef get_test_failure_bug_info(keyvals, job_id, testname): 308*9c5db199SXin Li """Extract information about a bug filed against a test failure. 309*9c5db199SXin Li 310*9c5db199SXin Li This method tries to extract bug_id and bug_count from the keyvals 311*9c5db199SXin Li of a suite. If for some reason it cannot retrieve the bug_id it will 312*9c5db199SXin Li return (None, None) and there will be no link to the bug filed. We will 313*9c5db199SXin Li instead link directly to the logs of the failed test. 314*9c5db199SXin Li 315*9c5db199SXin Li If it cannot retrieve the bug_count, it will return (int(bug_id), None) 316*9c5db199SXin Li and this will result in a link to the bug filed, with an inline message 317*9c5db199SXin Li saying we weren't able to determine how many times the bug occured. 318*9c5db199SXin Li 319*9c5db199SXin Li If it retrieved both the bug_id and bug_count, we return a tuple of 2 320*9c5db199SXin Li integers and link to the bug filed, as well as mention how many times 321*9c5db199SXin Li the bug has occured in the buildbot stages. 322*9c5db199SXin Li 323*9c5db199SXin Li @param keyvals Keyvals associated with a suite job. 324*9c5db199SXin Li @param job_id The afe job id of the job that runs the test. 325*9c5db199SXin Li @param testname Name of a test from the suite. 326*9c5db199SXin Li @return None if there is no bug info, or a pair with the 327*9c5db199SXin Li id of the bug, and the count of the number of 328*9c5db199SXin Li times the bug has been seen. 329*9c5db199SXin Li """ 330*9c5db199SXin Li testname = _testname_to_keyval_key(testname) 331*9c5db199SXin Li keyval_base = '%s_%s' % (job_id, testname) if job_id else testname 332*9c5db199SXin Li bug_id = keyvals.get(keyval_base + _BUG_ID_KEYVAL) 333*9c5db199SXin Li if not bug_id: 334*9c5db199SXin Li return None, None 335*9c5db199SXin Li bug_id = int(bug_id) 336*9c5db199SXin Li bug_count = keyvals.get(keyval_base + _BUG_COUNT_KEYVAL) 337*9c5db199SXin Li bug_count = int(bug_count) if bug_count else None 338*9c5db199SXin Li return bug_id, bug_count 339*9c5db199SXin Li 340*9c5db199SXin Li 341*9c5db199SXin Lidef create_job_name(build, suite, test_name): 342*9c5db199SXin Li """Create the name of a test job based on given build, suite, and test_name. 343*9c5db199SXin Li 344*9c5db199SXin Li @param build: name of the build, e.g., lumpy-release/R31-1234.0.0. 345*9c5db199SXin Li @param suite: name of the suite, e.g., bvt. 346*9c5db199SXin Li @param test_name: name of the test, e.g., stub_ServerToClientPass. 347*9c5db199SXin Li @return: the test job's name, e.g., 348*9c5db199SXin Li lumpy-release/R31-1234.0.0/bvt/stub_ServerToClientPass. 349*9c5db199SXin Li """ 350*9c5db199SXin Li return '/'.join([build, suite, test_name]) 351*9c5db199SXin Li 352*9c5db199SXin Li 353*9c5db199SXin Lidef get_test_name(build, suite, test_job_name): 354*9c5db199SXin Li """Get the test name from test job name. 355*9c5db199SXin Li 356*9c5db199SXin Li Name of test job may contain information like build and suite. This method 357*9c5db199SXin Li strips these information and return only the test name. 358*9c5db199SXin Li 359*9c5db199SXin Li @param build: name of the build, e.g., lumpy-release/R31-1234.0.0. 360*9c5db199SXin Li @param suite: name of the suite, e.g., bvt. 361*9c5db199SXin Li @param test_job_name: name of the test job, e.g., 362*9c5db199SXin Li lumpy-release/R31-1234.0.0/bvt/stub_ServerToClientPass. 363*9c5db199SXin Li @return: the test name, e.g., stub_ServerToClientPass. 364*9c5db199SXin Li """ 365*9c5db199SXin Li # Do not change this naming convention without updating 366*9c5db199SXin Li # site_utils.parse_job_name. 367*9c5db199SXin Li return test_job_name.replace('%s/%s/' % (build, suite), '') 368