1# Copyright (c) 2012 The Chromium OS 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 5from distutils import version 6import cStringIO 7import HTMLParser 8import httplib 9import json 10import logging 11import multiprocessing 12import os 13import re 14import sys 15import urllib2 16 17from autotest_lib.client.bin import utils as site_utils 18from autotest_lib.client.common_lib import error 19from autotest_lib.client.common_lib import global_config 20from autotest_lib.client.common_lib import utils 21from autotest_lib.client.common_lib.cros import retry 22from autotest_lib.client.common_lib.cros.graphite import autotest_stats 23# TODO(cmasone): redo this class using requests module; http://crosbug.com/30107 24 25 26CONFIG = global_config.global_config 27# This file is generated at build time and specifies, per suite and per test, 28# the DEPENDENCIES list specified in each control file. It's a dict of dicts: 29# {'bvt': {'/path/to/autotest/control/site_tests/test1/control': ['dep1']} 30# 'suite': {'/path/to/autotest/control/site_tests/test2/control': ['dep2']} 31# 'power': {'/path/to/autotest/control/site_tests/test1/control': ['dep1'], 32# '/path/to/autotest/control/site_tests/test3/control': ['dep3']} 33# } 34DEPENDENCIES_FILE = 'test_suites/dependency_info' 35# Number of seconds for caller to poll devserver's is_staged call to check if 36# artifacts are staged. 37_ARTIFACT_STAGE_POLLING_INTERVAL = 5 38# Artifacts that should be staged when client calls devserver RPC to stage an 39# image. 40_ARTIFACTS_TO_BE_STAGED_FOR_IMAGE = 'full_payload,test_suites,stateful' 41# Artifacts that should be staged when client calls devserver RPC to stage an 42# image with autotest artifact. 43_ARTIFACTS_TO_BE_STAGED_FOR_IMAGE_WITH_AUTOTEST = ('full_payload,test_suites,' 44 'control_files,stateful,' 45 'autotest_packages') 46# Artifacts that should be staged when client calls devserver RPC to stage an 47# Android build. 48_ANDROID_ARTIFACTS_TO_BE_STAGED_FOR_IMAGE = ('bootloader_image,radio_image,' 49 'zip_images,test_zip') 50# Artifacts that should be staged when client calls devserver RPC to stage an 51# Android build. 52_BRILLO_ARTIFACTS_TO_BE_STAGED_FOR_IMAGE = ('zip_images,vendor_partitions') 53SKIP_DEVSERVER_HEALTH_CHECK = CONFIG.get_config_value( 54 'CROS', 'skip_devserver_health_check', type=bool) 55# Number of seconds for the call to get devserver load to time out. 56TIMEOUT_GET_DEVSERVER_LOAD = 2.0 57 58# Android artifact path in devserver 59ANDROID_BUILD_NAME_PATTERN = CONFIG.get_config_value( 60 'CROS', 'android_build_name_pattern', type=str).replace('\\', '') 61 62# Return value from a devserver RPC indicating the call succeeded. 63SUCCESS = 'Success' 64 65PREFER_LOCAL_DEVSERVER = CONFIG.get_config_value( 66 'CROS', 'prefer_local_devserver', type=bool, default=False) 67 68ENABLE_DEVSERVER_IN_RESTRICTED_SUBNET = CONFIG.get_config_value( 69 'CROS', 'enable_devserver_in_restricted_subnet', type=bool, 70 default=False) 71 72class MarkupStripper(HTMLParser.HTMLParser): 73 """HTML parser that strips HTML tags, coded characters like & 74 75 Works by, basically, not doing anything for any tags, and only recording 76 the content of text nodes in an internal data structure. 77 """ 78 def __init__(self): 79 self.reset() 80 self.fed = [] 81 82 83 def handle_data(self, d): 84 """Consume content of text nodes, store it away.""" 85 self.fed.append(d) 86 87 88 def get_data(self): 89 """Concatenate and return all stored data.""" 90 return ''.join(self.fed) 91 92 93def _get_image_storage_server(): 94 return CONFIG.get_config_value('CROS', 'image_storage_server', type=str) 95 96 97def _get_canary_channel_server(): 98 """ 99 Get the url of the canary-channel server, 100 eg: gsutil://chromeos-releases/canary-channel/<board>/<release> 101 102 @return: The url to the canary channel server. 103 """ 104 return CONFIG.get_config_value('CROS', 'canary_channel_server', type=str) 105 106 107def _get_storage_server_for_artifacts(artifacts=None): 108 """Gets the appropriate storage server for the given artifacts. 109 110 @param artifacts: A list of artifacts we need to stage. 111 @return: The address of the storage server that has these artifacts. 112 The default image storage server if no artifacts are specified. 113 """ 114 factory_artifact = global_config.global_config.get_config_value( 115 'CROS', 'factory_artifact', type=str, default='') 116 if artifacts and factory_artifact and factory_artifact in artifacts: 117 return _get_canary_channel_server() 118 return _get_image_storage_server() 119 120 121def _get_dev_server_list(): 122 return CONFIG.get_config_value('CROS', 'dev_server', type=list, default=[]) 123 124 125def _get_crash_server_list(): 126 return CONFIG.get_config_value('CROS', 'crash_server', type=list, 127 default=[]) 128 129 130def remote_devserver_call(timeout_min=30): 131 """A decorator to use with remote devserver calls. 132 133 This decorator converts urllib2.HTTPErrors into DevServerExceptions with 134 any embedded error info converted into plain text. 135 The method retries on urllib2.URLError to avoid devserver flakiness. 136 """ 137 #pylint: disable=C0111 138 def inner_decorator(method): 139 140 @retry.retry(urllib2.URLError, timeout_min=timeout_min) 141 def wrapper(*args, **kwargs): 142 """This wrapper actually catches the HTTPError.""" 143 try: 144 return method(*args, **kwargs) 145 except urllib2.HTTPError as e: 146 error_markup = e.read() 147 strip = MarkupStripper() 148 try: 149 strip.feed(error_markup.decode('utf_32')) 150 except UnicodeDecodeError: 151 strip.feed(error_markup) 152 raise DevServerException(strip.get_data()) 153 154 return wrapper 155 156 return inner_decorator 157 158 159class DevServerException(Exception): 160 """Raised when the dev server returns a non-200 HTTP response.""" 161 pass 162 163 164class DevServer(object): 165 """Base class for all DevServer-like server stubs. 166 167 This is the base class for interacting with all Dev Server-like servers. 168 A caller should instantiate a sub-class of DevServer with: 169 170 host = SubClassServer.resolve(build) 171 server = SubClassServer(host) 172 """ 173 _MIN_FREE_DISK_SPACE_GB = 20 174 # Threshold for the CPU load percentage for a devserver to be selected. 175 MAX_CPU_LOAD = 80.0 176 # Threshold for the network IO, set to 80MB/s 177 MAX_NETWORK_IO = 1024 * 1024 * 80 178 DISK_IO = 'disk_total_bytes_per_second' 179 NETWORK_IO = 'network_total_bytes_per_second' 180 CPU_LOAD = 'cpu_percent' 181 FREE_DISK = 'free_disk' 182 STAGING_THREAD_COUNT = 'staging_thread_count' 183 184 185 def __init__(self, devserver): 186 self._devserver = devserver 187 188 189 def url(self): 190 """Returns the url for this devserver.""" 191 return self._devserver 192 193 194 @staticmethod 195 def get_server_name(url): 196 """Strip the http:// prefix and port from a url. 197 198 @param url: A url of a server. 199 200 @return the server name without http:// prefix and port. 201 202 """ 203 return re.sub(r':\d+$', '', url.lstrip('http://')) 204 205 206 @staticmethod 207 def get_devserver_load_wrapper(devserver, timeout_sec, output): 208 """A wrapper function to call get_devserver_load in parallel. 209 210 @param devserver: url of the devserver. 211 @param timeout_sec: Number of seconds before time out the devserver 212 call. 213 @param output: An output queue to save results to. 214 """ 215 load = DevServer.get_devserver_load(devserver, 216 timeout_min=timeout_sec/60.0) 217 if load: 218 load['devserver'] = devserver 219 output.put(load) 220 221 222 @staticmethod 223 def get_devserver_load(devserver, timeout_min=0.1): 224 """Returns True if the |devserver| is healthy to stage build. 225 226 @param devserver: url of the devserver. 227 @param timeout_min: How long to wait in minutes before deciding the 228 the devserver is not up (float). 229 230 @return: A dictionary of the devserver's load. 231 232 """ 233 server_name = DevServer.get_server_name(devserver) 234 # statsd treats |.| as path separator. 235 server_name = server_name.replace('.', '_') 236 call = DevServer._build_call(devserver, 'check_health') 237 238 @remote_devserver_call(timeout_min=timeout_min) 239 def make_call(): 240 """Inner method that makes the call.""" 241 return utils.urlopen_socket_timeout( 242 call, timeout=timeout_min * 60).read() 243 244 try: 245 result_dict = json.load(cStringIO.StringIO(make_call())) 246 for key, val in result_dict.iteritems(): 247 try: 248 autotest_stats.Gauge(server_name).send(key, float(val)) 249 except ValueError: 250 # Ignore all non-numerical health data. 251 pass 252 253 return result_dict 254 except Exception as e: 255 logging.error('Devserver call failed: "%s", timeout: %s seconds,' 256 ' Error: %s', call, timeout_min * 60, e) 257 258 259 @staticmethod 260 def is_free_disk_ok(load): 261 """Check if a devserver has enough free disk. 262 263 @param load: A dict of the load of the devserver. 264 265 @return: True if the devserver has enough free disk or disk check is 266 skipped in global config. 267 268 """ 269 if SKIP_DEVSERVER_HEALTH_CHECK: 270 logging.debug('devserver health check is skipped.') 271 elif load[DevServer.FREE_DISK] < DevServer._MIN_FREE_DISK_SPACE_GB: 272 return False 273 274 return True 275 276 277 @staticmethod 278 def devserver_healthy(devserver, timeout_min=0.1): 279 """Returns True if the |devserver| is healthy to stage build. 280 281 @param devserver: url of the devserver. 282 @param timeout_min: How long to wait in minutes before deciding the 283 the devserver is not up (float). 284 285 @return: True if devserver is healthy. Return False otherwise. 286 287 """ 288 server_name = DevServer.get_server_name(devserver) 289 # statsd treats |.| as path separator. 290 server_name = server_name.replace('.', '_') 291 load = DevServer.get_devserver_load(devserver, timeout_min=timeout_min) 292 if not load: 293 # Failed to get the load of devserver. 294 autotest_stats.Counter(server_name + 295 '.devserver_not_healthy').increment() 296 return False 297 298 disk_ok = DevServer.is_free_disk_ok(load) 299 if not disk_ok: 300 logging.error('Devserver check_health failed. Free disk space is ' 301 'low. Only %dGB is available.', 302 load[DevServer.FREE_DISK]) 303 counter = '.devserver_healthy' if disk_ok else '.devserver_not_healthy' 304 # This counter indicates the load of a devserver. By comparing the 305 # value of this counter for all devservers, we can evaluate the 306 # load balancing across all devservers. 307 autotest_stats.Counter(server_name + counter).increment() 308 return disk_ok 309 310 311 @staticmethod 312 def _build_call(host, method, **kwargs): 313 """Build a URL to |host| that calls |method|, passing |kwargs|. 314 315 Builds a URL that calls |method| on the dev server defined by |host|, 316 passing a set of key/value pairs built from the dict |kwargs|. 317 318 @param host: a string that is the host basename e.g. http://server:90. 319 @param method: the dev server method to call. 320 @param kwargs: a dict mapping arg names to arg values. 321 @return the URL string. 322 """ 323 argstr = '&'.join(map(lambda x: "%s=%s" % x, kwargs.iteritems())) 324 return "%(host)s/%(method)s?%(argstr)s" % dict( 325 host=host, method=method, argstr=argstr) 326 327 328 def build_call(self, method, **kwargs): 329 """Builds a devserver RPC string that can be invoked using urllib.open. 330 331 @param method: remote devserver method to call. 332 """ 333 return self._build_call(self._devserver, method, **kwargs) 334 335 336 @classmethod 337 def build_all_calls(cls, method, **kwargs): 338 """Builds a list of URLs that makes RPC calls on all devservers. 339 340 Build a URL that calls |method| on the dev server, passing a set 341 of key/value pairs built from the dict |kwargs|. 342 343 @param method: the dev server method to call. 344 @param kwargs: a dict mapping arg names to arg values 345 @return the URL string 346 """ 347 calls = [] 348 # Note we use cls.servers as servers is class specific. 349 for server in cls.servers(): 350 if cls.devserver_healthy(server): 351 calls.append(cls._build_call(server, method, **kwargs)) 352 353 return calls 354 355 356 @staticmethod 357 def servers(): 358 """Returns a list of servers that can serve as this type of server.""" 359 raise NotImplementedError() 360 361 362 @classmethod 363 def get_devservers_in_same_subnet(cls, ip, mask_bits=19): 364 """Get the devservers in the same subnet of the given ip. 365 366 @param ip: The IP address of a dut to look for devserver. 367 @param mask_bits: Number of mask bits. Default is 19. 368 369 @return: A list of devservers in the same subnet of the given ip. 370 371 """ 372 # server from cls.servers() is a URL, e.g., http://10.1.1.10:8082, so 373 # we need a dict to return the full devserver path once the IPs are 374 # filtered in get_servers_in_same_subnet. 375 server_names = {} 376 all_devservers = [] 377 for server in cls.servers(): 378 server_name = ImageServer.get_server_name(server) 379 server_names[server_name] = server 380 all_devservers.append(server_name) 381 devservers = utils.get_servers_in_same_subnet(ip, mask_bits, 382 all_devservers) 383 return [server_names[s] for s in devservers] 384 385 386 @classmethod 387 def get_unrestricted_devservers( 388 cls, restricted_subnet=utils.RESTRICTED_SUBNETS): 389 """Get the devservers not in any restricted subnet specified in 390 restricted_subnet. 391 392 @param restricted_subnet: A list of restriected subnets. 393 394 @return: A list of devservers not in any restricted subnet. 395 396 """ 397 devservers = [] 398 for server in cls.servers(): 399 server_name = ImageServer.get_server_name(server) 400 if not utils.get_restricted_subnet(server_name, restricted_subnet): 401 devservers.append(server) 402 return devservers 403 404 405 @classmethod 406 def get_healthy_devserver(cls, build, devservers): 407 """"Get a healthy devserver instance from the list of devservers. 408 409 @param build: The build (e.g. x86-mario-release/R18-1586.0.0-a1-b1514). 410 411 @return: A DevServer object of a healthy devserver. Return None if no 412 healthy devserver is found. 413 414 """ 415 while devservers: 416 hash_index = hash(build) % len(devservers) 417 devserver = devservers.pop(hash_index) 418 if cls.devserver_healthy(devserver): 419 return cls(devserver) 420 421 422 @classmethod 423 def resolve(cls, build, hostname=None): 424 """"Resolves a build to a devserver instance. 425 426 @param build: The build (e.g. x86-mario-release/R18-1586.0.0-a1-b1514). 427 @param hostname: The hostname of dut that requests a devserver. It's 428 used to make sure a devserver in the same subnet is 429 preferred. 430 431 @raise DevServerException: If no devserver is available. 432 """ 433 host_ip = None 434 if hostname: 435 host_ip = site_utils.get_ip_address(hostname) 436 if not host_ip: 437 logging.error('Failed to get IP address of %s. Will pick a ' 438 'devserver without subnet constraint.', hostname) 439 440 devservers = cls.servers() 441 442 # Go through all restricted subnet settings and check if the DUT is 443 # inside a restricted subnet. If so, get the subnet setting. 444 restricted_subnet = None 445 if host_ip and ENABLE_DEVSERVER_IN_RESTRICTED_SUBNET: 446 for subnet_ip, mask_bits in utils.RESTRICTED_SUBNETS: 447 if utils.is_in_same_subnet(host_ip, subnet_ip, mask_bits): 448 restricted_subnet = subnet_ip 449 logging.debug('The host %s (%s) is in a restricted subnet. ' 450 'Try to locate a devserver inside subnet ' 451 '%s:%d.', hostname, host_ip, subnet_ip, 452 mask_bits) 453 devservers = cls.get_devservers_in_same_subnet( 454 subnet_ip, mask_bits) 455 break 456 # If devserver election is not restricted and 457 # enable_devserver_in_restricted_subnet in global config is set to True, 458 # select a devserver from unrestricted servers. Otherwise, drone will 459 # not be able to access devserver in restricted subnet. 460 can_retry = False 461 if (not restricted_subnet and utils.RESTRICTED_SUBNETS and 462 ENABLE_DEVSERVER_IN_RESTRICTED_SUBNET): 463 devservers = cls.get_unrestricted_devservers() 464 if PREFER_LOCAL_DEVSERVER and host_ip: 465 can_retry = True 466 devservers = cls.get_devserver_in_same_subnet( 467 host_ip, cls.get_unrestricted_devservers() ) 468 devserver = cls.get_healthy_devserver(build, devservers) 469 470 if not devserver and can_retry: 471 devserver = cls.get_healthy_devserver( 472 build, cls.get_unrestricted_devservers()) 473 if devserver: 474 return devserver 475 else: 476 if restricted_subnet: 477 subnet_error = ('in the same subnet as the host %s (%s)' % 478 (hostname, host_ip)) 479 else: 480 subnet_error = '' 481 error_msg = 'All devservers %s are currently down!!!' % subnet_error 482 logging.error(error_msg) 483 raise DevServerException(error_msg) 484 485 486class CrashServer(DevServer): 487 """Class of DevServer that symbolicates crash dumps.""" 488 @staticmethod 489 def servers(): 490 return _get_crash_server_list() 491 492 493 @remote_devserver_call() 494 def symbolicate_dump(self, minidump_path, build): 495 """Ask the devserver to symbolicate the dump at minidump_path. 496 497 Stage the debug symbols for |build| and, if that works, ask the 498 devserver to symbolicate the dump at |minidump_path|. 499 500 @param minidump_path: the on-disk path of the minidump. 501 @param build: The build (e.g. x86-mario-release/R18-1586.0.0-a1-b1514) 502 whose debug symbols are needed for symbolication. 503 @return The contents of the stack trace 504 @raise DevServerException upon any return code that's not HTTP OK. 505 """ 506 try: 507 import requests 508 except ImportError: 509 logging.warning("Can't 'import requests' to connect to dev server.") 510 return '' 511 server_name = self.get_server_name(self.url()) 512 server_name = server_name.replace('.', '_') 513 stats_key = 'CrashServer.%s.symbolicate_dump' % server_name 514 autotest_stats.Counter(stats_key).increment() 515 timer = autotest_stats.Timer(stats_key) 516 timer.start() 517 # Symbolicate minidump. 518 call = self.build_call('symbolicate_dump', 519 archive_url=_get_image_storage_server() + build) 520 request = requests.post( 521 call, files={'minidump': open(minidump_path, 'rb')}) 522 if request.status_code == requests.codes.OK: 523 timer.stop() 524 return request.text 525 526 error_fd = cStringIO.StringIO(request.text) 527 raise urllib2.HTTPError( 528 call, request.status_code, request.text, request.headers, 529 error_fd) 530 531 532class ImageServerBase(DevServer): 533 """Base class for devservers used to stage builds. 534 535 CrOS and Android builds are staged in different ways as they have different 536 sets of artifacts. This base class abstracts the shared functions between 537 the two types of ImageServer. 538 """ 539 540 @classmethod 541 def servers(cls): 542 """Returns a list of servers that can serve as a desired type of 543 devserver. 544 """ 545 return _get_dev_server_list() 546 547 548 def _get_image_url(self, image): 549 """Returns the url of the directory for this image on the devserver. 550 551 @param image: the image that was fetched. 552 """ 553 image = self.translate(image) 554 url_pattern = CONFIG.get_config_value('CROS', 'image_url_pattern', 555 type=str) 556 return (url_pattern % (self.url(), image)).replace('update', 'static') 557 558 559 @staticmethod 560 def create_stats_str(subname, server_name, artifacts): 561 """Create a graphite name given the staged items. 562 563 The resulting name will look like 564 'dev_server.subname.DEVSERVER_URL.artifact1_artifact2' 565 The name can be used to create a stats object like 566 stats.Timer, stats.Counter, etc. 567 568 @param subname: A name for the graphite sub path. 569 @param server_name: name of the devserver, e.g 172.22.33.44. 570 @param artifacts: A list of artifacts. 571 572 @return A name described above. 573 574 """ 575 staged_items = sorted(artifacts) if artifacts else [] 576 staged_items_str = '_'.join(staged_items).replace( 577 '.', '_') if staged_items else None 578 server_name = server_name.replace('.', '_') 579 stats_str = 'dev_server.%s.%s' % (subname, server_name) 580 if staged_items_str: 581 stats_str += '.%s' % staged_items_str 582 return stats_str 583 584 585 @staticmethod 586 def create_metadata(server_name, image, artifacts=None, files=None): 587 """Create a metadata dictionary given the staged items. 588 589 The metadata can be send to metadata db along with stats. 590 591 @param server_name: name of the devserver, e.g 172.22.33.44. 592 @param image: The name of the image. 593 @param artifacts: A list of artifacts. 594 @param files: A list of files. 595 596 @return A metadata dictionary. 597 598 """ 599 metadata = {'devserver': server_name, 600 'image': image, 601 '_type': 'devserver'} 602 if artifacts: 603 metadata['artifacts'] = ' '.join(artifacts) 604 if files: 605 metadata['files'] = ' '.join(files) 606 return metadata 607 608 609 def _poll_is_staged(self, **kwargs): 610 """Polling devserver.is_staged until all artifacts are staged. 611 612 @param kwargs: keyword arguments to make is_staged devserver call. 613 614 @return: True if all artifacts are staged in devserver. 615 """ 616 call = self.build_call('is_staged', **kwargs) 617 618 def all_staged(): 619 """Call devserver.is_staged rpc to check if all files are staged. 620 621 @return: True if all artifacts are staged in devserver. False 622 otherwise. 623 @rasies DevServerException, the exception is a wrapper of all 624 exceptions that were raised when devserver tried to download 625 the artifacts. devserver raises an HTTPError when an 626 exception was raised in the code. Such exception should be 627 re-raised here to stop the caller from waiting. If the call 628 to devserver failed for connection issue, a URLError 629 exception is raised, and caller should retry the call to 630 avoid such network flakiness. 631 632 """ 633 try: 634 return urllib2.urlopen(call).read() == 'True' 635 except urllib2.HTTPError as e: 636 error_markup = e.read() 637 strip = MarkupStripper() 638 try: 639 strip.feed(error_markup.decode('utf_32')) 640 except UnicodeDecodeError: 641 strip.feed(error_markup) 642 raise DevServerException(strip.get_data()) 643 except urllib2.URLError as e: 644 # Could be connection issue, retry it. 645 # For example: <urlopen error [Errno 111] Connection refused> 646 return False 647 648 site_utils.poll_for_condition( 649 all_staged, 650 exception=site_utils.TimeoutError(), 651 timeout=sys.maxint, 652 sleep_interval=_ARTIFACT_STAGE_POLLING_INTERVAL) 653 654 return True 655 656 657 def _call_and_wait(self, call_name, error_message, 658 expected_response=SUCCESS, **kwargs): 659 """Helper method to make a urlopen call, and wait for artifacts staged. 660 661 @param call_name: name of devserver rpc call. 662 @param error_message: Error message to be thrown if response does not 663 match expected_response. 664 @param expected_response: Expected response from rpc, default to 665 |Success|. If it's set to None, do not compare 666 the actual response. Any response is consider 667 to be good. 668 @param kwargs: keyword arguments to make is_staged devserver call. 669 670 @return: The response from rpc. 671 @raise DevServerException upon any return code that's expected_response. 672 673 """ 674 call = self.build_call(call_name, async=True, **kwargs) 675 try: 676 response = urllib2.urlopen(call).read() 677 except httplib.BadStatusLine as e: 678 logging.error(e) 679 raise DevServerException('Received Bad Status line, Devserver %s ' 680 'might have gone down while handling ' 681 'the call: %s' % (self.url(), call)) 682 683 if expected_response and not response == expected_response: 684 raise DevServerException(error_message) 685 686 # `os_type` is needed in build a devserver call, but not needed for 687 # wait_for_artifacts_staged, since that method is implemented by 688 # each ImageServerBase child class. 689 if 'os_type' in kwargs: 690 del kwargs['os_type'] 691 self.wait_for_artifacts_staged(**kwargs) 692 return response 693 694 695 def _stage_artifacts(self, build, artifacts, files, archive_url, **kwargs): 696 """Tell the devserver to download and stage |artifacts| from |image| 697 specified by kwargs. 698 699 This is the main call point for staging any specific artifacts for a 700 given build. To see the list of artifacts one can stage see: 701 702 ~src/platfrom/dev/artifact_info.py. 703 704 This is maintained along with the actual devserver code. 705 706 @param artifacts: A list of artifacts. 707 @param files: A list of files to stage. 708 @param archive_url: Optional parameter that has the archive_url to stage 709 this artifact from. Default is specified in autotest config + 710 image. 711 @param kwargs: keyword arguments that specify the build information, to 712 make stage devserver call. 713 714 @raise DevServerException upon any return code that's not HTTP OK. 715 """ 716 if not archive_url: 717 archive_url = _get_storage_server_for_artifacts(artifacts) + build 718 719 artifacts_arg = ','.join(artifacts) if artifacts else '' 720 files_arg = ','.join(files) if files else '' 721 error_message = ("staging %s for %s failed;" 722 "HTTP OK not accompanied by 'Success'." % 723 ('artifacts=%s files=%s ' % (artifacts_arg, files_arg), 724 build)) 725 726 staging_info = ('build=%s, artifacts=%s, files=%s, archive_url=%s' % 727 (build, artifacts, files, archive_url)) 728 logging.info('Staging artifacts on devserver %s: %s', 729 self.url(), staging_info) 730 if artifacts: 731 server_name = self.get_server_name(self.url()) 732 timer_key = self.create_stats_str( 733 'stage_artifacts', server_name, artifacts) 734 counter_key = self.create_stats_str( 735 'stage_artifacts_count', server_name, artifacts) 736 metadata = self.create_metadata(server_name, build, artifacts, 737 files) 738 autotest_stats.Counter(counter_key, metadata=metadata).increment() 739 timer = autotest_stats.Timer(timer_key, metadata=metadata) 740 timer.start() 741 try: 742 arguments = {'archive_url': archive_url, 743 'artifacts': artifacts_arg, 744 'files': files_arg} 745 if kwargs: 746 arguments.update(kwargs) 747 self.call_and_wait(call_name='stage',error_message=error_message, 748 **arguments) 749 if artifacts: 750 timer.stop() 751 logging.info('Finished staging artifacts: %s', staging_info) 752 except error.TimeoutException: 753 logging.error('stage_artifacts timed out: %s', staging_info) 754 if artifacts: 755 timeout_key = self.create_stats_str( 756 'stage_artifacts_timeout', server_name, artifacts) 757 autotest_stats.Counter(timeout_key, 758 metadata=metadata).increment() 759 raise DevServerException( 760 'stage_artifacts timed out: %s' % staging_info) 761 762 763 def call_and_wait(self, *args, **kwargs): 764 """Helper method to make a urlopen call, and wait for artifacts staged. 765 766 This method needs to be overridden in the subclass to implement the 767 logic to call _call_and_wait. 768 """ 769 raise NotImplementedError 770 771 772 def _trigger_download(self, build, artifacts, files, synchronous=True, 773 **kwargs_build_info): 774 """Tell the devserver to download and stage image specified in 775 kwargs_build_info. 776 777 Tells the devserver to fetch |image| from the image storage server 778 named by _get_image_storage_server(). 779 780 If |synchronous| is True, waits for the entire download to finish 781 staging before returning. Otherwise only the artifacts necessary 782 to start installing images onto DUT's will be staged before returning. 783 A caller can then call finish_download to guarantee the rest of the 784 artifacts have finished staging. 785 786 @param synchronous: if True, waits until all components of the image are 787 staged before returning. 788 @param kwargs_build_info: Dictionary of build information. 789 For CrOS, it is None as build is the CrOS image name. 790 For Android, it is {'target': target, 791 'build_id': build_id, 792 'branch': branch} 793 794 @raise DevServerException upon any return code that's not HTTP OK. 795 796 """ 797 if kwargs_build_info: 798 archive_url = None 799 else: 800 archive_url = _get_image_storage_server() + build 801 error_message = ("trigger_download for %s failed;" 802 "HTTP OK not accompanied by 'Success'." % build) 803 kwargs = {'archive_url': archive_url, 804 'artifacts': artifacts, 805 'files': files, 806 'error_message': error_message} 807 if kwargs_build_info: 808 kwargs.update(kwargs_build_info) 809 810 logging.info('trigger_download starts for %s', build) 811 server_name = self.get_server_name(self.url()) 812 artifacts_list = artifacts.split(',') 813 counter_key = self.create_stats_str( 814 'trigger_download_count', server_name, artifacts_list) 815 metadata = self.create_metadata(server_name, build, artifacts_list) 816 autotest_stats.Counter(counter_key, metadata=metadata).increment() 817 try: 818 response = self.call_and_wait(call_name='stage', **kwargs) 819 logging.info('trigger_download finishes for %s', build) 820 except error.TimeoutException: 821 logging.error('trigger_download timed out for %s.', build) 822 timeout_key = self.create_stats_str( 823 'trigger_download_timeout', server_name, artifacts_list) 824 autotest_stats.Counter(timeout_key, metadata=metadata).increment() 825 raise DevServerException( 826 'trigger_download timed out for %s.' % build) 827 was_successful = response == SUCCESS 828 if was_successful and synchronous: 829 self._finish_download(build, artifacts, files, **kwargs_build_info) 830 831 832 def _finish_download(self, build, artifacts, files, **kwargs_build_info): 833 """Tell the devserver to finish staging image specified in 834 kwargs_build_info. 835 836 If trigger_download is called with synchronous=False, it will return 837 before all artifacts have been staged. This method contacts the 838 devserver and blocks until all staging is completed and should be 839 called after a call to trigger_download. 840 841 @param kwargs_build_info: Dictionary of build information. 842 For CrOS, it is None as build is the CrOS image name. 843 For Android, it is {'target': target, 844 'build_id': build_id, 845 'branch': branch} 846 847 @raise DevServerException upon any return code that's not HTTP OK. 848 """ 849 archive_url = _get_image_storage_server() + build 850 error_message = ("finish_download for %s failed;" 851 "HTTP OK not accompanied by 'Success'." % build) 852 kwargs = {'archive_url': archive_url, 853 'artifacts': artifacts, 854 'files': files, 855 'error_message': error_message} 856 if kwargs_build_info: 857 kwargs.update(kwargs_build_info) 858 try: 859 self.call_and_wait(call_name='stage', **kwargs) 860 except error.TimeoutException: 861 logging.error('finish_download timed out for %s', build) 862 server_name = self.get_server_name(self.url()) 863 artifacts_list = artifacts.split(',') 864 timeout_key = self.create_stats_str( 865 'finish_download_timeout', server_name, artifacts_list) 866 metadata = self.create_metadata(server_name, build, artifacts_list) 867 autotest_stats.Counter(timeout_key, metadata=metadata).increment() 868 raise DevServerException( 869 'finish_download timed out for %s.' % build) 870 871 872class ImageServer(ImageServerBase): 873 """Class for DevServer that handles RPCs related to CrOS images. 874 875 The calls to devserver to stage artifacts, including stage and download, are 876 made in async mode. That is, when caller makes an RPC |stage| to request 877 devserver to stage certain artifacts, devserver handles the call and starts 878 staging artifacts in a new thread, and return |Success| without waiting for 879 staging being completed. When caller receives message |Success|, it polls 880 devserver's is_staged call until all artifacts are staged. 881 Such mechanism is designed to prevent cherrypy threads in devserver being 882 running out, as staging artifacts might take long time, and cherrypy starts 883 with a fixed number of threads that handle devserver rpc. 884 """ 885 886 class ArtifactUrls(object): 887 """A container for URLs of staged artifacts. 888 889 Attributes: 890 full_payload: URL for downloading a staged full release update 891 mton_payload: URL for downloading a staged M-to-N release update 892 nton_payload: URL for downloading a staged N-to-N release update 893 894 """ 895 def __init__(self, full_payload=None, mton_payload=None, 896 nton_payload=None): 897 self.full_payload = full_payload 898 self.mton_payload = mton_payload 899 self.nton_payload = nton_payload 900 901 902 def wait_for_artifacts_staged(self, archive_url, artifacts='', files=''): 903 """Polling devserver.is_staged until all artifacts are staged. 904 905 @param archive_url: Google Storage URL for the build. 906 @param artifacts: Comma separated list of artifacts to download. 907 @param files: Comma separated list of files to download. 908 @return: True if all artifacts are staged in devserver. 909 """ 910 kwargs = {'archive_url': archive_url, 911 'artifacts': artifacts, 912 'files': files} 913 return self._poll_is_staged(**kwargs) 914 915 916 @remote_devserver_call() 917 def call_and_wait(self, call_name, archive_url, artifacts, files, 918 error_message, expected_response=SUCCESS): 919 """Helper method to make a urlopen call, and wait for artifacts staged. 920 921 @param call_name: name of devserver rpc call. 922 @param archive_url: Google Storage URL for the build.. 923 @param artifacts: Comma separated list of artifacts to download. 924 @param files: Comma separated list of files to download. 925 @param expected_response: Expected response from rpc, default to 926 |Success|. If it's set to None, do not compare 927 the actual response. Any response is consider 928 to be good. 929 @param error_message: Error message to be thrown if response does not 930 match expected_response. 931 932 @return: The response from rpc. 933 @raise DevServerException upon any return code that's expected_response. 934 935 """ 936 kwargs = {'archive_url': archive_url, 937 'artifacts': artifacts, 938 'files': files} 939 return self._call_and_wait(call_name, error_message, 940 expected_response, **kwargs) 941 942 943 @remote_devserver_call() 944 def stage_artifacts(self, image, artifacts=None, files='', 945 archive_url=None): 946 """Tell the devserver to download and stage |artifacts| from |image|. 947 948 This is the main call point for staging any specific artifacts for a 949 given build. To see the list of artifacts one can stage see: 950 951 ~src/platfrom/dev/artifact_info.py. 952 953 This is maintained along with the actual devserver code. 954 955 @param image: the image to fetch and stage. 956 @param artifacts: A list of artifacts. 957 @param files: A list of files to stage. 958 @param archive_url: Optional parameter that has the archive_url to stage 959 this artifact from. Default is specified in autotest config + 960 image. 961 962 @raise DevServerException upon any return code that's not HTTP OK. 963 """ 964 if not artifacts and not files: 965 raise DevServerException('Must specify something to stage.') 966 image = self.translate(image) 967 self._stage_artifacts(image, artifacts, files, archive_url) 968 969 970 @remote_devserver_call(timeout_min=0.5) 971 def list_image_dir(self, image): 972 """List the contents of the image stage directory, on the devserver. 973 974 @param image: The image name, eg: <board>-<branch>/<Milestone>-<build>. 975 976 @raise DevServerException upon any return code that's not HTTP OK. 977 """ 978 image = self.translate(image) 979 logging.info('Requesting contents from devserver %s for image %s', 980 self.url(), image) 981 archive_url = _get_storage_server_for_artifacts() + image 982 call = self.build_call('list_image_dir', archive_url=archive_url) 983 response = urllib2.urlopen(call) 984 for line in [line.rstrip() for line in response]: 985 logging.info(line) 986 987 988 def trigger_download(self, image, synchronous=True): 989 """Tell the devserver to download and stage |image|. 990 991 Tells the devserver to fetch |image| from the image storage server 992 named by _get_image_storage_server(). 993 994 If |synchronous| is True, waits for the entire download to finish 995 staging before returning. Otherwise only the artifacts necessary 996 to start installing images onto DUT's will be staged before returning. 997 A caller can then call finish_download to guarantee the rest of the 998 artifacts have finished staging. 999 1000 @param image: the image to fetch and stage. 1001 @param synchronous: if True, waits until all components of the image are 1002 staged before returning. 1003 1004 @raise DevServerException upon any return code that's not HTTP OK. 1005 1006 """ 1007 image = self.translate(image) 1008 artifacts = _ARTIFACTS_TO_BE_STAGED_FOR_IMAGE 1009 self._trigger_download(image, artifacts, files='', 1010 synchronous=synchronous) 1011 1012 1013 @remote_devserver_call() 1014 def setup_telemetry(self, build): 1015 """Tell the devserver to setup telemetry for this build. 1016 1017 The devserver will stage autotest and then extract the required files 1018 for telemetry. 1019 1020 @param build: the build to setup telemetry for. 1021 1022 @returns path on the devserver that telemetry is installed to. 1023 """ 1024 build = self.translate(build) 1025 archive_url = _get_image_storage_server() + build 1026 call = self.build_call('setup_telemetry', archive_url=archive_url) 1027 try: 1028 response = urllib2.urlopen(call).read() 1029 except httplib.BadStatusLine as e: 1030 logging.error(e) 1031 raise DevServerException('Received Bad Status line, Devserver %s ' 1032 'might have gone down while handling ' 1033 'the call: %s' % (self.url(), call)) 1034 return response 1035 1036 1037 def finish_download(self, image): 1038 """Tell the devserver to finish staging |image|. 1039 1040 If trigger_download is called with synchronous=False, it will return 1041 before all artifacts have been staged. This method contacts the 1042 devserver and blocks until all staging is completed and should be 1043 called after a call to trigger_download. 1044 1045 @param image: the image to fetch and stage. 1046 @raise DevServerException upon any return code that's not HTTP OK. 1047 """ 1048 image = self.translate(image) 1049 artifacts = _ARTIFACTS_TO_BE_STAGED_FOR_IMAGE_WITH_AUTOTEST 1050 self._finish_download(image, artifacts, files='') 1051 1052 1053 def get_update_url(self, image): 1054 """Returns the url that should be passed to the updater. 1055 1056 @param image: the image that was fetched. 1057 """ 1058 image = self.translate(image) 1059 url_pattern = CONFIG.get_config_value('CROS', 'image_url_pattern', 1060 type=str) 1061 return (url_pattern % (self.url(), image)) 1062 1063 1064 def get_staged_file_url(self, filename, image): 1065 """Returns the url of a staged file for this image on the devserver.""" 1066 return '/'.join([self._get_image_url(image), filename]) 1067 1068 1069 def get_full_payload_url(self, image): 1070 """Returns a URL to a staged full payload. 1071 1072 @param image: the image that was fetched. 1073 1074 @return A fully qualified URL that can be used for downloading the 1075 payload. 1076 1077 """ 1078 return self._get_image_url(image) + '/update.gz' 1079 1080 1081 def get_test_image_url(self, image): 1082 """Returns a URL to a staged test image. 1083 1084 @param image: the image that was fetched. 1085 1086 @return A fully qualified URL that can be used for downloading the 1087 image. 1088 1089 """ 1090 return self._get_image_url(image) + '/chromiumos_test_image.bin' 1091 1092 1093 @remote_devserver_call() 1094 def list_control_files(self, build, suite_name=''): 1095 """Ask the devserver to list all control files for |build|. 1096 1097 @param build: The build (e.g. x86-mario-release/R18-1586.0.0-a1-b1514) 1098 whose control files the caller wants listed. 1099 @param suite_name: The name of the suite for which we require control 1100 files. 1101 @return None on failure, or a list of control file paths 1102 (e.g. server/site_tests/autoupdate/control) 1103 @raise DevServerException upon any return code that's not HTTP OK. 1104 """ 1105 build = self.translate(build) 1106 call = self.build_call('controlfiles', build=build, 1107 suite_name=suite_name) 1108 response = urllib2.urlopen(call) 1109 return [line.rstrip() for line in response] 1110 1111 1112 @remote_devserver_call() 1113 def get_control_file(self, build, control_path): 1114 """Ask the devserver for the contents of a control file. 1115 1116 @param build: The build (e.g. x86-mario-release/R18-1586.0.0-a1-b1514) 1117 whose control file the caller wants to fetch. 1118 @param control_path: The file to fetch 1119 (e.g. server/site_tests/autoupdate/control) 1120 @return The contents of the desired file. 1121 @raise DevServerException upon any return code that's not HTTP OK. 1122 """ 1123 build = self.translate(build) 1124 call = self.build_call('controlfiles', build=build, 1125 control_path=control_path) 1126 return urllib2.urlopen(call).read() 1127 1128 1129 @remote_devserver_call() 1130 def get_dependencies_file(self, build): 1131 """Ask the dev server for the contents of the suite dependencies file. 1132 1133 Ask the dev server at |self._dev_server| for the contents of the 1134 pre-processed suite dependencies file (at DEPENDENCIES_FILE) 1135 for |build|. 1136 1137 @param build: The build (e.g. x86-mario-release/R21-2333.0.0) 1138 whose dependencies the caller is interested in. 1139 @return The contents of the dependencies file, which should eval to 1140 a dict of dicts, as per site_utils/suite_preprocessor.py. 1141 @raise DevServerException upon any return code that's not HTTP OK. 1142 """ 1143 build = self.translate(build) 1144 call = self.build_call('controlfiles', 1145 build=build, control_path=DEPENDENCIES_FILE) 1146 return urllib2.urlopen(call).read() 1147 1148 1149 @remote_devserver_call() 1150 def get_latest_build_in_gs(self, board): 1151 """Ask the devservers for the latest offical build in Google Storage. 1152 1153 @param board: The board for who we want the latest official build. 1154 @return A string of the returned build rambi-release/R37-5868.0.0 1155 @raise DevServerException upon any return code that's not HTTP OK. 1156 """ 1157 call = self.build_call( 1158 'xbuddy_translate/remote/%s/latest-official' % board, 1159 image_dir=_get_image_storage_server()) 1160 image_name = urllib2.urlopen(call).read() 1161 return os.path.dirname(image_name) 1162 1163 1164 def translate(self, build_name): 1165 """Translate the build name if it's in LATEST format. 1166 1167 If the build name is in the format [builder]/LATEST, return the latest 1168 build in Google Storage otherwise return the build name as is. 1169 1170 @param build_name: build_name to check. 1171 1172 @return The actual build name to use. 1173 """ 1174 match = re.match(r'([\w-]+)-(\w+)/LATEST', build_name) 1175 if not match: 1176 return build_name 1177 translated_build = self.get_latest_build_in_gs(match.groups()[0]) 1178 logging.debug('Translated relative build %s to %s', build_name, 1179 translated_build) 1180 return translated_build 1181 1182 1183 @classmethod 1184 @remote_devserver_call() 1185 def get_latest_build(cls, target, milestone=''): 1186 """Ask all the devservers for the latest build for a given target. 1187 1188 @param target: The build target, typically a combination of the board 1189 and the type of build e.g. x86-mario-release. 1190 @param milestone: For latest build set to '', for builds only in a 1191 specific milestone set to a str of format Rxx 1192 (e.g. R16). Default: ''. Since we are dealing with a 1193 webserver sending an empty string, '', ensures that 1194 the variable in the URL is ignored as if it was set 1195 to None. 1196 @return A string of the returned build e.g. R20-2226.0.0. 1197 @raise DevServerException upon any return code that's not HTTP OK. 1198 """ 1199 calls = cls.build_all_calls('latestbuild', target=target, 1200 milestone=milestone) 1201 latest_builds = [] 1202 for call in calls: 1203 latest_builds.append(urllib2.urlopen(call).read()) 1204 1205 return max(latest_builds, key=version.LooseVersion) 1206 1207 1208class AndroidBuildServer(ImageServerBase): 1209 """Class for DevServer that handles RPCs related to Android builds. 1210 1211 The calls to devserver to stage artifacts, including stage and download, are 1212 made in async mode. That is, when caller makes an RPC |stage| to request 1213 devserver to stage certain artifacts, devserver handles the call and starts 1214 staging artifacts in a new thread, and return |Success| without waiting for 1215 staging being completed. When caller receives message |Success|, it polls 1216 devserver's is_staged call until all artifacts are staged. 1217 Such mechanism is designed to prevent cherrypy threads in devserver being 1218 running out, as staging artifacts might take long time, and cherrypy starts 1219 with a fixed number of threads that handle devserver rpc. 1220 """ 1221 1222 def wait_for_artifacts_staged(self, target, build_id, branch, 1223 archive_url=None, artifacts='', files=''): 1224 """Polling devserver.is_staged until all artifacts are staged. 1225 1226 @param target: Target of the android build to stage, e.g., 1227 shamu-userdebug. 1228 @param build_id: Build id of the android build to stage. 1229 @param branch: Branch of the android build to stage. 1230 @param archive_url: Google Storage URL for the build. 1231 @param artifacts: Comma separated list of artifacts to download. 1232 @param files: Comma separated list of files to download. 1233 1234 @return: True if all artifacts are staged in devserver. 1235 """ 1236 kwargs = {'target': target, 1237 'build_id': build_id, 1238 'branch': branch, 1239 'artifacts': artifacts, 1240 'files': files, 1241 'os_type': 'android'} 1242 if archive_url: 1243 kwargs['archive_url'] = archive_url 1244 return self._poll_is_staged(**kwargs) 1245 1246 1247 @remote_devserver_call() 1248 def call_and_wait(self, call_name, target, build_id, branch, archive_url, 1249 artifacts, files, error_message, 1250 expected_response=SUCCESS): 1251 """Helper method to make a urlopen call, and wait for artifacts staged. 1252 1253 @param call_name: name of devserver rpc call. 1254 @param target: Target of the android build to stage, e.g., 1255 shamu-userdebug. 1256 @param build_id: Build id of the android build to stage. 1257 @param branch: Branch of the android build to stage. 1258 @param archive_url: Google Storage URL for the CrOS build. 1259 @param artifacts: Comma separated list of artifacts to download. 1260 @param files: Comma separated list of files to download. 1261 @param expected_response: Expected response from rpc, default to 1262 |Success|. If it's set to None, do not compare 1263 the actual response. Any response is consider 1264 to be good. 1265 @param error_message: Error message to be thrown if response does not 1266 match expected_response. 1267 1268 @return: The response from rpc. 1269 @raise DevServerException upon any return code that's expected_response. 1270 1271 """ 1272 kwargs = {'target': target, 1273 'build_id': build_id, 1274 'branch': branch, 1275 'artifacts': artifacts, 1276 'files': files, 1277 'os_type': 'android'} 1278 if archive_url: 1279 kwargs['archive_url'] = archive_url 1280 return self._call_and_wait(call_name, error_message, expected_response, 1281 **kwargs) 1282 1283 1284 @remote_devserver_call() 1285 def stage_artifacts(self, target, build_id, branch, artifacts=None, 1286 files='', archive_url=None): 1287 """Tell the devserver to download and stage |artifacts| from |image|. 1288 1289 This is the main call point for staging any specific artifacts for a 1290 given build. To see the list of artifacts one can stage see: 1291 1292 ~src/platfrom/dev/artifact_info.py. 1293 1294 This is maintained along with the actual devserver code. 1295 1296 @param target: Target of the android build to stage, e.g., 1297 shamu-userdebug. 1298 @param build_id: Build id of the android build to stage. 1299 @param branch: Branch of the android build to stage. 1300 @param artifacts: A list of artifacts. 1301 @param files: A list of files to stage. 1302 @param archive_url: Optional parameter that has the archive_url to stage 1303 this artifact from. Default is specified in autotest config + 1304 image. 1305 1306 @raise DevServerException upon any return code that's not HTTP OK. 1307 """ 1308 android_build_info = {'target': target, 1309 'build_id': build_id, 1310 'branch': branch} 1311 if not artifacts and not files: 1312 raise DevServerException('Must specify something to stage.') 1313 if not all(android_build_info.values()): 1314 raise DevServerException( 1315 'To stage an Android build, must specify target, build id ' 1316 'and branch.') 1317 build = ANDROID_BUILD_NAME_PATTERN % android_build_info 1318 self._stage_artifacts(build, artifacts, files, archive_url, 1319 **android_build_info) 1320 1321 1322 def trigger_download(self, target, build_id, branch, is_brillo=False, 1323 synchronous=True): 1324 """Tell the devserver to download and stage an Android build. 1325 1326 Tells the devserver to fetch an Android build from the image storage 1327 server named by _get_image_storage_server(). 1328 1329 If |synchronous| is True, waits for the entire download to finish 1330 staging before returning. Otherwise only the artifacts necessary 1331 to start installing images onto DUT's will be staged before returning. 1332 A caller can then call finish_download to guarantee the rest of the 1333 artifacts have finished staging. 1334 1335 @param target: Target of the android build to stage, e.g., 1336 shamu-userdebug. 1337 @param build_id: Build id of the android build to stage. 1338 @param branch: Branch of the android build to stage. 1339 @param is_brillo: Set to True if it's a Brillo build. Default is False. 1340 @param synchronous: if True, waits until all components of the image are 1341 staged before returning. 1342 1343 @raise DevServerException upon any return code that's not HTTP OK. 1344 1345 """ 1346 android_build_info = {'target': target, 1347 'build_id': build_id, 1348 'branch': branch} 1349 build = ANDROID_BUILD_NAME_PATTERN % android_build_info 1350 artifacts = (_BRILLO_ARTIFACTS_TO_BE_STAGED_FOR_IMAGE if is_brillo else 1351 _ANDROID_ARTIFACTS_TO_BE_STAGED_FOR_IMAGE) 1352 self._trigger_download(build, artifacts, files='', 1353 synchronous=synchronous, **android_build_info) 1354 1355 1356 def finish_download(self, target, build_id, branch, is_brillo=False): 1357 """Tell the devserver to finish staging an Android build. 1358 1359 If trigger_download is called with synchronous=False, it will return 1360 before all artifacts have been staged. This method contacts the 1361 devserver and blocks until all staging is completed and should be 1362 called after a call to trigger_download. 1363 1364 @param target: Target of the android build to stage, e.g., 1365 shamu-userdebug. 1366 @param build_id: Build id of the android build to stage. 1367 @param branch: Branch of the android build to stage. 1368 @param is_brillo: Set to True if it's a Brillo build. Default is False. 1369 1370 @raise DevServerException upon any return code that's not HTTP OK. 1371 """ 1372 android_build_info = {'target': target, 1373 'build_id': build_id, 1374 'branch': branch} 1375 build = ANDROID_BUILD_NAME_PATTERN % android_build_info 1376 artifacts = (_BRILLO_ARTIFACTS_TO_BE_STAGED_FOR_IMAGE if is_brillo else 1377 _ANDROID_ARTIFACTS_TO_BE_STAGED_FOR_IMAGE) 1378 self._finish_download(build, artifacts, files='', **android_build_info) 1379 1380 1381 def get_staged_file_url(self, filename, target, build_id, branch): 1382 """Returns the url of a staged file for this image on the devserver. 1383 1384 @param filename: Name of the file. 1385 @param target: Target of the android build to stage, e.g., 1386 shamu-userdebug. 1387 @param build_id: Build id of the android build to stage. 1388 @param branch: Branch of the android build to stage. 1389 1390 @return: The url of a staged file for this image on the devserver. 1391 """ 1392 android_build_info = {'target': target, 1393 'build_id': build_id, 1394 'branch': branch, 1395 'os_type': 'android'} 1396 build = ANDROID_BUILD_NAME_PATTERN % android_build_info 1397 return '/'.join([self._get_image_url(build), filename]) 1398 1399 1400 def translate(self, build_name): 1401 """Translate the build name if it's in LATEST format. 1402 1403 If the build name is in the format [branch]/[target]/LATEST, return the 1404 latest build in Launch Control otherwise return the build name as is. 1405 1406 @param build_name: build_name to check. 1407 1408 @return The actual build name to use. 1409 """ 1410 branch, target, build_id = utils.parse_android_build(build_name) 1411 if build_id != 'LATEST': 1412 return build_name 1413 call = self.build_call('latestbuild', branch=branch, target=target, 1414 os_type='android') 1415 translated_build_id = urllib2.urlopen(call).read() 1416 translated_build = (ANDROID_BUILD_NAME_PATTERN % 1417 {'branch': branch, 1418 'target': target, 1419 'build_id': translated_build_id}) 1420 logging.debug('Translated relative build %s to %s', build_name, 1421 translated_build) 1422 return translated_build 1423 1424 1425def _is_load_healthy(load): 1426 """Check if devserver's load meets the minimum threshold. 1427 1428 @param load: The devserver's load stats to check. 1429 1430 @return: True if the load meets the minimum threshold. Return False 1431 otherwise. 1432 1433 """ 1434 # Threshold checks, including CPU load. 1435 if load[DevServer.CPU_LOAD] > DevServer.MAX_CPU_LOAD: 1436 logging.debug('CPU load of devserver %s is at %s%%, which is higher ' 1437 'than the threshold of %s%%', load['devserver'], 1438 load[DevServer.CPU_LOAD], DevServer.MAX_CPU_LOAD) 1439 return False 1440 if load[DevServer.NETWORK_IO] > DevServer.MAX_NETWORK_IO: 1441 logging.debug('Network IO of devserver %s is at %i Bps, which is ' 1442 'higher than the threshold of %i bytes per second.', 1443 load['devserver'], load[DevServer.NETWORK_IO], 1444 DevServer.MAX_NETWORK_IO) 1445 return False 1446 return True 1447 1448 1449def _compare_load(devserver1, devserver2): 1450 """Comparator function to compare load between two devservers. 1451 1452 @param devserver1: A dictionary of devserver load stats to be compared. 1453 @param devserver2: A dictionary of devserver load stats to be compared. 1454 1455 @return: Negative value if the load of `devserver1` is less than the load 1456 of `devserver2`. Return positive value otherwise. 1457 1458 """ 1459 return int(devserver1[DevServer.DISK_IO] - devserver2[DevServer.DISK_IO]) 1460 1461 1462def get_least_loaded_devserver(devserver_type=ImageServer): 1463 """Get the devserver with the least load. 1464 1465 Iterate through all devservers and get the one with least load. 1466 1467 TODO(crbug.com/486278): Devserver with required build already staged should 1468 take higher priority. This will need check_health call to be able to verify 1469 existence of a given build/artifact. Also, in case all devservers are 1470 overloaded, the logic here should fall back to the old behavior that randomly 1471 selects a devserver based on the hash of the image name/url. 1472 1473 @param devserver_type: Type of devserver to select from. Default is set to 1474 ImageServer. 1475 1476 @return: Name of the devserver with the least load. 1477 1478 """ 1479 # get_devserver_load call needs to be made in a new process to allow force 1480 # timeout using signal. 1481 output = multiprocessing.Queue() 1482 processes = [] 1483 for devserver in devserver_type.servers(): 1484 processes.append(multiprocessing.Process( 1485 target=DevServer.get_devserver_load_wrapper, 1486 args=(devserver, TIMEOUT_GET_DEVSERVER_LOAD, output))) 1487 1488 for p in processes: 1489 p.start() 1490 for p in processes: 1491 p.join() 1492 loads = [output.get() for p in processes] 1493 # Filter out any load failed to be retrieved or does not support load check. 1494 loads = [load for load in loads if load and DevServer.CPU_LOAD in load and 1495 DevServer.is_free_disk_ok(load)] 1496 if not loads: 1497 logging.debug('Failed to retrieve load stats from any devserver. No ' 1498 'load balancing can be applied.') 1499 return None 1500 loads = [load for load in loads if _is_load_healthy(load)] 1501 if not loads: 1502 logging.error('No devserver has the capacity to be selected.') 1503 return None 1504 loads = sorted(loads, cmp=_compare_load) 1505 return loads[0]['devserver'] 1506