[autotest] Add logic to symbolicate dumps using dev server

re-run /usr/local/autotest/utils/build_externals.py before trying to use this
outside the chroot.

BUG=chromium-os:29730
TEST=point local autoserv at a local dev server, run a test that causes a crash; it should symbolicate the crash into a .txt named after the minidump file.

CQ-DEPEND=Ib9d5b0abb73567fd8450db50d3e50d27bdc169f4

Change-Id: Id0d1fd6b4f00e560dd5683d87140ed943697f185
Reviewed-on: https://gerrit.chromium.org/gerrit/21277
Commit-Ready: Chris Masone <cmasone@chromium.org>
Reviewed-by: Chris Masone <cmasone@chromium.org>
Tested-by: Chris Masone <cmasone@chromium.org>
diff --git a/client/common_lib/cros/dev_server.py b/client/common_lib/cros/dev_server.py
index b2bde59..53a3240 100644
--- a/client/common_lib/cros/dev_server.py
+++ b/client/common_lib/cros/dev_server.py
@@ -6,7 +6,7 @@
 import urllib2
 
 from autotest_lib.client.common_lib import global_config
-
+# TODO(cmasone): redo this class using requests module; http://crosbug.com/30107
 
 CONFIG = global_config.global_config
 
@@ -95,7 +95,7 @@
                 staged before returning.
         @return True if the remote call returns HTTP OK, False if it returns
                 an internal server error.
