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