dev_server.py revision 2e7b2ea7850bdd3ca92a5ef47f11d91a08c2b3db
14f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2e83b0cadc67882c1ba7f430d16dab80c9b3a0228Dan Handley# Use of this source code is governed by a BSD-style license that can be
34f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta# found in the LICENSE file.
44f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
54f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Guptafrom distutils import version
64f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Guptaimport json
74f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Guptaimport logging
84f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Guptaimport os
94f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Guptaimport urllib2
104f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Guptaimport HTMLParser
114f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Guptaimport cStringIO
124f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Guptaimport re
134f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Guptaimport sys
144f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
154f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Guptafrom autotest_lib.client.common_lib import global_config
164f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Guptafrom autotest_lib.client.common_lib import utils
174f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Guptafrom autotest_lib.client.common_lib.cros import retry
184f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Guptafrom autotest_lib.client.bin import utils as site_utils
194f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Guptafrom autotest_lib.site_utils.graphite import stats
204f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta# TODO(cmasone): redo this class using requests module; http://crosbug.com/30107
214f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
224f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
234f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin GuptaCONFIG = global_config.global_config
244f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta# This file is generated at build time and specifies, per suite and per test,
254f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta# the DEPENDENCIES list specified in each control file.  It's a dict of dicts:
264f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta# {'bvt':   {'/path/to/autotest/control/site_tests/test1/control': ['dep1']}
274f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta#  'suite': {'/path/to/autotest/control/site_tests/test2/control': ['dep2']}
284f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta#  'power': {'/path/to/autotest/control/site_tests/test1/control': ['dep1'],
294f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta#            '/path/to/autotest/control/site_tests/test3/control': ['dep3']}
304f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta# }
314f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin GuptaDEPENDENCIES_FILE = 'test_suites/dependency_info'
324f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta# Number of seconds for caller to poll devserver's is_staged call to check if
334f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta# artifacts are staged.
344f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta_ARTIFACT_STAGE_POLLING_INTERVAL = 5
354f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta# Artifacts that should be staged when client calls devserver RPC to stage an
364f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta# image.
374f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta_ARTIFACTS_TO_BE_STAGED_FOR_IMAGE = 'full_payload,test_suites,stateful'
384f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta# Artifacts that should be staged when client calls devserver RPC to stage an
394f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta# image with autotest artifact.
408d69a03f6a7db3c437b7cfdd15402627277d8cb4Sandrine Bailleux_ARTIFACTS_TO_BE_STAGED_FOR_IMAGE_WITH_AUTOTEST = ('full_payload,test_suites,'
414f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta                                                   'autotest,stateful')
424f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
434f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
444f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Guptaclass MarkupStripper(HTMLParser.HTMLParser):
454f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    """HTML parser that strips HTML tags, coded characters like &
464f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
474f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    Works by, basically, not doing anything for any tags, and only recording
484f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    the content of text nodes in an internal data structure.
494f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    """
504f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    def __init__(self):
514f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        self.reset()
524f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        self.fed = []
534f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
544f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
554f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    def handle_data(self, d):
564f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        """Consume content of text nodes, store it away."""
574f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        self.fed.append(d)
584f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
594f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
604f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    def get_data(self):
614f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        """Concatenate and return all stored data."""
624f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        return ''.join(self.fed)
634f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
644f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
654f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Guptadef _get_image_storage_server():
664f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    return CONFIG.get_config_value('CROS', 'image_storage_server', type=str)
674f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
684f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
694f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Guptadef _get_dev_server_list():
704f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    return CONFIG.get_config_value('CROS', 'dev_server', type=list, default=[])
714f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
724f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
734f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Guptadef _get_crash_server_list():
744f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    return CONFIG.get_config_value('CROS', 'crash_server', type=list,
754f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        default=[])
764f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
774f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
784f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Guptadef remote_devserver_call(timeout_min=30):
794f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    """A decorator to use with remote devserver calls.
804f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
814f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    This decorator converts urllib2.HTTPErrors into DevServerExceptions with
824f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    any embedded error info converted into plain text.
834f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    The method retries on urllib2.URLError to avoid devserver flakiness.
844f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    """
854f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    #pylint: disable=C0111
864f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    def inner_decorator(method):
874f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
884f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        @retry.retry(urllib2.URLError, timeout_min=timeout_min)
894f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        def wrapper(*args, **kwargs):
904f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta            """This wrapper actually catches the HTTPError."""
914f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta            try:
924f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta                return method(*args, **kwargs)
934f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta            except urllib2.HTTPError as e:
944f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta                error_markup = e.read()
954f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta                strip = MarkupStripper()
964f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta                try:
974f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta                    strip.feed(error_markup.decode('utf_32'))
984f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta                except UnicodeDecodeError:
994f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta                    strip.feed(error_markup)
1004f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta                raise DevServerException(strip.get_data())
1014f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
1024f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        return wrapper
1034f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
1044f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    return inner_decorator
1054f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
1064f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
1074f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Guptaclass DevServerException(Exception):
1084f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    """Raised when the dev server returns a non-200 HTTP response."""
1094f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    pass
1104f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
1114f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
1124f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Guptaclass DevServer(object):
1134f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    """Base class for all DevServer-like server stubs.
1144f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
1154f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    This is the base class for interacting with all Dev Server-like servers.
1164f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    A caller should instantiate a sub-class of DevServer with:
1174f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
1184f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    host = SubClassServer.resolve(build)
1194f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    server = SubClassServer(host)
1204f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    """
1214f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    _MIN_FREE_DISK_SPACE_GB = 20
1224f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
1234f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    def __init__(self, devserver):
1244f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        self._devserver = devserver
1254f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
1264f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
1274f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    def url(self):
1284f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        """Returns the url for this devserver."""
1294f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        return self._devserver
1304f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
1314f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
1324f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    @staticmethod
1334f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    def devserver_healthy(devserver, timeout_min=0.1):
1344f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        """Returns True if the |devserver| is healthy to stage build.
1354f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
1364f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        @param devserver: url of the devserver.
1374f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        @param timeout_min: How long to wait in minutes before deciding the
1384f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta                            the devserver is not up (float).
1394f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        """
1404f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        server_name =  re.sub(r':\d+$', '', devserver.lstrip('http://'))
1414f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        # statsd treats |.| as path separator.
1424f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        server_name =  server_name.replace('.', '_')
1434f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        call = DevServer._build_call(devserver, 'check_health')
1444f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
1454f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        @remote_devserver_call(timeout_min=timeout_min)
1464f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        def make_call():
1474f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta            """Inner method that makes the call."""
1484f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta            return utils.urlopen_socket_timeout(call,
1494f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta                timeout=timeout_min*60).read()
1504f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
1514f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        try:
1524f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta            result_dict = json.load(cStringIO.StringIO(make_call()))
1534f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta            free_disk = result_dict['free_disk']
1544f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta            stats.Gauge(server_name).send('free_disk', free_disk)
1554f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
1564f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta            skip_devserver_health_check = CONFIG.get_config_value('CROS',
1574f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta                                              'skip_devserver_health_check',
1584f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta                                              type=bool)
1594f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta            if skip_devserver_health_check:
1604f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta                logging.debug('devserver health check is skipped.')
1614f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta            elif (free_disk < DevServer._MIN_FREE_DISK_SPACE_GB):
1624f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta                logging.error('Devserver check_health failed. Free disk space '
1634f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta                              'is low. Only %dGB is available.', free_disk)
1644f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta                stats.Counter(server_name +'.devserver_not_healthy').increment()
1654f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta                return False
1664f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
1674f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta            # This counter indicates the load of a devserver. By comparing the
1684f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta            # value of this counter for all devservers, we can evaluate the
1694f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta            # load balancing across all devservers.
1704f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta            stats.Counter(server_name + '.devserver_healthy').increment()
1714f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta            return True
1724f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        except Exception as e:
1734f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta            logging.error('Devserver call failed: "%s", timeout: %s seconds,'
1744f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta                          ' Error: %s', call, timeout_min*60, str(e))
1754f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta            stats.Counter(server_name + '.devserver_not_healthy').increment()
1764f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta            return False
1774f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
1784f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
1794f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    @staticmethod
1804f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    def _build_call(host, method, **kwargs):
1814f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        """Build a URL to |host| that calls |method|, passing |kwargs|.
1824f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
1834f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        Builds a URL that calls |method| on the dev server defined by |host|,
1844f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        passing a set of key/value pairs built from the dict |kwargs|.
1854f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
1864f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        @param host: a string that is the host basename e.g. http://server:90.
1874f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        @param method: the dev server method to call.
1884f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        @param kwargs: a dict mapping arg names to arg values.
1894f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        @return the URL string.
1904f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        """
1914f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        argstr = '&'.join(map(lambda x: "%s=%s" % x, kwargs.iteritems()))
1924f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        return "%(host)s/%(method)s?%(argstr)s" % dict(
1934f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta                host=host, method=method, argstr=argstr)
1944f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
1954f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
1964f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    def build_call(self, method, **kwargs):
1974f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        """Builds a devserver RPC string that can be invoked using urllib.open.
1984f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
1994f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        @param method: remote devserver method to call.
2004f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        """
2014f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta        return self._build_call(self._devserver, method, **kwargs)
2024f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
2034f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta
2044f6ad66ae9fcc8bcb3b0fcee10b7ab1ffcaf1a5Achin Gupta    @classmethod
205    def build_all_calls(cls, method, **kwargs):
206        """Builds a list of URLs that makes RPC calls on all devservers.
207
208        Build a URL that calls |method| on the dev server, passing a set
209        of key/value pairs built from the dict |kwargs|.
210
211        @param method: the dev server method to call.
212        @param kwargs: a dict mapping arg names to arg values
213        @return the URL string
214        """
215        calls = []
216        # Note we use cls.servers as servers is class specific.
217        for server in cls.servers():
218            if cls.devserver_healthy(server):
219                calls.append(cls._build_call(server, method, **kwargs))
220
221        return calls
222
223
224    @staticmethod
225    def servers():
226        """Returns a list of servers that can serve as this type of server."""
227        raise NotImplementedError()
228
229
230    @classmethod
231    def resolve(cls, build):
232        """"Resolves a build to a devserver instance.
233
234        @param build: The build (e.g. x86-mario-release/R18-1586.0.0-a1-b1514).
235        """
236        devservers = cls.servers()
237        while devservers:
238            hash_index = hash(build) % len(devservers)
239            devserver = devservers.pop(hash_index)
240            if cls.devserver_healthy(devserver):
241                return cls(devserver)
242        else:
243            logging.error('All devservers are currently down!!!')
244            raise DevServerException('All devservers are currently down!!!')
245
246
247class CrashServer(DevServer):
248    """Class of DevServer that symbolicates crash dumps."""
249    @staticmethod
250    def servers():
251        return _get_crash_server_list()
252
253
254    @remote_devserver_call()
255    def symbolicate_dump(self, minidump_path, build):
256        """Ask the devserver to symbolicate the dump at minidump_path.
257
258        Stage the debug symbols for |build| and, if that works, ask the
259        devserver to symbolicate the dump at |minidump_path|.
260
261        @param minidump_path: the on-disk path of the minidump.
262        @param build: The build (e.g. x86-mario-release/R18-1586.0.0-a1-b1514)
263                      whose debug symbols are needed for symbolication.
264        @return The contents of the stack trace
265        @raise DevServerException upon any return code that's not HTTP OK.
266        """
267        try:
268            import requests
269        except ImportError:
270            logging.warning("Can't 'import requests' to connect to dev server.")
271            return ''
272
273        stats.Counter('CrashServer.symbolicate_dump').increment()
274        timer = stats.Timer('CrashServer.symbolicate_dump')
275        timer.start()
276        # Symbolicate minidump.
277        call = self.build_call('symbolicate_dump',
278                               archive_url=_get_image_storage_server() + build)
279        request = requests.post(
280                call, files={'minidump': open(minidump_path, 'rb')})
281        if request.status_code == requests.codes.OK:
282            timer.stop()
283            return request.text
284
285        error_fd = cStringIO.StringIO(request.text)
286        raise urllib2.HTTPError(
287                call, request.status_code, request.text, request.headers,
288                error_fd)
289
290
291class ImageServer(DevServer):
292    """Class for DevServer that handles image-related RPCs.
293
294    The calls to devserver to stage artifacts, including stage and download, are
295    made in async mode. That is, when caller makes an RPC |stage| to request
296    devserver to stage certain artifacts, devserver handles the call and starts
297    staging artifacts in a new thread, and return |Success| without waiting for
298    staging being completed. When caller receives message |Success|, it polls
299    devserver's is_staged call until all artifacts are staged.
300    Such mechanism is designed to prevent cherrypy threads in devserver being
301    running out, as staging artifacts might take long time, and cherrypy starts
302    with a fixed number of threads that handle devserver rpc.
303    """
304    @staticmethod
305    def servers():
306        return _get_dev_server_list()
307
308
309    @classmethod
310    def devserver_url_for_servo(cls, board):
311        """Returns the devserver url for use with servo recovery.
312
313        @param board: The board (e.g. 'x86-mario').
314        """
315        # Ideally, for load balancing we'd select the server based
316        # on the board.  For now, to simplify manual steps on the
317        # server side, we ignore the board type and hard-code the
318        # server as first in the list.
319        #
320        # TODO(jrbarnette) Once we have automated selection of the
321        # build for recovery, we should revisit this.
322        url_pattern = CONFIG.get_config_value('CROS',
323                                              'servo_url_pattern',
324                                              type=str)
325        return url_pattern % (cls.servers()[0], board)
326
327
328    class ArtifactUrls(object):
329        """A container for URLs of staged artifacts.
330
331        Attributes:
332            full_payload: URL for downloading a staged full release update
333            mton_payload: URL for downloading a staged M-to-N release update
334            nton_payload: URL for downloading a staged N-to-N release update
335
336        """
337        def __init__(self, full_payload=None, mton_payload=None,
338                     nton_payload=None):
339            self.full_payload = full_payload
340            self.mton_payload = mton_payload
341            self.nton_payload = nton_payload
342
343
344    def wait_for_artifacts_staged(self, archive_url, artifacts=''):
345        """Polling devserver.is_staged until all artifacts are staged.
346
347        @param archive_url: Google Storage URL for the build.
348        @param artifacts: Comma separated list of artifacts to download.
349        @return: True if all artifacts are staged in devserver.
350        """
351        call = self.build_call('is_staged',
352                               archive_url=archive_url,
353                               artifacts=artifacts)
354
355        def all_staged():
356            """Call devserver.is_staged rpc to check if all files are staged.
357
358            @return: True if all artifacts are staged in devserver. False
359                     otherwise.
360
361            """
362            try:
363                return urllib2.urlopen(call).read() == 'True'
364            except IOError:
365                return False
366
367        site_utils.poll_for_condition(all_staged,
368                                exception=site_utils.TimeoutError(),
369                                timeout=sys.maxint,
370                                sleep_interval=_ARTIFACT_STAGE_POLLING_INTERVAL)
371        return True
372
373
374    def call_and_wait(self, call_name, archive_url, artifacts,
375                      error_message, expected_response='Success'):
376        """Helper method to make a urlopen call, and wait for artifacts staged.
377
378        @param call_name: name of devserver rpc call.
379        @param archive_url: Google Storage URL for the build..
380        @param artifacts: Comma separated list of artifacts to download.
381        @param expected_response: Expected response from rpc, default to
382                                  |Success|. If it's set to None, do not compare
383                                  the actual response. Any response is consider
384                                  to be good.
385        @param error_message: Error message to be thrown if response does not
386                              match expected_response.
387
388        @return: The response from rpc.
389        @raise DevServerException upon any return code that's expected_response.
390
391        """
392        call = self.build_call(call_name,
393                               archive_url=archive_url,
394                               artifacts=artifacts,
395                               async=True)
396        response = urllib2.urlopen(call).read()
397        if expected_response and not response == expected_response:
398              raise DevServerException(error_message)
399
400        self.wait_for_artifacts_staged(archive_url, artifacts)
401        return response
402
403
404    @remote_devserver_call()
405    def stage_artifacts(self, image, artifacts):
406        """Tell the devserver to download and stage |artifacts| from |image|.
407
408        This is the main call point for staging any specific artifacts for a
409        given build. To see the list of artifacts one can stage see:
410
411        ~src/platfrom/dev/artifact_info.py.
412
413        This is maintained along with the actual devserver code.
414
415        @param image: the image to fetch and stage.
416        @param artifacts: A list of artifacts.
417
418        @raise DevServerException upon any return code that's not HTTP OK.
419        """
420        archive_url = _get_image_storage_server() + image
421        artifacts = ','.join(artifacts)
422        error_message = ("staging artifacts %s for %s failed;"
423                         "HTTP OK not accompanied by 'Success'." %
424                         (' '.join(artifacts), image))
425        self.call_and_wait(call_name='stage',
426                           archive_url=archive_url,
427                           artifacts=artifacts,
428                           error_message=error_message)
429
430
431    @remote_devserver_call()
432    def trigger_download(self, image, synchronous=True):
433        """Tell the devserver to download and stage |image|.
434
435        Tells the devserver to fetch |image| from the image storage server
436        named by _get_image_storage_server().
437
438        If |synchronous| is True, waits for the entire download to finish
439        staging before returning. Otherwise only the artifacts necessary
440        to start installing images onto DUT's will be staged before returning.
441        A caller can then call finish_download to guarantee the rest of the
442        artifacts have finished staging.
443
444        @param image: the image to fetch and stage.
445        @param synchronous: if True, waits until all components of the image are
446               staged before returning.
447
448        @raise DevServerException upon any return code that's not HTTP OK.
449
450        """
451        archive_url=_get_image_storage_server() + image
452        artifacts = _ARTIFACTS_TO_BE_STAGED_FOR_IMAGE
453        error_message = ("trigger_download for %s failed;"
454                         "HTTP OK not accompanied by 'Success'." % image)
455        response = self.call_and_wait(call_name='stage',
456                                      archive_url=archive_url,
457                                      artifacts=artifacts,
458                                      error_message=error_message)
459        was_successful = response == 'Success'
460        if was_successful and synchronous:
461            self.finish_download(image)
462
463
464    @remote_devserver_call()
465    def setup_telemetry(self, build):
466        """Tell the devserver to setup telemetry for this build.
467
468        The devserver will stage autotest and then extract the required files
469        for telemetry.
470
471        @param build: the build to setup telemetry for.
472
473        @returns path on the devserver that telemetry is installed to.
474        """
475        archive_url=_get_image_storage_server() + build
476        artifacts = _ARTIFACTS_TO_BE_STAGED_FOR_TELEMETRY
477        response = self.call_and_wait(call_name='setup_telemetry',
478                                      archive_url=archive_url,
479                                      artifacts=artifacts,
480                                      expected_response=None)
481        return response
482
483
484    @remote_devserver_call()
485    def finish_download(self, image):
486        """Tell the devserver to finish staging |image|.
487
488        If trigger_download is called with synchronous=False, it will return
489        before all artifacts have been staged. This method contacts the
490        devserver and blocks until all staging is completed and should be
491        called after a call to trigger_download.
492
493        @param image: the image to fetch and stage.
494        @raise DevServerException upon any return code that's not HTTP OK.
495        """
496        archive_url=_get_image_storage_server() + image
497        artifacts = _ARTIFACTS_TO_BE_STAGED_FOR_IMAGE_WITH_AUTOTEST
498        error_message = ("finish_download for %s failed;"
499                         "HTTP OK not accompanied by 'Success'." % image)
500        self.call_and_wait(call_name='stage',
501                           archive_url=archive_url,
502                           artifacts=artifacts,
503                           error_message=error_message)
504
505
506    def get_update_url(self, image):
507        """Returns the url that should be passed to the updater.
508
509        @param image: the image that was fetched.
510        """
511        url_pattern = CONFIG.get_config_value('CROS', 'image_url_pattern',
512                                              type=str)
513        return (url_pattern % (self.url(), image))
514
515
516    def _get_image_url(self, image):
517        """Returns the url of the directory for this image on the devserver.
518
519        @param image: the image that was fetched.
520        """
521        url_pattern = CONFIG.get_config_value('CROS', 'image_url_pattern',
522                                              type=str)
523        return (url_pattern % (self.url(), image)).replace(
524                  'update', 'static')
525
526
527    def get_delta_payload_url(self, payload_type, image):
528        """Returns a URL to a staged delta payload.
529
530        @param payload_type: either 'mton' or 'nton'
531        @param image: the image that was fetched.
532
533        @return A fully qualified URL that can be used for downloading the
534                payload.
535
536        @raise DevServerException if payload type argument is invalid.
537
538        """
539        if payload_type not in ('mton', 'nton'):
540            raise DevServerException('invalid delta payload type: %s' %
541                                     payload_type)
542        version = os.path.basename(image)
543        base_url = self._get_image_url(image)
544        return base_url + '/au/%s_%s/update.gz' % (version, payload_type)
545
546
547    def get_full_payload_url(self, image):
548        """Returns a URL to a staged full payload.
549
550        @param image: the image that was fetched.
551
552        @return A fully qualified URL that can be used for downloading the
553                payload.
554
555        """
556        return self._get_image_url(image) + '/update.gz'
557
558
559    def get_test_image_url(self, image):
560        """Returns a URL to a staged test image.
561
562        @param image: the image that was fetched.
563
564        @return A fully qualified URL that can be used for downloading the
565                image.
566
567        """
568        return self._get_image_url(image) + '/chromiumos_test_image.bin'
569
570
571    @remote_devserver_call()
572    def list_control_files(self, build, suite_name=''):
573        """Ask the devserver to list all control files for |build|.
574
575        @param build: The build (e.g. x86-mario-release/R18-1586.0.0-a1-b1514)
576                      whose control files the caller wants listed.
577        @param suite_name: The name of the suite for which we require control
578                           files.
579        @return None on failure, or a list of control file paths
580                (e.g. server/site_tests/autoupdate/control)
581        @raise DevServerException upon any return code that's not HTTP OK.
582        """
583        call = self.build_call('controlfiles', build=build,
584                               suite_name=suite_name)
585        response = urllib2.urlopen(call)
586        return [line.rstrip() for line in response]
587
588
589    @remote_devserver_call()
590    def get_control_file(self, build, control_path):
591        """Ask the devserver for the contents of a control file.
592
593        @param build: The build (e.g. x86-mario-release/R18-1586.0.0-a1-b1514)
594                      whose control file the caller wants to fetch.
595        @param control_path: The file to fetch
596                             (e.g. server/site_tests/autoupdate/control)
597        @return The contents of the desired file.
598        @raise DevServerException upon any return code that's not HTTP OK.
599        """
600        call = self.build_call('controlfiles', build=build,
601                               control_path=control_path)
602        return urllib2.urlopen(call).read()
603
604
605    @remote_devserver_call()
606    def get_dependencies_file(self, build):
607        """Ask the dev server for the contents of the suite dependencies file.
608
609        Ask the dev server at |self._dev_server| for the contents of the
610        pre-processed suite dependencies file (at DEPENDENCIES_FILE)
611        for |build|.
612
613        @param build: The build (e.g. x86-mario-release/R21-2333.0.0)
614                      whose dependencies the caller is interested in.
615        @return The contents of the dependencies file, which should eval to
616                a dict of dicts, as per site_utils/suite_preprocessor.py.
617        @raise DevServerException upon any return code that's not HTTP OK.
618        """
619        call = self.build_call('controlfiles',
620                               build=build, control_path=DEPENDENCIES_FILE)
621        return urllib2.urlopen(call).read()
622
623
624    @classmethod
625    @remote_devserver_call()
626    def get_latest_build(cls, target, milestone=''):
627        """Ask all the devservers for the latest build for a given target.
628
629        @param target: The build target, typically a combination of the board
630                       and the type of build e.g. x86-mario-release.
631        @param milestone:  For latest build set to '', for builds only in a
632                           specific milestone set to a str of format Rxx
633                           (e.g. R16). Default: ''. Since we are dealing with a
634                           webserver sending an empty string, '', ensures that
635                           the variable in the URL is ignored as if it was set
636                           to None.
637        @return A string of the returned build e.g. R20-2226.0.0.
638        @raise DevServerException upon any return code that's not HTTP OK.
639        """
640        calls = cls.build_all_calls('latestbuild', target=target,
641                                    milestone=milestone)
642        latest_builds = []
643        for call in calls:
644            latest_builds.append(urllib2.urlopen(call).read())
645
646        return max(latest_builds, key=version.LooseVersion)
647