Make devserver record and present a detailed log of client events.

The devserver now stores a complete list of timestamped attributes, as
they are extracted from client messages. A log is indexed by client IP
addresses.  Each of the events in a client's log is a set of attributes
and values, including the type of the event, a status code, the reported
board and OS version, and a timestamp.  A dedicated HTTP API allows to
read client logs in JSON encoding.  Previous client tracking
functionality is preserved for backward compatibility.

TEST=Unittests; complete update cycle of a chromebook client over
a network connection.
BUG=chromium-os:25028

Change-Id: I579d2daf5bf925bd1a75e1a27585f62a59442967
Reviewed-on: https://gerrit.chromium.org/gerrit/14090
Commit-Ready: Gilad Arnold <garnold@chromium.org>
Reviewed-by: Gilad Arnold <garnold@chromium.org>
Tested-by: Gilad Arnold <garnold@chromium.org>
diff --git a/devserver.py b/devserver.py
index 4e304ec..4185407 100755
--- a/devserver.py
+++ b/devserver.py
@@ -145,6 +145,13 @@
     return updater.HandleHostInfoPing(ip)
 
   @cherrypy.expose
+  def hostlog(self, ip):
+    """Returns a JSON object containing a log of events pertaining to a
+    particular host, or all hosts. Log events contain a timestamp and any
+    subset of the attributes listed for the hostinfo method."""
+    return updater.HandleHostLogPing(ip)
+
+  @cherrypy.expose
   def setnextupdate(self, ip):
     """Allows the response to the next update ping from a host to be set.
 
@@ -243,14 +250,14 @@
   @cherrypy.expose
   def update(self, *args):
     label = '/'.join(args)
-    body_length = int(cherrypy.request.headers['Content-Length'])
+    body_length = int(cherrypy.request.headers.get('Content-Length', 0))
     data = cherrypy.request.rfile.read(body_length)
     return updater.HandleUpdatePing(data, label)
 
 
 if __name__ == '__main__':
   usage = 'usage: %prog [options]'
-  parser = optparse.OptionParser(usage)
+  parser = optparse.OptionParser(usage=usage)
   parser.add_option('--archive_dir', dest='archive_dir',
                     help='serve archived builds only.')
   parser.add_option('--board', dest='board',
@@ -280,7 +287,7 @@
   parser.add_option('--payload', dest='payload',
                     help='Use update payload from specified directory.')
   parser.add_option('--port', default=8080,
-                    help='Port for the dev server to use.')
+                    help='Port for the dev server to use (default: 8080).')
   parser.add_option('--private_key', default=None,
                     help='Path to the private key in pem format.')
   parser.add_option('--production', action='store_true', default=False,
@@ -295,7 +302,8 @@
   parser.add_option('--validate_factory_config', action="store_true",
                     dest='validate_factory_config',
                     help='Validate factory config file, then exit.')
-  parser.set_usage(parser.format_help())
+  parser.add_option('-l', '--logging', action="store_true", default=False,
+                    help='Enable logging and reporting of update processes.')
   (options, _) = parser.parse_args()
 
   devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))