site_utils.py revision fa705d2882fb4de845957d1a064c0c268b9d1b2a
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 version_match(build_version, release_version, update_url=''): 455 """Compare release versino from lsb-release with cros-version label. 456 457 build_version is a string based on build name. It is prefixed with builder 458 info and branch ID, e.g., lumpy-release/R43-6809.0.0. It may not include 459 builder info, e.g., lumpy-release, in which case, update_url shall be passed 460 in to determine if the build is a trybot or pgo-generate build. 461 release_version is retrieved from lsb-release. 462 These two values might not match exactly. 463 464 The method is designed to compare version for following 6 scenarios with 465 samples of build version and expected release version: 466 1. trybot non-release build (paladin, pre-cq or test-ap build). 467 build version: trybot-lumpy-paladin/R27-3837.0.0-b123 468 release version: 3837.0.2013_03_21_1340 469 470 2. trybot release build. 471 build version: trybot-lumpy-release/R27-3837.0.0-b456 472 release version: 3837.0.0 473 474 3. buildbot official release build. 475 build version: lumpy-release/R27-3837.0.0 476 release version: 3837.0.0 477 478 4. non-official paladin rc build. 479 build version: lumpy-paladin/R27-3878.0.0-rc7 480 release version: 3837.0.0-rc7 481 482 5. chrome-perf build. 483 build version: lumpy-chrome-perf/R28-3837.0.0-b2996 484 release version: 3837.0.0 485 486 6. pgo-generate build. 487 build version: lumpy-release-pgo-generate/R28-3837.0.0-b2996 488 release version: 3837.0.0-pgo-generate 489 490 TODO: This logic has a bug if a trybot paladin build failed to be 491 installed in a DUT running an older trybot paladin build with same 492 platform number, but different build number (-b###). So to conclusively 493 determine if a tryjob paladin build is imaged successfully, we may need 494 to find out the date string from update url. 495 496 @param build_version: Build name for cros version, e.g. 497 peppy-release/R43-6809.0.0 or R43-6809.0.0 498 @param release_version: Release version retrieved from lsb-release, 499 e.g., 6809.0.0 500 @param update_url: Update url which include the full builder information. 501 Default is set to empty string. 502 503 @return: True if the values match, otherwise returns False. 504 """ 505 # If the build is from release, CQ or PFQ builder, cros-version label must 506 # be ended with release version in lsb-release. 507 if build_version.endswith(release_version): 508 return True 509 510 # Remove R#- and -b# at the end of build version 511 stripped_version = re.sub(r'(R\d+-|-b\d+)', '', build_version) 512 # Trim the builder info, e.g., trybot-lumpy-paladin/ 513 stripped_version = stripped_version.split('/')[-1] 514 515 is_trybot_non_release_build = ( 516 re.match(r'.*trybot-.+-(paladin|pre-cq|test-ap)', build_version) or 517 re.match(r'.*trybot-.+-(paladin|pre-cq|test-ap)', update_url)) 518 519 # Replace date string with 0 in release_version 520 release_version_no_date = re.sub(r'\d{4}_\d{2}_\d{2}_\d+', '0', 521 release_version) 522 has_date_string = release_version != release_version_no_date 523 524 is_pgo_generate_build = ( 525 re.match(r'.+-pgo-generate', build_version) or 526 re.match(r'.+-pgo-generate', update_url)) 527 528 # Remove |-pgo-generate| in release_version 529 release_version_no_pgo = release_version.replace('-pgo-generate', '') 530 has_pgo_generate = release_version != release_version_no_pgo 531 532 if is_trybot_non_release_build: 533 if not has_date_string: 534 logging.error('A trybot paladin or pre-cq build is expected. ' 535 'Version "%s" is not a paladin or pre-cq build.', 536 release_version) 537 return False 538 return stripped_version == release_version_no_date 539 elif is_pgo_generate_build: 540 if not has_pgo_generate: 541 logging.error('A pgo-generate build is expected. Version ' 542 '"%s" is not a pgo-generate build.', 543 release_version) 544 return False 545 return stripped_version == release_version_no_pgo 546 else: 547 if has_date_string: 548 logging.error('Unexpected date found in a non trybot paladin or ' 549 'pre-cq build.') 550 return False 551 # Versioned build, i.e., rc or release build. 552 return stripped_version == release_version 553 554 555def get_real_user(): 556 """Get the real user that runs the script. 557 558 The function check environment variable SUDO_USER for the user if the 559 script is run with sudo. Otherwise, it returns the value of environment 560 variable USER. 561 562 @return: The user name that runs the script. 563 564 """ 565 user = os.environ.get('SUDO_USER') 566 if not user: 567 user = os.environ.get('USER') 568 return user 569 570 571def sudo_require_password(): 572 """Test if the process can run sudo command without using password. 573 574 @return: True if the process needs password to run sudo command. 575 576 """ 577 try: 578 base_utils.run('sudo -n true') 579 return False 580 except error.CmdError: 581 logging.warn('sudo command requires password.') 582 return True 583 584 585def is_in_container(): 586 """Check if the process is running inside a container. 587 588 @return: True if the process is running inside a container, otherwise False. 589 """ 590 result = base_utils.run('grep -q "/lxc/" /proc/1/cgroup', 591 verbose=False, ignore_status=True) 592 return result.exit_status == 0 593 594 595def is_flash_installed(): 596 """ 597 The Adobe Flash binary is only distributed with internal builds. 598 """ 599 return (os.path.exists('/opt/google/chrome/pepper/libpepflashplayer.so') 600 and os.path.exists('/opt/google/chrome/pepper/pepper-flash.info')) 601 602 603def verify_flash_installed(): 604 """ 605 The Adobe Flash binary is only distributed with internal builds. 606 Warn users of public builds of the extra dependency. 607 """ 608 if not is_flash_installed(): 609 raise error.TestNAError('No Adobe Flash binary installed.') 610 611 612def is_in_same_subnet(ip_1, ip_2, mask_bits=24): 613 """Check if two IP addresses are in the same subnet with given mask bits. 614 615 The two IP addresses are string of IPv4, e.g., '192.168.0.3'. 616 617 @param ip_1: First IP address to compare. 618 @param ip_2: Second IP address to compare. 619 @param mask_bits: Number of mask bits for subnet comparison. Default to 24. 620 621 @return: True if the two IP addresses are in the same subnet. 622 623 """ 624 mask = ((2L<<mask_bits-1) -1)<<(32-mask_bits) 625 ip_1_num = struct.unpack('!I', socket.inet_aton(ip_1))[0] 626 ip_2_num = struct.unpack('!I', socket.inet_aton(ip_2))[0] 627 return ip_1_num & mask == ip_2_num & mask 628 629 630def get_ip_address(hostname): 631 """Get the IP address of given hostname. 632 633 @param hostname: Hostname of a DUT. 634 635 @return: The IP address of given hostname. None if failed to resolve 636 hostname. 637 """ 638 try: 639 if hostname: 640 return socket.gethostbyname(hostname) 641 except socket.gaierror as e: 642 logging.error('Failed to get IP address of %s, error: %s.', hostname, e) 643 644 645def get_servers_in_same_subnet(host_ip, mask_bits, servers=None, 646 server_ip_map=None): 647 """Get the servers in the same subnet of the given host ip. 648 649 @param host_ip: The IP address of a dut to look for devserver. 650 @param mask_bits: Number of mask bits. 651 @param servers: A list of servers to be filtered by subnet specified by 652 host_ip and mask_bits. 653 @param server_ip_map: A map between the server name and its IP address. 654 The map can be pre-built for better performance, e.g., when 655 allocating a drone for an agent task. 656 657 @return: A list of servers in the same subnet of the given host ip. 658 659 """ 660 matched_servers = [] 661 if not servers and not server_ip_map: 662 raise ValueError('Either `servers` or `server_ip_map` must be given.') 663 if not servers: 664 servers = server_ip_map.keys() 665 # Make sure server_ip_map is an empty dict if it's not set. 666 if not server_ip_map: 667 server_ip_map = {} 668 for server in servers: 669 server_ip = server_ip_map.get(server, get_ip_address(server)) 670 if server_ip and is_in_same_subnet(server_ip, host_ip, mask_bits): 671 matched_servers.append(server) 672 return matched_servers 673 674 675def get_restricted_subnet(hostname, restricted_subnets=RESTRICTED_SUBNETS): 676 """Get the restricted subnet of given hostname. 677 678 @param hostname: Name of the host to look for matched restricted subnet. 679 @param restricted_subnets: A list of restricted subnets, default is set to 680 RESTRICTED_SUBNETS. 681 682 @return: A tuple of (subnet_ip, mask_bits), which defines a restricted 683 subnet. 684 """ 685 host_ip = get_ip_address(hostname) 686 if not host_ip: 687 return 688 for subnet_ip, mask_bits in restricted_subnets: 689 if is_in_same_subnet(subnet_ip, host_ip, mask_bits): 690 return subnet_ip, mask_bits 691 692 693def get_wireless_ssid(hostname): 694 """Get the wireless ssid based on given hostname. 695 696 The method tries to locate the wireless ssid in the same subnet of given 697 hostname first. If none is found, it returns the default setting in 698 CLIENT/wireless_ssid. 699 700 @param hostname: Hostname of the test device. 701 702 @return: wireless ssid for the test device. 703 """ 704 default_ssid = CONFIG.get_config_value('CLIENT', 'wireless_ssid', 705 default=None) 706 host_ip = get_ip_address(hostname) 707 if not host_ip: 708 return default_ssid 709 710 # Get all wireless ssid in the global config. 711 ssids = CONFIG.get_config_value_regex('CLIENT', WIRELESS_SSID_PATTERN) 712 713 # There could be multiple subnet matches, pick the one with most strict 714 # match, i.e., the one with highest maskbit. 715 matched_ssid = default_ssid 716 matched_maskbit = -1 717 for key, value in ssids.items(): 718 # The config key filtered by regex WIRELESS_SSID_PATTERN has a format of 719 # wireless_ssid_[subnet_ip]/[maskbit], for example: 720 # wireless_ssid_192.168.0.1/24 721 # Following line extract the subnet ip and mask bit from the key name. 722 match = re.match(WIRELESS_SSID_PATTERN, key) 723 subnet_ip, maskbit = match.groups() 724 maskbit = int(maskbit) 725 if (is_in_same_subnet(subnet_ip, host_ip, maskbit) and 726 maskbit > matched_maskbit): 727 matched_ssid = value 728 matched_maskbit = maskbit 729 return matched_ssid 730 731 732def parse_launch_control_build(build_name): 733 """Get branch, target, build_id from the given Launch Control build_name. 734 735 @param build_name: Name of a Launch Control build, should be formated as 736 branch/target/build_id 737 738 @return: Tuple of branch, target, build_id 739 @raise ValueError: If the build_name is not correctly formated. 740 """ 741 branch, target, build_id = build_name.split('/') 742 return branch, target, build_id 743 744 745def parse_android_target(target): 746 """Get board and build type from the given target. 747 748 @param target: Name of an Android build target, e.g., shamu-eng. 749 750 @return: Tuple of board, build_type 751 @raise ValueError: If the target is not correctly formated. 752 """ 753 board, build_type = target.split('-') 754 return board, build_type 755 756 757def parse_launch_control_target(target): 758 """Parse the build target and type from a Launch Control target. 759 760 The Launch Control target has the format of build_target-build_type, e.g., 761 shamu-eng or dragonboard-userdebug. This method extracts the build target 762 and type from the target name. 763 764 @param target: Name of a Launch Control target, e.g., shamu-eng. 765 766 @return: (build_target, build_type), e.g., ('shamu', 'userdebug') 767 """ 768 match = re.match('(?P<build_target>.+)-(?P<build_type>[^-]+)', target) 769 if match: 770 return match.group('build_target'), match.group('build_type') 771 else: 772 return None, None 773 774 775def is_launch_control_build(build): 776 """Check if a given build is a Launch Control build. 777 778 @param build: Name of a build, e.g., 779 ChromeOS build: daisy-release/R50-1234.0.0 780 Launch Control build: git_mnc_release/shamu-eng 781 782 @return: True if the build name matches the pattern of a Launch Control 783 build, False otherwise. 784 """ 785 try: 786 _, target, _ = parse_launch_control_build(build) 787 build_target, _ = parse_launch_control_target(target) 788 if build_target: 789 return True 790 except ValueError: 791 # parse_launch_control_build or parse_launch_control_target failed. 792 pass 793 return False 794 795 796def which(exec_file): 797 """Finds an executable file. 798 799 If the file name contains a path component, it is checked as-is. 800 Otherwise, we check with each of the path components found in the system 801 PATH prepended. This behavior is similar to the 'which' command-line tool. 802 803 @param exec_file: Name or path to desired executable. 804 805 @return: An actual path to the executable, or None if not found. 806 """ 807 if os.path.dirname(exec_file): 808 return exec_file if os.access(exec_file, os.X_OK) else None 809 sys_path = os.environ.get('PATH') 810 prefix_list = sys_path.split(os.pathsep) if sys_path else [] 811 for prefix in prefix_list: 812 path = os.path.join(prefix, exec_file) 813 if os.access(path, os.X_OK): 814 return path 815 816 817class TimeoutError(error.TestError): 818 """Error raised when we time out when waiting on a condition.""" 819 pass 820 821 822def poll_for_condition(condition, 823 exception=None, 824 timeout=10, 825 sleep_interval=0.1, 826 desc=None): 827 """Polls until a condition becomes true. 828 829 @param condition: function taking no args and returning bool 830 @param exception: exception to throw if condition doesn't become true 831 @param timeout: maximum number of seconds to wait 832 @param sleep_interval: time to sleep between polls 833 @param desc: description of default TimeoutError used if 'exception' is 834 None 835 836 @return The true value that caused the poll loop to terminate. 837 838 @raise 'exception' arg if supplied; TimeoutError otherwise 839 """ 840 start_time = time.time() 841 while True: 842 value = condition() 843 if value: 844 return value 845 if time.time() + sleep_interval - start_time > timeout: 846 if exception: 847 logging.error(exception) 848 raise exception 849 850 if desc: 851 desc = 'Timed out waiting for condition: ' + desc 852 else: 853 desc = 'Timed out waiting for unnamed condition' 854 logging.error(desc) 855 raise TimeoutError(desc) 856 857 time.sleep(sleep_interval) 858