• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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