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/autoupdate_unittest.py b/autoupdate_unittest.py
index 78ed62b..4c97332 100755
--- a/autoupdate_unittest.py
+++ b/autoupdate_unittest.py
@@ -145,14 +145,13 @@
     self.mox.ReplayAll()
     au_mock = self._DummyAutoupdateConstructor()
     self.assertEqual(au_mock.HandleUpdatePing(test_data), self.payload)
-    self.assertEqual(
-        au_mock.host_info['127.0.0.1']['last_known_version'], 'ForcedUpdate')
-    self.assertEqual(
-        au_mock.host_info['127.0.0.1']['last_event_type'],
-        self.test_dict['event_type'])
-    self.assertEqual(
-        au_mock.host_info['127.0.0.1']['last_event_status'],
-        self.test_dict['event_result'])
+    curr_host_info = au_mock.host_infos.GetHostInfo('127.0.0.1');
+    self.assertEqual(curr_host_info.GetAttr('last_known_version'),
+                     'ForcedUpdate')
+    self.assertEqual(curr_host_info.GetAttr('last_event_type'),
+                     self.test_dict['event_type'])
+    self.assertEqual(curr_host_info.GetAttr('last_event_status'),
+                     self.test_dict['event_result'])
     self.mox.VerifyAll()
 
   def testChangeUrlPort(self):
@@ -172,9 +171,9 @@
     au_mock = self._DummyAutoupdateConstructor()
     self.assertRaises(AssertionError, au_mock.HandleHostInfoPing, None)
 
-    # Setup fake host_info entry and ensure it comes back to us in one piece.
+    # Setup fake host_infos entry and ensure it comes back to us in one piece.
     test_ip = '1.2.3.4'
-    au_mock.host_info[test_ip] = self.test_dict
+    au_mock.host_infos.GetInitHostInfo(test_ip).attrs = self.test_dict
     self.assertEqual(
         json.loads(au_mock.HandleHostInfoPing(test_ip)), self.test_dict)
 
@@ -191,7 +190,8 @@
 
     au_mock.HandleSetUpdatePing(test_ip, test_label)
     self.assertEqual(
-        au_mock.host_info[test_ip]['forced_update_label'], test_label)
+        au_mock.host_infos.GetHostInfo(test_ip).GetAttr('forced_update_label'),
+        test_label)
 
   def testHandleUpdatePingWithSetUpdate(self):
     self.mox.StubOutWithMock(autoupdate.Autoupdate, 'GenerateLatestUpdateImage')
@@ -218,9 +218,12 @@
     au_mock = self._DummyAutoupdateConstructor()
     au_mock.HandleSetUpdatePing('127.0.0.1', test_label)
     self.assertEqual(
-        au_mock.host_info['127.0.0.1']['forced_update_label'], test_label)
+        au_mock.host_infos.GetHostInfo('127.0.0.1').
+        GetAttr('forced_update_label'),
+        test_label)
     self.assertEqual(au_mock.HandleUpdatePing(test_data), self.payload)
-    self.assertFalse('forced_update_label' in au_mock.host_info['127.0.0.1'])
+    self.assertFalse('forced_update_label' in
+        au_mock.host_infos.GetHostInfo('127.0.0.1').attrs)
 
   def testGetVersionFromDir(self):
     au = self._DummyAutoupdateConstructor()
@@ -262,5 +265,5 @@
     self.assertFalse(au._CanUpdate('0.16.892.0', '0.16.892.0'))
 
 
-if __name__ == '__main__':
-  unittest.main()
+suite = unittest.TestLoader().loadTestsFromTestCase(AutoupdateTest)
+unittest.TextTestRunner(verbosity=3).run(suite)