site_utils.py revision 7836d25b375a791ebc7b3d0d255c8732c4f35be2
1# Copyright (c) 2012 The Chromium 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 5import inspect 6import logging 7import os 8import re 9import signal 10import socket 11import time 12import urllib2 13import uuid 14 15from autotest_lib.client.common_lib import base_utils 16from autotest_lib.client.common_lib import error 17from autotest_lib.client.common_lib import global_config 18from autotest_lib.client.common_lib import lsbrelease_utils 19from autotest_lib.client.cros import constants 20 21 22# Keep checking if the pid is alive every second until the timeout (in seconds) 23CHECK_PID_IS_ALIVE_TIMEOUT = 6 24 25_LOCAL_HOST_LIST = ('localhost', '127.0.0.1') 26 27# The default address of a vm gateway. 28DEFAULT_VM_GATEWAY = '10.0.2.2' 29 30# Google Storage bucket URI to store results in. 31DEFAULT_OFFLOAD_GSURI = global_config.global_config.get_config_value( 32 'CROS', 'results_storage_server', default=None) 33 34# Default Moblab Ethernet Interface. 35MOBLAB_ETH = 'eth0' 36 37def ping(host, deadline=None, tries=None, timeout=60): 38 """Attempt to ping |host|. 39 40 Shell out to 'ping' to try to reach |host| for |timeout| seconds. 41 Returns exit code of ping. 42 43 Per 'man ping', if you specify BOTH |deadline| and |tries|, ping only 44 returns 0 if we get responses to |tries| pings within |deadline| seconds. 45 46 Specifying |deadline| or |count| alone should return 0 as long as 47 some packets receive responses. 48 49 @param host: the host to ping. 50 @param deadline: seconds within which |tries| pings must succeed. 51 @param tries: number of pings to send. 52 @param timeout: number of seconds after which to kill 'ping' command. 53 @return exit code of ping command. 54 """ 55 args = [host] 56 if deadline: 57 args.append('-w%d' % deadline) 58 if tries: 59 args.append('-c%d' % tries) 60 return base_utils.run('ping', args=args, 61 ignore_status=True, timeout=timeout, 62 stdout_tee=base_utils.TEE_TO_LOGS, 63 stderr_tee=base_utils.TEE_TO_LOGS).exit_status 64 65 66def host_is_in_lab_zone(hostname): 67 """Check if the host is in the CLIENT.dns_zone. 68 69 @param hostname: The hostname to check. 70 @returns True if hostname.dns_zone resolves, otherwise False. 71 """ 72 host_parts = hostname.split('.') 73 dns_zone = global_config.global_config.get_config_value('CLIENT', 'dns_zone', 74 default=None) 75 fqdn = '%s.%s' % (host_parts[0], dns_zone) 76 try: 77 socket.gethostbyname(fqdn) 78 return True 79 except socket.gaierror: 80 return False 81 82 83def get_chrome_version(job_views): 84 """ 85 Retrieves the version of the chrome binary associated with a job. 86 87 When a test runs we query the chrome binary for it's version and drop 88 that value into a client keyval. To retrieve the chrome version we get all 89 the views associated with a test from the db, including those of the 90 server and client jobs, and parse the version out of the first test view 91 that has it. If we never ran a single test in the suite the job_views 92 dictionary will not contain a chrome version. 93 94 This method cannot retrieve the chrome version from a dictionary that 95 does not conform to the structure of an autotest tko view. 96 97 @param job_views: a list of a job's result views, as returned by 98 the get_detailed_test_views method in rpc_interface. 99 @return: The chrome version string, or None if one can't be found. 100 """ 101 102 # Aborted jobs have no views. 103 if not job_views: 104 return None 105 106 for view in job_views: 107 if (view.get('attributes') 108 and constants.CHROME_VERSION in view['attributes'].keys()): 109 110 return view['attributes'].get(constants.CHROME_VERSION) 111 112 logging.warning('Could not find chrome version for failure.') 113 return None 114 115 116def get_interface_mac_address(interface): 117 """Return the MAC address of a given interface. 118 119 @param interface: Interface to look up the MAC address of. 120 """ 121 interface_link = base_utils.run( 122 'ip addr show %s | grep link/ether' % interface).stdout 123 # The output will be in the format of: 124 # 'link/ether <mac> brd ff:ff:ff:ff:ff:ff' 125 return interface_link.split()[1] 126 127 128def get_offload_gsuri(): 129 """Return the GSURI to offload test results to. 130 131 For the normal use case this is the results_storage_server in the 132 global_config. 133 134 However partners using Moblab will be offloading their results to a 135 subdirectory of their image storage buckets. The subdirectory is 136 determined by the MAC Address of the Moblab device. 137 138 @returns gsuri to offload test results to. 139 """ 140 if not lsbrelease_utils.is_moblab(): 141 return DEFAULT_OFFLOAD_GSURI 142 moblab_id_filepath = '/home/moblab/.moblab_id' 143 if os.path.exists(moblab_id_filepath): 144 with open(moblab_id_filepath, 'r') as moblab_id_file: 145 random_id = moblab_id_file.read() 146 else: 147 random_id = uuid.uuid1() 148 with open(moblab_id_filepath, 'w') as moblab_id_file: 149 moblab_id_file.write('%s' % random_id) 150 return '%sresults/%s/%s/' % ( 151 global_config.global_config.get_config_value( 152 'CROS', 'image_storage_server'), 153 get_interface_mac_address(MOBLAB_ETH), random_id) 154 155 156# TODO(petermayo): crosbug.com/31826 Share this with _GsUpload in 157# //chromite.git/buildbot/prebuilt.py somewhere/somehow 158def gs_upload(local_file, remote_file, acl, result_dir=None, 159 transfer_timeout=300, acl_timeout=300): 160 """Upload to GS bucket. 161 162 @param local_file: Local file to upload 163 @param remote_file: Remote location to upload the local_file to. 164 @param acl: name or file used for controlling access to the uploaded 165 file. 166 @param result_dir: Result directory if you want to add tracing to the 167 upload. 168 @param transfer_timeout: Timeout for this upload call. 169 @param acl_timeout: Timeout for the acl call needed to confirm that 170 the uploader has permissions to execute the upload. 171 172 @raise CmdError: the exit code of the gsutil call was not 0. 173 174 @returns True/False - depending on if the upload succeeded or failed. 175 """ 176 # https://developers.google.com/storage/docs/accesscontrol#extension 177 CANNED_ACLS = ['project-private', 'private', 'public-read', 178 'public-read-write', 'authenticated-read', 179 'bucket-owner-read', 'bucket-owner-full-control'] 180 _GSUTIL_BIN = 'gsutil' 181 acl_cmd = None 182 if acl in CANNED_ACLS: 183 cmd = '%s cp -a %s %s %s' % (_GSUTIL_BIN, acl, local_file, remote_file) 184 else: 185 # For private uploads we assume that the overlay board is set up 186 # properly and a googlestore_acl.xml is present, if not this script 187 # errors 188 cmd = '%s cp -a private %s %s' % (_GSUTIL_BIN, local_file, remote_file) 189 if not os.path.exists(acl): 190 logging.error('Unable to find ACL File %s.', acl) 191 return False 192 acl_cmd = '%s setacl %s %s' % (_GSUTIL_BIN, acl, remote_file) 193 if not result_dir: 194 base_utils.run(cmd, timeout=transfer_timeout, verbose=True) 195 if acl_cmd: 196 base_utils.run(acl_cmd, timeout=acl_timeout, verbose=True) 197 return True 198 with open(os.path.join(result_dir, 'tracing'), 'w') as ftrace: 199 ftrace.write('Preamble\n') 200 base_utils.run(cmd, timeout=transfer_timeout, verbose=True, 201 stdout_tee=ftrace, stderr_tee=ftrace) 202 if acl_cmd: 203 ftrace.write('\nACL setting\n') 204 # Apply the passed in ACL xml file to the uploaded object. 205 base_utils.run(acl_cmd, timeout=acl_timeout, verbose=True, 206 stdout_tee=ftrace, stderr_tee=ftrace) 207 ftrace.write('Postamble\n') 208 return True 209 210 211def gs_ls(uri_pattern): 212 """Returns a list of URIs that match a given pattern. 213 214 @param uri_pattern: a GS URI pattern, may contain wildcards 215 216 @return A list of URIs matching the given pattern. 217 218 @raise CmdError: the gsutil command failed. 219 220 """ 221 gs_cmd = ' '.join(['gsutil', 'ls', uri_pattern]) 222 result = base_utils.system_output(gs_cmd).splitlines() 223 return [path.rstrip() for path in result if path] 224 225 226def nuke_pids(pid_list, signal_queue=[signal.SIGTERM, signal.SIGKILL]): 227 """ 228 Given a list of pid's, kill them via an esclating series of signals. 229 230 @param pid_list: List of PID's to kill. 231 @param signal_queue: Queue of signals to send the PID's to terminate them. 232 233 @return: A mapping of the signal name to the number of processes it 234 was sent to. 235 """ 236 sig_count = {} 237 # Though this is slightly hacky it beats hardcoding names anyday. 238 sig_names = dict((k, v) for v, k in signal.__dict__.iteritems() 239 if v.startswith('SIG')) 240 for sig in signal_queue: 241 logging.debug('Sending signal %s to the following pids:', sig) 242 sig_count[sig_names.get(sig, 'unknown_signal')] = len(pid_list) 243 for pid in pid_list: 244 logging.debug('Pid %d', pid) 245 try: 246 os.kill(pid, sig) 247 except OSError: 248 # The process may have died from a previous signal before we 249 # could kill it. 250 pass 251 if sig == signal.SIGKILL: 252 return sig_count 253 pid_list = [pid for pid in pid_list if base_utils.pid_is_alive(pid)] 254 if not pid_list: 255 break 256 time.sleep(CHECK_PID_IS_ALIVE_TIMEOUT) 257 failed_list = [] 258 for pid in pid_list: 259 if base_utils.pid_is_alive(pid): 260 failed_list.append('Could not kill %d for process name: %s.' % pid, 261 base_utils.get_process_name(pid)) 262 if failed_list: 263 raise error.AutoservRunError('Following errors occured: %s' % 264 failed_list, None) 265 return sig_count 266 267 268def externalize_host(host): 269 """Returns an externally accessible host name. 270 271 @param host: a host name or address (string) 272 273 @return An externally visible host name or address 274 275 """ 276 return socket.gethostname() if host in _LOCAL_HOST_LIST else host 277 278 279def urlopen_socket_timeout(url, data=None, timeout=5): 280 """ 281 Wrapper to urllib2.urlopen with a socket timeout. 282 283 This method will convert all socket timeouts to 284 TimeoutExceptions, so we can use it in conjunction 285 with the rpc retry decorator and continue to handle 286 other URLErrors as we see fit. 287 288 @param url: The url to open. 289 @param data: The data to send to the url (eg: the urlencoded dictionary 290 used with a POST call). 291 @param timeout: The timeout for this urlopen call. 292 293 @return: The response of the urlopen call. 294 295 @raises: error.TimeoutException when a socket timeout occurs. 296 urllib2.URLError for errors that not caused by timeout. 297 urllib2.HTTPError for errors like 404 url not found. 298 """ 299 old_timeout = socket.getdefaulttimeout() 300 socket.setdefaulttimeout(timeout) 301 try: 302 return urllib2.urlopen(url, data=data) 303 except urllib2.URLError as e: 304 if type(e.reason) is socket.timeout: 305 raise error.TimeoutException(str(e)) 306 raise 307 finally: 308 socket.setdefaulttimeout(old_timeout) 309 310 311def parse_chrome_version(version_string): 312 """ 313 Parse a chrome version string and return version and milestone. 314 315 Given a chrome version of the form "W.X.Y.Z", return "W.X.Y.Z" as 316 the version and "W" as the milestone. 317 318 @param version_string: Chrome version string. 319 @return: a tuple (chrome_version, milestone). If the incoming version 320 string is not of the form "W.X.Y.Z", chrome_version will 321 be set to the incoming "version_string" argument and the 322 milestone will be set to the empty string. 323 """ 324 match = re.search('(\d+)\.\d+\.\d+\.\d+', version_string) 325 ver = match.group(0) if match else version_string 326 milestone = match.group(1) if match else '' 327 return ver, milestone 328 329 330def is_localhost(server): 331 """Check if server is equivalent to localhost. 332 333 @param server: Name of the server to check. 334 335 @return: True if given server is equivalent to localhost. 336 337 @raise socket.gaierror: If server name failed to be resolved. 338 """ 339 if server in _LOCAL_HOST_LIST: 340 return True 341 try: 342 return (socket.gethostbyname(socket.gethostname()) == 343 socket.gethostbyname(server)) 344 except socket.gaierror: 345 logging.error('Failed to resolve server name %s.', server) 346 return False 347 348 349def get_function_arg_value(func, arg_name, args, kwargs): 350 """Get the value of the given argument for the function. 351 352 @param func: Function being called with given arguments. 353 @param arg_name: Name of the argument to look for value. 354 @param args: arguments for function to be called. 355 @param kwargs: keyword arguments for function to be called. 356 357 @return: The value of the given argument for the function. 358 359 @raise ValueError: If the argument is not listed function arguemnts. 360 @raise KeyError: If no value is found for the given argument. 361 """ 362 if arg_name in kwargs: 363 return kwargs[arg_name] 364 365 argspec = inspect.getargspec(func) 366 index = argspec.args.index(arg_name) 367 try: 368 return args[index] 369 except IndexError: 370 try: 371 # The argument can use a default value. Reverse the default value 372 # so argument with default value can be counted from the last to 373 # the first. 374 return argspec.defaults[::-1][len(argspec.args) - index - 1] 375 except IndexError: 376 raise KeyError('Argument %s is not given a value. argspec: %s, ' 377 'args:%s, kwargs:%s' % 378 (arg_name, argspec, args, kwargs)) 379 380 381def version_match(build_version, release_version, update_url=''): 382 """Compare release versino from lsb-release with cros-version label. 383 384 build_version is a string based on build name. It is prefixed with builder 385 info and branch ID, e.g., lumpy-release/R43-6809.0.0. It may not include 386 builder info, e.g., lumpy-release, in which case, update_url shall be passed 387 in to determine if the build is a trybot or pgo-generate build. 388 release_version is retrieved from lsb-release. 389 These two values might not match exactly. 390 391 The method is designed to compare version for following 6 scenarios with 392 samples of build version and expected release version: 393 1. trybot paladin build. 394 build version: trybot-lumpy-paladin/R27-3837.0.0-b123 395 release version: 3837.0.2013_03_21_1340 396 397 2. trybot release build. 398 build version: trybot-lumpy-release/R27-3837.0.0-b456 399 release version: 3837.0.0 400 401 3. buildbot official release build. 402 build version: lumpy-release/R27-3837.0.0 403 release version: 3837.0.0 404 405 4. non-official paladin rc build. 406 build version: lumpy-paladin/R27-3878.0.0-rc7 407 release version: 3837.0.0-rc7 408 409 5. chrome-perf build. 410 build version: lumpy-chrome-perf/R28-3837.0.0-b2996 411 release version: 3837.0.0 412 413 6. pgo-generate build. 414 build version: lumpy-release-pgo-generate/R28-3837.0.0-b2996 415 release version: 3837.0.0-pgo-generate 416 417 TODO: This logic has a bug if a trybot paladin build failed to be 418 installed in a DUT running an older trybot paladin build with same 419 platform number, but different build number (-b###). So to conclusively 420 determine if a tryjob paladin build is imaged successfully, we may need 421 to find out the date string from update url. 422 423 @param build_version: Build name for cros version, e.g. 424 peppy-release/R43-6809.0.0 or R43-6809.0.0 425 @param release_version: Release version retrieved from lsb-release, 426 e.g., 6809.0.0 427 @param update_url: Update url which include the full builder information. 428 Default is set to empty string. 429 430 @return: True if the values match, otherwise returns False. 431 """ 432 # If the build is from release, CQ or PFQ builder, cros-version label must 433 # be ended with release version in lsb-release. 434 if build_version.endswith(release_version): 435 return True 436 437 # Remove R#- and -b# at the end of build version 438 stripped_version = re.sub(r'(R\d+-|-b\d+)', '', build_version) 439 # Trim the builder info, e.g., trybot-lumpy-paladin/ 440 stripped_version = stripped_version.split('/')[-1] 441 442 is_trybot_paladin_build = ( 443 re.match(r'.*trybot-.+-paladin', build_version) or 444 re.match(r'.*trybot-.+-paladin', update_url)) 445 446 # Replace date string with 0 in release_version 447 release_version_no_date = re.sub(r'\d{4}_\d{2}_\d{2}_\d+', '0', 448 release_version) 449 has_date_string = release_version != release_version_no_date 450 451 is_pgo_generate_build = ( 452 re.match(r'.+-pgo-generate', build_version) or 453 re.match(r'.+-pgo-generate', update_url)) 454 455 # Remove |-pgo-generate| in release_version 456 release_version_no_pgo = release_version.replace('-pgo-generate', '') 457 has_pgo_generate = release_version != release_version_no_pgo 458 459 if is_trybot_paladin_build: 460 if not has_date_string: 461 logging.error('A trybot paladin build is expected. Version ' 462 '"%s" is not a paladin build.', release_version) 463 return False 464 return stripped_version == release_version_no_date 465 elif is_pgo_generate_build: 466 if not has_pgo_generate: 467 logging.error('A pgo-generate build is expected. Version ' 468 '"%s" is not a pgo-generate build.', 469 release_version) 470 return False 471 return stripped_version == release_version_no_pgo 472 else: 473 if has_date_string: 474 logging.error('Unexpected date found in a non trybot paladin ' 475 'build.') 476 return False 477 # Versioned build, i.e., rc or release build. 478 return stripped_version == release_version 479 480 481def get_real_user(): 482 """Get the real user that runs the script. 483 484 The function check environment variable SUDO_USER for the user if the 485 script is run with sudo. Otherwise, it returns the value of environment 486 variable USER. 487 488 @return: The user name that runs the script. 489 490 """ 491 user = os.environ.get('SUDO_USER') 492 if not user: 493 user = os.environ.get('USER') 494 return user 495