site_utils.py revision aa401d354435497a4195efad12c89912d5028187
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 struct 12import time 13import urllib2 14import uuid 15 16from autotest_lib.client.common_lib import base_utils 17from autotest_lib.client.common_lib import error 18from autotest_lib.client.common_lib import global_config 19from autotest_lib.client.common_lib import lsbrelease_utils 20from autotest_lib.client.cros import constants 21 22 23CONFIG = global_config.global_config 24 25# Keep checking if the pid is alive every second until the timeout (in seconds) 26CHECK_PID_IS_ALIVE_TIMEOUT = 6 27 28_LOCAL_HOST_LIST = ('localhost', '127.0.0.1') 29 30# The default address of a vm gateway. 31DEFAULT_VM_GATEWAY = '10.0.2.2' 32 33# Google Storage bucket URI to store results in. 34DEFAULT_OFFLOAD_GSURI = CONFIG.get_config_value( 35 'CROS', 'results_storage_server', default=None) 36 37# Default Moblab Ethernet Interface. 38MOBLAB_ETH = 'eth0' 39 40# A list of subnets that requires dedicated devserver and drone in the same 41# subnet. Each item is a tuple of (subnet_ip, mask_bits), e.g., 42# ('192.168.0.0', 24)) 43RESTRICTED_SUBNETS = [] 44restricted_subnets_list = CONFIG.get_config_value( 45 'CROS', 'restricted_subnets', type=list, default=[]) 46# TODO(dshi): Remove the code to split subnet with `:` after R51 is off stable 47# channel, and update shadow config to use `/` as delimiter for consistency. 48for subnet in restricted_subnets_list: 49 ip, mask_bits = subnet.split('/') if '/' in subnet else subnet.split(':') 50 RESTRICTED_SUBNETS.append((ip, int(mask_bits))) 51 52# regex pattern for CLIENT/wireless_ssid_ config. For example, global config 53# can have following config in CLIENT section to indicate that hosts in subnet 54# 192.168.0.1/24 should use wireless ssid of `ssid_1` 55# wireless_ssid_192.168.0.1/24: ssid_1 56WIRELESS_SSID_PATTERN = 'wireless_ssid_(.*)/(\d+)' 57 58def ping(host, deadline=None, tries=None, timeout=60): 59 """Attempt to ping |host|. 60 61 Shell out to 'ping' to try to reach |host| for |timeout| seconds. 62 Returns exit code of ping. 63 64 Per 'man ping', if you specify BOTH |deadline| and |tries|, ping only 65 returns 0 if we get responses to |tries| pings within |deadline| seconds. 66 67 Specifying |deadline| or |count| alone should return 0 as long as 68 some packets receive responses. 69 70 @param host: the host to ping. 71 @param deadline: seconds within which |tries| pings must succeed. 72 @param tries: number of pings to send. 73 @param timeout: number of seconds after which to kill 'ping' command. 74 @return exit code of ping command. 75 """ 76 args = [host] 77 if deadline: 78 args.append('-w%d' % deadline) 79 if tries: 80 args.append('-c%d' % tries) 81 return base_utils.run('ping', args=args, 82 ignore_status=True, timeout=timeout, 83 stdout_tee=base_utils.TEE_TO_LOGS, 84 stderr_tee=base_utils.TEE_TO_LOGS).exit_status 85 86 87def host_is_in_lab_zone(hostname): 88 """Check if the host is in the CLIENT.dns_zone. 89 90 @param hostname: The hostname to check. 91 @returns True if hostname.dns_zone resolves, otherwise False. 92 """ 93 host_parts = hostname.split('.') 94 dns_zone = CONFIG.get_config_value('CLIENT', 'dns_zone', default=None) 95 fqdn = '%s.%s' % (host_parts[0], dns_zone) 96 try: 97 socket.gethostbyname(fqdn) 98 return True 99 except socket.gaierror: 100 return False 101 102 103def host_could_be_in_afe(hostname): 104 """Check if the host could be in Autotest Front End. 105 106 Report whether or not a host could be in AFE, without actually 107 consulting AFE. This method exists because some systems are in the 108 lab zone, but not actually managed by AFE. 109 110 @param hostname: The hostname to check. 111 @returns True if hostname is in lab zone, and does not match *-dev-* 112 """ 113 # Do the 'dev' check first, so that we skip DNS lookup if the 114 # hostname matches. This should give us greater resilience to lab 115 # failures. 116 return (hostname.find('-dev-') == -1) and host_is_in_lab_zone(hostname) 117 118 119def get_chrome_version(job_views): 120 """ 121 Retrieves the version of the chrome binary associated with a job. 122 123 When a test runs we query the chrome binary for it's version and drop 124 that value into a client keyval. To retrieve the chrome version we get all 125 the views associated with a test from the db, including those of the 126 server and client jobs, and parse the version out of the first test view 127 that has it. If we never ran a single test in the suite the job_views 128 dictionary will not contain a chrome version. 129 130 This method cannot retrieve the chrome version from a dictionary that 131 does not conform to the structure of an autotest tko view. 132 133 @param job_views: a list of a job's result views, as returned by 134 the get_detailed_test_views method in rpc_interface. 135 @return: The chrome version string, or None if one can't be found. 136 """ 137 138 # Aborted jobs have no views. 139 if not job_views: 140 return None 141 142 for view in job_views: 143 if (view.get('attributes') 144 and constants.CHROME_VERSION in view['attributes'].keys()): 145 146 return view['attributes'].get(constants.CHROME_VERSION) 147 148 logging.warning('Could not find chrome version for failure.') 149 return None 150 151 152def get_interface_mac_address(interface): 153 """Return the MAC address of a given interface. 154 155 @param interface: Interface to look up the MAC address of. 156 """ 157 interface_link = base_utils.run( 158 'ip addr show %s | grep link/ether' % interface).stdout 159 # The output will be in the format of: 160 # 'link/ether <mac> brd ff:ff:ff:ff:ff:ff' 161 return interface_link.split()[1] 162 163 164def get_moblab_id(): 165 """Gets the moblab random id. 166 167 The random id file is cached on disk. If it does not exist, a new file is 168 created the first time. 169 170 @returns the moblab random id. 171 """ 172 moblab_id_filepath = '/home/moblab/.moblab_id' 173 if os.path.exists(moblab_id_filepath): 174 with open(moblab_id_filepath, 'r') as moblab_id_file: 175 random_id = moblab_id_file.read() 176 else: 177 random_id = uuid.uuid1() 178 with open(moblab_id_filepath, 'w') as moblab_id_file: 179 moblab_id_file.write('%s' % random_id) 180 return random_id 181 182 183def get_offload_gsuri(): 184 """Return the GSURI to offload test results to. 185 186 For the normal use case this is the results_storage_server in the 187 global_config. 188 189 However partners using Moblab will be offloading their results to a 190 subdirectory of their image storage buckets. The subdirectory is 191 determined by the MAC Address of the Moblab device. 192 193 @returns gsuri to offload test results to. 194 """ 195 # For non-moblab, use results_storage_server or default. 196 if not lsbrelease_utils.is_moblab(): 197 return DEFAULT_OFFLOAD_GSURI 198 199 # For moblab, use results_storage_server or image_storage_server as bucket 200 # name and mac-address/moblab_id as path. 201 gsuri = DEFAULT_OFFLOAD_GSURI 202 if not gsuri: 203 gsuri = CONFIG.get_config_value('CROS', 'image_storage_server') 204 205 return '%sresults/%s/%s/' % ( 206 gsuri, get_interface_mac_address(MOBLAB_ETH), get_moblab_id()) 207 208 209# TODO(petermayo): crosbug.com/31826 Share this with _GsUpload in 210# //chromite.git/buildbot/prebuilt.py somewhere/somehow 211def gs_upload(local_file, remote_file, acl, result_dir=None, 212 transfer_timeout=300, acl_timeout=300): 213 """Upload to GS bucket. 214 215 @param local_file: Local file to upload 216 @param remote_file: Remote location to upload the local_file to. 217 @param acl: name or file used for controlling access to the uploaded 218 file. 219 @param result_dir: Result directory if you want to add tracing to the 220 upload. 221 @param transfer_timeout: Timeout for this upload call. 222 @param acl_timeout: Timeout for the acl call needed to confirm that 223 the uploader has permissions to execute the upload. 224 225 @raise CmdError: the exit code of the gsutil call was not 0. 226 227 @returns True/False - depending on if the upload succeeded or failed. 228 """ 229 # https://developers.google.com/storage/docs/accesscontrol#extension 230 CANNED_ACLS = ['project-private', 'private', 'public-read', 231 'public-read-write', 'authenticated-read', 232 'bucket-owner-read', 'bucket-owner-full-control'] 233 _GSUTIL_BIN = 'gsutil' 234 acl_cmd = None 235 if acl in CANNED_ACLS: 236 cmd = '%s cp -a %s %s %s' % (_GSUTIL_BIN, acl, local_file, remote_file) 237 else: 238 # For private uploads we assume that the overlay board is set up 239 # properly and a googlestore_acl.xml is present, if not this script 240 # errors 241 cmd = '%s cp -a private %s %s' % (_GSUTIL_BIN, local_file, remote_file) 242 if not os.path.exists(acl): 243 logging.error('Unable to find ACL File %s.', acl) 244 return False 245 acl_cmd = '%s setacl %s %s' % (_GSUTIL_BIN, acl, remote_file) 246 if not result_dir: 247 base_utils.run(cmd, timeout=transfer_timeout, verbose=True) 248 if acl_cmd: 249 base_utils.run(acl_cmd, timeout=acl_timeout, verbose=True) 250 return True 251 with open(os.path.join(result_dir, 'tracing'), 'w') as ftrace: 252 ftrace.write('Preamble\n') 253 base_utils.run(cmd, timeout=transfer_timeout, verbose=True, 254 stdout_tee=ftrace, stderr_tee=ftrace) 255 if acl_cmd: 256 ftrace.write('\nACL setting\n') 257 # Apply the passed in ACL xml file to the uploaded object. 258 base_utils.run(acl_cmd, timeout=acl_timeout, verbose=True, 259 stdout_tee=ftrace, stderr_tee=ftrace) 260 ftrace.write('Postamble\n') 261 return True 262 263 264def gs_ls(uri_pattern): 265 """Returns a list of URIs that match a given pattern. 266 267 @param uri_pattern: a GS URI pattern, may contain wildcards 268 269 @return A list of URIs matching the given pattern. 270 271 @raise CmdError: the gsutil command failed. 272 273 """ 274 gs_cmd = ' '.join(['gsutil', 'ls', uri_pattern]) 275 result = base_utils.system_output(gs_cmd).splitlines() 276 return [path.rstrip() for path in result if path] 277 278 279def nuke_pids(pid_list, signal_queue=[signal.SIGTERM, signal.SIGKILL]): 280 """ 281 Given a list of pid's, kill them via an esclating series of signals. 282 283 @param pid_list: List of PID's to kill. 284 @param signal_queue: Queue of signals to send the PID's to terminate them. 285 286 @return: A mapping of the signal name to the number of processes it 287 was sent to. 288 """ 289 sig_count = {} 290 # Though this is slightly hacky it beats hardcoding names anyday. 291 sig_names = dict((k, v) for v, k in signal.__dict__.iteritems() 292 if v.startswith('SIG')) 293 for sig in signal_queue: 294 logging.debug('Sending signal %s to the following pids:', sig) 295 sig_count[sig_names.get(sig, 'unknown_signal')] = len(pid_list) 296 for pid in pid_list: 297 logging.debug('Pid %d', pid) 298 try: 299 os.kill(pid, sig) 300 except OSError: 301 # The process may have died from a previous signal before we 302 # could kill it. 303 pass 304 if sig == signal.SIGKILL: 305 return sig_count 306 pid_list = [pid for pid in pid_list if base_utils.pid_is_alive(pid)] 307 if not pid_list: 308 break 309 time.sleep(CHECK_PID_IS_ALIVE_TIMEOUT) 310 failed_list = [] 311 for pid in pid_list: 312 if base_utils.pid_is_alive(pid): 313 failed_list.append('Could not kill %d for process name: %s.' % pid, 314 base_utils.get_process_name(pid)) 315 if failed_list: 316 raise error.AutoservRunError('Following errors occured: %s' % 317 failed_list, None) 318 return sig_count 319 320 321def externalize_host(host): 322 """Returns an externally accessible host name. 323 324 @param host: a host name or address (string) 325 326 @return An externally visible host name or address 327 328 """ 329 return socket.gethostname() if host in _LOCAL_HOST_LIST else host 330 331 332def urlopen_socket_timeout(url, data=None, timeout=5): 333 """ 334 Wrapper to urllib2.urlopen with a socket timeout. 335 336 This method will convert all socket timeouts to 337 TimeoutExceptions, so we can use it in conjunction 338 with the rpc retry decorator and continue to handle 339 other URLErrors as we see fit. 340 341 @param url: The url to open. 342 @param data: The data to send to the url (eg: the urlencoded dictionary 343 used with a POST call). 344 @param timeout: The timeout for this urlopen call. 345 346 @return: The response of the urlopen call. 347 348 @raises: error.TimeoutException when a socket timeout occurs. 349 urllib2.URLError for errors that not caused by timeout. 350 urllib2.HTTPError for errors like 404 url not found. 351 """ 352 old_timeout = socket.getdefaulttimeout() 353 socket.setdefaulttimeout(timeout) 354 try: 355 return urllib2.urlopen(url, data=data) 356 except urllib2.URLError as e: 357 if type(e.reason) is socket.timeout: 358 raise error.TimeoutException(str(e)) 359 raise 360 finally: 361 socket.setdefaulttimeout(old_timeout) 362 363 364def parse_chrome_version(version_string): 365 """ 366 Parse a chrome version string and return version and milestone. 367 368 Given a chrome version of the form "W.X.Y.Z", return "W.X.Y.Z" as 369 the version and "W" as the milestone. 370 371 @param version_string: Chrome version string. 372 @return: a tuple (chrome_version, milestone). If the incoming version 373 string is not of the form "W.X.Y.Z", chrome_version will 374 be set to the incoming "version_string" argument and the 375 milestone will be set to the empty string. 376 """ 377 match = re.search('(\d+)\.\d+\.\d+\.\d+', version_string) 378 ver = match.group(0) if match else version_string 379 milestone = match.group(1) if match else '' 380 return ver, milestone 381 382 383def is_localhost(server): 384 """Check if server is equivalent to localhost. 385 386 @param server: Name of the server to check. 387 388 @return: True if given server is equivalent to localhost. 389 390 @raise socket.gaierror: If server name failed to be resolved. 391 """ 392 if server in _LOCAL_HOST_LIST: 393 return True 394 try: 395 return (socket.gethostbyname(socket.gethostname()) == 396 socket.gethostbyname(server)) 397 except socket.gaierror: 398 logging.error('Failed to resolve server name %s.', server) 399 return False 400 401 402def is_puppylab_vm(server): 403 """Check if server is a virtual machine in puppylab. 404 405 In the virtual machine testing environment (i.e., puppylab), each 406 shard VM has a hostname like localhost:<port>. 407 408 @param server: Server name to check. 409 410 @return True if given server is a virtual machine in puppylab. 411 412 """ 413 # TODO(mkryu): This is a puppylab specific hack. Please update 414 # this method if you have a better solution. 415 regex = re.compile(r'(.+):\d+') 416 m = regex.match(server) 417 if m: 418 return m.group(1) in _LOCAL_HOST_LIST 419 return False 420 421 422def get_function_arg_value(func, arg_name, args, kwargs): 423 """Get the value of the given argument for the function. 424 425 @param func: Function being called with given arguments. 426 @param arg_name: Name of the argument to look for value. 427 @param args: arguments for function to be called. 428 @param kwargs: keyword arguments for function to be called. 429 430 @return: The value of the given argument for the function. 431 432 @raise ValueError: If the argument is not listed function arguemnts. 433 @raise KeyError: If no value is found for the given argument. 434 """ 435 if arg_name in kwargs: 436 return kwargs[arg_name] 437 438 argspec = inspect.getargspec(func) 439 index = argspec.args.index(arg_name) 440 try: 441 return args[index] 442 except IndexError: 443 try: 444 # The argument can use a default value. Reverse the default value 445 # so argument with default value can be counted from the last to 446 # the first. 447 return argspec.defaults[::-1][len(argspec.args) - index - 1] 448 except IndexError: 449 raise KeyError('Argument %s is not given a value. argspec: %s, ' 450 'args:%s, kwargs:%s' % 451 (arg_name, argspec, args, kwargs)) 452 453 454def has_systemd(): 455 """Check if the host is running systemd. 456 457 @return: True if the host uses systemd, otherwise returns False. 458 """ 459 return os.path.basename(os.readlink('/proc/1/exe')) == 'systemd' 460 461 462def version_match(build_version, release_version, update_url=''): 463 """Compare release versino from lsb-release with cros-version label. 464 465 build_version is a string based on build name. It is prefixed with builder 466 info and branch ID, e.g., lumpy-release/R43-6809.0.0. It may not include 467 builder info, e.g., lumpy-release, in which case, update_url shall be passed 468 in to determine if the build is a trybot or pgo-generate build. 469 release_version is retrieved from lsb-release. 470 These two values might not match exactly. 471 472 The method is designed to compare version for following 6 scenarios with 473 samples of build version and expected release version: 474 1. trybot non-release build (paladin, pre-cq or test-ap build). 475 build version: trybot-lumpy-paladin/R27-3837.0.0-b123 476 release version: 3837.0.2013_03_21_1340 477 478 2. trybot release build. 479 build version: trybot-lumpy-release/R27-3837.0.0-b456 480 release version: 3837.0.0 481 482 3. buildbot official release build. 483 build version: lumpy-release/R27-3837.0.0 484 release version: 3837.0.0 485 486 4. non-official paladin rc build. 487 build version: lumpy-paladin/R27-3878.0.0-rc7 488 release version: 3837.0.0-rc7 489 490 5. chrome-perf build. 491 build version: lumpy-chrome-perf/R28-3837.0.0-b2996 492 release version: 3837.0.0 493 494 6. pgo-generate build. 495 build version: lumpy-release-pgo-generate/R28-3837.0.0-b2996 496 release version: 3837.0.0-pgo-generate 497 498 TODO: This logic has a bug if a trybot paladin build failed to be 499 installed in a DUT running an older trybot paladin build with same 500 platform number, but different build number (-b###). So to conclusively 501 determine if a tryjob paladin build is imaged successfully, we may need 502 to find out the date string from update url. 503 504 @param build_version: Build name for cros version, e.g. 505 peppy-release/R43-6809.0.0 or R43-6809.0.0 506 @param release_version: Release version retrieved from lsb-release, 507 e.g., 6809.0.0 508 @param update_url: Update url which include the full builder information. 509 Default is set to empty string. 510 511 @return: True if the values match, otherwise returns False. 512 """ 513 # If the build is from release, CQ or PFQ builder, cros-version label must 514 # be ended with release version in lsb-release. 515 if build_version.endswith(release_version): 516 return True 517 518 # Remove R#- and -b# at the end of build version 519 stripped_version = re.sub(r'(R\d+-|-b\d+)', '', build_version) 520 # Trim the builder info, e.g., trybot-lumpy-paladin/ 521 stripped_version = stripped_version.split('/')[-1] 522 523 is_trybot_non_release_build = ( 524 re.match(r'.*trybot-.+-(paladin|pre-cq|test-ap)', build_version) or 525 re.match(r'.*trybot-.+-(paladin|pre-cq|test-ap)', update_url)) 526 527 # Replace date string with 0 in release_version 528 release_version_no_date = re.sub(r'\d{4}_\d{2}_\d{2}_\d+', '0', 529 release_version) 530 has_date_string = release_version != release_version_no_date 531 532 is_pgo_generate_build = ( 533 re.match(r'.+-pgo-generate', build_version) or 534 re.match(r'.+-pgo-generate', update_url)) 535 536 # Remove |-pgo-generate| in release_version 537 release_version_no_pgo = release_version.replace('-pgo-generate', '') 538 has_pgo_generate = release_version != release_version_no_pgo 539 540 if is_trybot_non_release_build: 541 if not has_date_string: 542 logging.error('A trybot paladin or pre-cq build is expected. ' 543 'Version "%s" is not a paladin or pre-cq build.', 544 release_version) 545 return False 546 return stripped_version == release_version_no_date 547 elif is_pgo_generate_build: 548 if not has_pgo_generate: 549 logging.error('A pgo-generate build is expected. Version ' 550 '"%s" is not a pgo-generate build.', 551 release_version) 552 return False 553 return stripped_version == release_version_no_pgo 554 else: 555 if has_date_string: 556 logging.error('Unexpected date found in a non trybot paladin or ' 557 'pre-cq build.') 558 return False 559 # Versioned build, i.e., rc or release build. 560 return stripped_version == release_version 561 562 563def get_real_user(): 564 """Get the real user that runs the script. 565 566 The function check environment variable SUDO_USER for the user if the 567 script is run with sudo. Otherwise, it returns the value of environment 568 variable USER. 569 570 @return: The user name that runs the script. 571 572 """ 573 user = os.environ.get('SUDO_USER') 574 if not user: 575 user = os.environ.get('USER') 576 return user 577 578 579def get_service_pid(service_name): 580 """Return pid of service. 581 582 @param service_name: string name of service. 583 584 @return: pid or 0 if service is not running. 585 """ 586 if has_systemd(): 587 # systemctl show prints 'MainPID=0' if the service is not running. 588 cmd_result = base_utils.run('systemctl show -p MainPID %s' % 589 service_name, ignore_status=True) 590 return int(cmd_result.stdout.split('=')[1]) 591 else: 592 cmd_result = base_utils.run('status %s' % service_name, 593 ignore_status=True) 594 if 'start/running' in cmd_result.stdout: 595 return int(cmd_result.stdout.split()[3]) 596 return 0 597 598 599def control_service(service_name, action='start', ignore_status=True): 600 """Controls a service. It can be used to start, stop or restart 601 a service. 602 603 @param service_name: string service to be restarted. 604 605 @param action: string choice of action to control command. 606 607 @param ignore_status: boolean ignore if system command fails. 608 609 @return: status code of the executed command. 610 """ 611 if action not in ('start', 'stop', 'restart'): 612 raise ValueError('Unknown action supplied as parameter.') 613 614 control_cmd = action + ' ' + service_name 615 if has_systemd(): 616 control_cmd = 'systemctl ' + control_cmd 617 return base_utils.system(control_cmd, ignore_status=ignore_status) 618 619 620def restart_service(service_name, ignore_status=True): 621 """Restarts a service 622 623 @param service_name: string service to be restarted. 624 625 @param ignore_status: boolean ignore if system command fails. 626 627 @return: status code of the executed command. 628 """ 629 return control_service(service_name, action='restart', ignore_status=ignore_status) 630 631 632def start_service(service_name, ignore_status=True): 633 """Starts a service 634 635 @param service_name: string service to be started. 636 637 @param ignore_status: boolean ignore if system command fails. 638 639 @return: status code of the executed command. 640 """ 641 return control_service(service_name, action='start', ignore_status=ignore_status) 642 643 644def stop_service(service_name, ignore_status=True): 645 """Stops a service 646 647 @param service_name: string service to be stopped. 648 649 @param ignore_status: boolean ignore if system command fails. 650 651 @return: status code of the executed command. 652 """ 653 return control_service(service_name, action='stop', ignore_status=ignore_status) 654 655 656def sudo_require_password(): 657 """Test if the process can run sudo command without using password. 658 659 @return: True if the process needs password to run sudo command. 660 661 """ 662 try: 663 base_utils.run('sudo -n true') 664 return False 665 except error.CmdError: 666 logging.warn('sudo command requires password.') 667 return True 668 669 670def is_in_container(): 671 """Check if the process is running inside a container. 672 673 @return: True if the process is running inside a container, otherwise False. 674 """ 675 result = base_utils.run('grep -q "/lxc/" /proc/1/cgroup', 676 verbose=False, ignore_status=True) 677 return result.exit_status == 0 678 679 680def is_flash_installed(): 681 """ 682 The Adobe Flash binary is only distributed with internal builds. 683 """ 684 return (os.path.exists('/opt/google/chrome/pepper/libpepflashplayer.so') 685 and os.path.exists('/opt/google/chrome/pepper/pepper-flash.info')) 686 687 688def verify_flash_installed(): 689 """ 690 The Adobe Flash binary is only distributed with internal builds. 691 Warn users of public builds of the extra dependency. 692 """ 693 if not is_flash_installed(): 694 raise error.TestNAError('No Adobe Flash binary installed.') 695 696 697def is_in_same_subnet(ip_1, ip_2, mask_bits=24): 698 """Check if two IP addresses are in the same subnet with given mask bits. 699 700 The two IP addresses are string of IPv4, e.g., '192.168.0.3'. 701 702 @param ip_1: First IP address to compare. 703 @param ip_2: Second IP address to compare. 704 @param mask_bits: Number of mask bits for subnet comparison. Default to 24. 705 706 @return: True if the two IP addresses are in the same subnet. 707 708 """ 709 mask = ((2L<<mask_bits-1) -1)<<(32-mask_bits) 710 ip_1_num = struct.unpack('!I', socket.inet_aton(ip_1))[0] 711 ip_2_num = struct.unpack('!I', socket.inet_aton(ip_2))[0] 712 return ip_1_num & mask == ip_2_num & mask 713 714 715def get_ip_address(hostname): 716 """Get the IP address of given hostname. 717 718 @param hostname: Hostname of a DUT. 719 720 @return: The IP address of given hostname. None if failed to resolve 721 hostname. 722 """ 723 try: 724 if hostname: 725 return socket.gethostbyname(hostname) 726 except socket.gaierror as e: 727 logging.error('Failed to get IP address of %s, error: %s.', hostname, e) 728 729 730def get_servers_in_same_subnet(host_ip, mask_bits, servers=None, 731 server_ip_map=None): 732 """Get the servers in the same subnet of the given host ip. 733 734 @param host_ip: The IP address of a dut to look for devserver. 735 @param mask_bits: Number of mask bits. 736 @param servers: A list of servers to be filtered by subnet specified by 737 host_ip and mask_bits. 738 @param server_ip_map: A map between the server name and its IP address. 739 The map can be pre-built for better performance, e.g., when 740 allocating a drone for an agent task. 741 742 @return: A list of servers in the same subnet of the given host ip. 743 744 """ 745 matched_servers = [] 746 if not servers and not server_ip_map: 747 raise ValueError('Either `servers` or `server_ip_map` must be given.') 748 if not servers: 749 servers = server_ip_map.keys() 750 # Make sure server_ip_map is an empty dict if it's not set. 751 if not server_ip_map: 752 server_ip_map = {} 753 for server in servers: 754 server_ip = server_ip_map.get(server, get_ip_address(server)) 755 if server_ip and is_in_same_subnet(server_ip, host_ip, mask_bits): 756 matched_servers.append(server) 757 return matched_servers 758 759 760def get_restricted_subnet(hostname, restricted_subnets=RESTRICTED_SUBNETS): 761 """Get the restricted subnet of given hostname. 762 763 @param hostname: Name of the host to look for matched restricted subnet. 764 @param restricted_subnets: A list of restricted subnets, default is set to 765 RESTRICTED_SUBNETS. 766 767 @return: A tuple of (subnet_ip, mask_bits), which defines a restricted 768 subnet. 769 """ 770 host_ip = get_ip_address(hostname) 771 if not host_ip: 772 return 773 for subnet_ip, mask_bits in restricted_subnets: 774 if is_in_same_subnet(subnet_ip, host_ip, mask_bits): 775 return subnet_ip, mask_bits 776 777 778def get_wireless_ssid(hostname): 779 """Get the wireless ssid based on given hostname. 780 781 The method tries to locate the wireless ssid in the same subnet of given 782 hostname first. If none is found, it returns the default setting in 783 CLIENT/wireless_ssid. 784 785 @param hostname: Hostname of the test device. 786 787 @return: wireless ssid for the test device. 788 """ 789 default_ssid = CONFIG.get_config_value('CLIENT', 'wireless_ssid', 790 default=None) 791 host_ip = get_ip_address(hostname) 792 if not host_ip: 793 return default_ssid 794 795 # Get all wireless ssid in the global config. 796 ssids = CONFIG.get_config_value_regex('CLIENT', WIRELESS_SSID_PATTERN) 797 798 # There could be multiple subnet matches, pick the one with most strict 799 # match, i.e., the one with highest maskbit. 800 matched_ssid = default_ssid 801 matched_maskbit = -1 802 for key, value in ssids.items(): 803 # The config key filtered by regex WIRELESS_SSID_PATTERN has a format of 804 # wireless_ssid_[subnet_ip]/[maskbit], for example: 805 # wireless_ssid_192.168.0.1/24 806 # Following line extract the subnet ip and mask bit from the key name. 807 match = re.match(WIRELESS_SSID_PATTERN, key) 808 subnet_ip, maskbit = match.groups() 809 maskbit = int(maskbit) 810 if (is_in_same_subnet(subnet_ip, host_ip, maskbit) and 811 maskbit > matched_maskbit): 812 matched_ssid = value 813 matched_maskbit = maskbit 814 return matched_ssid 815 816 817def parse_launch_control_build(build_name): 818 """Get branch, target, build_id from the given Launch Control build_name. 819 820 @param build_name: Name of a Launch Control build, should be formated as 821 branch/target/build_id 822 823 @return: Tuple of branch, target, build_id 824 @raise ValueError: If the build_name is not correctly formated. 825 """ 826 branch, target, build_id = build_name.split('/') 827 return branch, target, build_id 828 829 830def parse_android_target(target): 831 """Get board and build type from the given target. 832 833 @param target: Name of an Android build target, e.g., shamu-eng. 834 835 @return: Tuple of board, build_type 836 @raise ValueError: If the target is not correctly formated. 837 """ 838 board, build_type = target.split('-') 839 return board, build_type 840 841 842def parse_launch_control_target(target): 843 """Parse the build target and type from a Launch Control target. 844 845 The Launch Control target has the format of build_target-build_type, e.g., 846 shamu-eng or dragonboard-userdebug. This method extracts the build target 847 and type from the target name. 848 849 @param target: Name of a Launch Control target, e.g., shamu-eng. 850 851 @return: (build_target, build_type), e.g., ('shamu', 'userdebug') 852 """ 853 match = re.match('(?P<build_target>.+)-(?P<build_type>[^-]+)', target) 854 if match: 855 return match.group('build_target'), match.group('build_type') 856 else: 857 return None, None 858 859 860def is_launch_control_build(build): 861 """Check if a given build is a Launch Control build. 862 863 @param build: Name of a build, e.g., 864 ChromeOS build: daisy-release/R50-1234.0.0 865 Launch Control build: git_mnc_release/shamu-eng 866 867 @return: True if the build name matches the pattern of a Launch Control 868 build, False otherwise. 869 """ 870 try: 871 _, target, _ = parse_launch_control_build(build) 872 build_target, _ = parse_launch_control_target(target) 873 if build_target: 874 return True 875 except ValueError: 876 # parse_launch_control_build or parse_launch_control_target failed. 877 pass 878 return False 879 880 881def which(exec_file): 882 """Finds an executable file. 883 884 If the file name contains a path component, it is checked as-is. 885 Otherwise, we check with each of the path components found in the system 886 PATH prepended. This behavior is similar to the 'which' command-line tool. 887 888 @param exec_file: Name or path to desired executable. 889 890 @return: An actual path to the executable, or None if not found. 891 """ 892 if os.path.dirname(exec_file): 893 return exec_file if os.access(exec_file, os.X_OK) else None 894 sys_path = os.environ.get('PATH') 895 prefix_list = sys_path.split(os.pathsep) if sys_path else [] 896 for prefix in prefix_list: 897 path = os.path.join(prefix, exec_file) 898 if os.access(path, os.X_OK): 899 return path 900 901 902class TimeoutError(error.TestError): 903 """Error raised when we time out when waiting on a condition.""" 904 pass 905 906 907def poll_for_condition(condition, 908 exception=None, 909 timeout=10, 910 sleep_interval=0.1, 911 desc=None): 912 """Polls until a condition becomes true. 913 914 @param condition: function taking no args and returning bool 915 @param exception: exception to throw if condition doesn't become true 916 @param timeout: maximum number of seconds to wait 917 @param sleep_interval: time to sleep between polls 918 @param desc: description of default TimeoutError used if 'exception' is 919 None 920 921 @return The true value that caused the poll loop to terminate. 922 923 @raise 'exception' arg if supplied; TimeoutError otherwise 924 """ 925 start_time = time.time() 926 while True: 927 value = condition() 928 if value: 929 return value 930 if time.time() + sleep_interval - start_time > timeout: 931 if exception: 932 logging.error(exception) 933 raise exception 934 935 if desc: 936 desc = 'Timed out waiting for condition: ' + desc 937 else: 938 desc = 'Timed out waiting for unnamed condition' 939 logging.error(desc) 940 raise TimeoutError(desc) 941 942 time.sleep(sleep_interval) 943