-        @throws urllib2.HTTPError upon any return code that's not 200 or 500.
+        @raise urllib2.HTTPError upon any return code that's not 200 or 500.
         """
         call = self._build_call(
             'download',
@@ -120,7 +120,7 @@
         @param image: the image to fetch and stage.
         @return True if the remote call returns HTTP OK, False if it returns
                 an internal server error.
-        @throws urllib2.HTTPError upon any return code that's not 200 or 500.
+        @raise urllib2.HTTPError upon any return code that's not 200 or 500.
         """
         call = self._build_call(
             'wait_for_status',
@@ -140,7 +140,7 @@
                       whose control files the caller wants listed.
         @return None on failure, or a list of control file paths
                 (e.g. server/site_tests/autoupdate/control)
-        @throws urllib2.HTTPError upon any return code that's not 200 or 500.
+        @raise urllib2.HTTPError upon any return code that's not 200 or 500.
         """
         call = self._build_call('controlfiles', build=build)
         response = urllib2.urlopen(call)
@@ -151,7 +151,7 @@
     def get_control_file(self, build, control_path):
         """Ask the dev server for the contents of a control file.
 
-        Ask the dev server at |self._dev_server|for the contents of the
+        Ask the dev server at |self._dev_server| for the contents of the
         control file at |control_path| for |build|.
 
         @param build: The build (e.g. x86-mario-release/R18-1586.0.0-a1-b1514)
@@ -159,13 +159,40 @@
         @param control_path: The file to list
                              (e.g. server/site_tests/autoupdate/control)
         @return The contents of the desired file, or None
-        @throws urllib2.HTTPError upon any return code that's not 200 or 500.
+        @raise urllib2.HTTPError upon any return code that's not 200 or 500.
         """
         call = self._build_call('controlfiles',
                                 build=build, control_path=control_path)
         return urllib2.urlopen(call).read()
 
 
+    def symbolicate_dump(self, minidump_path):
+        """Ask the dev server to symbolicate the dump at minidump_path.
+
+        Ask the dev server at |self._dev_server| to symbolicate the dump
+        at minidump_path, using previously staged debug symbols.
+
+        @param minidump_path: the on-disk path of the minidump.
+        @return The contents of the stack trace
+        @raise urllib2.HTTPError upon any return code that's not 200.
+        """
+        call = self._build_call('symbolicate_dump')
+        try:
+            import requests
+        except ImportError:
+            logging.warning("Can't 'import requests' to connect to dev server.")
+            return ''
+        request = requests.post(call,
+                                files={'minidump': open(minidump_path, 'rb')})
+        if request.status_code == requests.codes.OK:
+            return request.text
+        raise urllib2.HTTPError(call,
+                                request.status_code,
+                                '%d' % request.status_code,
+                                request.headers,
+                                None)
+
+
     @remote_devserver_call(None)
     def get_latest_build(self, target, milestone=''):
         """Ask the dev server for the latest build for a given target.
@@ -183,7 +210,7 @@
                            to None.
         @return A string of the returned build e.g. R18-1586.0.0-a1-b1514
                 or None.
-        @throws urllib2.HTTPError upon any return code that's not 200 or 500.
+        @raise urllib2.HTTPError upon any return code that's not 200 or 500.
         """
         call = self._build_call('latestbuild', target=target,
                                 milestone=milestone)
diff --git a/server/site_crashcollect.py b/server/site_crashcollect.py
index 19a9400..a093fc8 100644
--- a/server/site_crashcollect.py
+++ b/server/site_crashcollect.py
@@ -4,7 +4,9 @@
 
 import logging
 import os
+import urllib2
 from autotest_lib.client.common_lib import utils as client_utils
+from autotest_lib.client.common_lib.cros import dev_server
 from autotest_lib.client.cros import constants
 from autotest_lib.server import utils
 
@@ -26,6 +28,24 @@
     return rc
 
 
+def symbolicate_minidump_with_devserver(minidump_path):
+    """
+    Generates a stack trace for the specified minidump by consulting devserver.
+
+    This function assumes the debug symbols have been staged on the devserver.
+    @return HTTP response code of the attempt.
+    """
+    devserver = dev_server.DevServer.create()
+    try:
+        trace_text = devserver.symbolicate_dump(minidump_path)
+        with open(minidump_path + '.txt', 'w') as trace_file:
+            trace_file.write(trace_text)
+    except urllib2.HTTPError as e:
+        logging.warn('HTTPError while symbolicating minidump: \n%r', e)
+        return e.code
+    return 200
+
+
 def find_and_generate_minidump_stacktraces(host_resultdir):
     """
     Finds all minidump files and generates a stack trace for each.
@@ -42,11 +62,18 @@
             minidump = os.path.join(dir, file)
             rc = generate_minidump_stacktrace(minidump)
             if rc == 0:
-                logging.info('Generated stack trace for dump %s' %
-                             minidump)
-            else:
-                logging.warn('Failed to generate stack trace for ' \
-                             'dump %s (rc=%d)' % (minidump, rc))
+                logging.info('Generated stack trace for dump %s', minidump)
+                return
+
+            http_rc = symbolicate_minidump_with_devserver(minidump)
+            if http_rc == 200:
+                logging.info('Generated stack trace for dump %s', minidump)
+                return
+
+            logging.warn('Failed to generate stack trace locally for ' \
+                         'dump %s (rc=%d)', (minidump, rc))
+            logging.warn('Failed to generate stack trace on devserver for ' \
+                         'dump %s (rc=%d)', (minidump, http_rc))
 
 
 def fetch_orphaned_crashdumps(host, host_resultdir):
diff --git a/utils/external_packages.py b/utils/external_packages.py
index 0b8cb56..18e50fe 100644
--- a/utils/external_packages.py
+++ b/utils/external_packages.py
@@ -654,6 +654,18 @@
         return True
 
 
+class RequestsPackage(ExternalPackage):
+    version = '0.11.2'
+    local_filename = 'requests-%s.tar.gz' % version
+    urls = ('http://pypi.python.org/packages/source/r/requests/' +
+            local_filename,)
+    hex_sum = '00a49e8bd6dd8955acf6f6269d1b85f50c70b712'
+
+    _build_and_install = ExternalPackage._build_and_install_from_package
+    _build_and_install_current_dir = (
+                        ExternalPackage._build_and_install_current_dir_setup_py)
+
+
 class SimplejsonPackage(ExternalPackage):
     version = '2.0.9'
     local_filename = 'simplejson-%s.tar.gz' % version