jsonpoll: add support to poll for radio stats

* Add polling the radio for stats as well as the modem.
* Log responses received so turbogrinder can alert on them.

Change-Id: I8bd6fb96fd786fd647634aa2145d75340577d155
diff --git a/jsonpoll/jsonpoll.py b/jsonpoll/jsonpoll.py
index 7e0c7ea..f69fac3 100755
--- a/jsonpoll/jsonpoll.py
+++ b/jsonpoll/jsonpoll.py
@@ -18,10 +18,12 @@
 __author__ = 'cgibson@google.com (Chris Gibson)'
 
 import errno
+import json
 import os
 import socket
 import sys
 import tempfile
+import textwrap
 import time
 import urllib2
 import options
@@ -53,12 +55,28 @@
     # The time to wait between requests in seconds.
     self.poll_interval_secs = interval
 
-    # TODO(cgibson): Support more request types once Glaukus Manager's JSON spec
-    # is more stable.
     self.api_modem_output_file = os.path.join(self.OUTPUT_DIR, 'modem.json')
-    self.paths_to_statfiles = {'api/modem': self.api_modem_output_file}
+    self.api_radio_output_file = os.path.join(self.OUTPUT_DIR, 'radio.json')
+    self.paths_to_statfiles = {'api/modem': self.api_modem_output_file,
+                               'api/radio': self.api_radio_output_file}
     self.last_response = None
 
+  def WriteToStderr(self, msg, is_json=False):
+    """Write a message to stderr."""
+    if is_json:
+      # Make the json easier to parse from the logs.
+      json_data = json.loads(msg)
+      json_str = json.dumps(json_data, sort_keys=True, indent=2,
+                            separators=(',', ': '))
+      # Logging pretty-printed json is like logging one huge line. Logos is
+      # configured to limit lines to 768 characters. Split the logged output at
+      # half of that to make sure logos doesn't clip our output.
+      sys.stderr.write('\n'.join(textwrap.wrap(json_str, width=384)))
+      sys.stderr.flush()
+    else:
+      sys.stderr.write(msg)
+      sys.stderr.flush()
+
   def RequestStats(self):
     """Sends a request via HTTP GET to the specified web server."""
     for path, output_file in self.paths_to_statfiles.iteritems():
@@ -67,45 +85,46 @@
       try:
         response = self.GetHttpResponse(url)
         if not response:
-          return False
+          self.WriteToStderr('Failed to get response from glaukus: %s', url)
+          continue
         elif self.last_response == response:
-          print 'Skipping file write as content has not changed.'
-          return True
+          self.WriteToStderr('Skipping file write as content has not changed.')
+          continue
         self.last_response = response
         with tempfile.NamedTemporaryFile(delete=False) as fd:
           if not self.CreateDirs(os.path.dirname(output_file)):
-            print ('Failed to create output directory: %s' %
-                   os.path.dirname(output_file))
-            return False
+            self.WriteToStderr('Failed to create output directory: %s' %
+                               os.path.dirname(output_file))
+            continue
           tmpfile = fd.name
           fd.write(response)
           fd.flush()
           os.fsync(fd.fileno())
-          print 'Wrote %d bytes to %s' % (len(response), tmpfile)
           try:
             os.rename(tmpfile, output_file)
           except OSError as ex:
-            print 'Failed to move %s to %s: %s' % (tmpfile, output_file, ex)
-            return False
-          print 'Moved %s to %s' % (tmpfile, output_file)
-        return True
+            self.WriteToStderr('Failed to move %s to %s: %s' % (
+                tmpfile, output_file, ex))
+            continue
       finally:
         if os.path.exists(tmpfile):
           os.unlink(tmpfile)
 
   def GetHttpResponse(self, url):
     """Creates a request and retrieves the response from a web server."""
-    print 'Connecting to %s' % url
     try:
       handle = urllib2.urlopen(url, timeout=self._SOCKET_TIMEOUT_SECS)
       response = handle.read()
     except socket.timeout as ex:
-      print ('Connection to %s timed out after %d seconds: %s'
-             % (url, self._SOCKET_TIMEOUT_SECS, ex))
+      self.WriteToStderr('Connection to %s timed out after %d seconds: %s'
+                         % (url, self._SOCKET_TIMEOUT_SECS, ex))
       return None
     except urllib2.URLError as ex:
-      print 'Connection to %s failed: %s' % (url, ex.reason)
+      self.WriteToStderr('Connection to %s failed: %s' % (url, ex.reason))
       return None
+    # Write the response to stderr so it will be uploaded with the other system
+    # log files. This will allow turbogrinder to alert on the radio subsystem.
+    self.WriteToStderr(response, is_json=True)
     return response
 
   def CreateDirs(self, dir_to_create):
@@ -115,7 +134,7 @@
     except os.error as ex:
       if ex.errno == errno.EEXIST:
         return True
-      print 'Failed to create directory: %s' % ex
+      self.WriteToStderr('Failed to create directory: %s' % ex)
       return False
     return True
 
diff --git a/jsonpoll/jsonpoll_test.py b/jsonpoll/jsonpoll_test.py
index ccd05ad..d8431f0 100644
--- a/jsonpoll/jsonpoll_test.py
+++ b/jsonpoll/jsonpoll_test.py
@@ -24,70 +24,43 @@
 import jsonpoll
 
 JSON_RESPONSE = {
-    'report': {
-        'abs_mse_db': '-3276.8',
-        'adc_count': '3359',
-        'external_agc_ind': '9.2',
-        'in_power_rssi_dbc': '-12.0',
-        'inband_power_rssi_dbc': '-67.2',
-        'internal_agc_db': '55.2',
-        'msr_pwr_rssi_dbm': '-20.0',
-        'norm_mse_db': '-3270.9',
-        'num_samples': 'Undefined',
-        'rad_mse_db': '-3267.9',
-        'rx_lock_loss_events': 'Undefined',
-        'rx_lock_loss_time_ms': 'Undefined',
-        'rx_locked': '0',
-        'sample_end_tstamp_ms': '1444839743297',
-        'sample_start_tstamp_ms': '1444839743287'
-    },
-    'result': {
-        'err_code': 0,
-        'err_msg': 'None',
-        'status': 'SUCCESS'
-    },
-    'running_config': {
-        'acmb_enable': True,
-        'bgt_tx_vga_gain_ind': 0,
-        'heartbeat_rate': 60,
-        'ip_addr': '10.0.0.40',
-        'ip_gateway': '10.0.0.1',
-        'ip_netmask': '255.255.255.0',
-        'ipv6_addr': 'fe80::7230:d5ff:fe00:1418',
-        'manual_acmb_profile_indx': 0,
-        'modem_cfg_file': 'default.bin',
-        'modem_on': True,
-        'pa_lna_enable': True,
-        'radio_heater_on': False,
-        'radio_on': True,
-        'report_avg_window_ms': 10,
-        'report_dest_ip': '192.168.1.1',
-        'report_dest_port': 4950,
-        'report_enable': True,
-        'report_interval_hz': 1,
-        'rx_khz': 85500000,
-        'tx_khz': 75500000
-    },
+    'firmware': '/foo/bar/modem.fw',
+    'network': {
+        'rxCounters': {
+            'broadcast': 0,
+            'bytes': 0,
+            'crcErrors': 0,
+            'frames': 0,
+            'frames1024_1518': 0,
+            'frames128_255': 0,
+            'frames256_511': 0,
+            'frames512_1023': 0,
+            'frames64': 0,
+            'frames65_127': 0,
+            'framesJumbo': 0,
+            'framesUndersized': 0,
+            'multicast': 0,
+            'unicast': 0
+        },
+    }
 }
 
 
 class FakeJsonPoll(jsonpoll.JsonPoll):
   """Mock JsonPoll."""
 
+  def WriteToStderr(self, unused_msg, unused_is_json=False):
+    self.error_count += 1
+
   def GetHttpResponse(self, unused_url):
     self.get_response_called = True
+    if self.generate_empty_response:
+      return None
     return json.dumps(JSON_RESPONSE)
 
 
 class JsonPollTest(unittest.TestCase):
 
-  def setUp(self):
-    self.CreateTempFile()
-    self.poller = FakeJsonPoll('fakehost.blah', 31337, 1)
-
-  def tearDown(self):
-    self.DeleteTempFile()
-
   def CreateTempFile(self):
     # Create a temp file and have that be the target output file.
     fd, self.output_file = tempfile.mkstemp()
@@ -97,11 +70,21 @@
     if os.path.exists(self.output_file):
       os.unlink(self.output_file)
 
+  def setUp(self):
+    self.CreateTempFile()
+    self.poller = FakeJsonPoll('fakehost.blah', 31337, 1)
+    self.poller.error_count = 0
+    self.poller.generate_empty_response = False
+
+  def tearDown(self):
+    self.DeleteTempFile()
+
   def testRequestStats(self):
     # Create a fake entry in the paths_to_stats map.
     self.poller.paths_to_statfiles = {'fake/url': self.output_file}
     self.poller.RequestStats()
     self.assertEqual(True, self.poller.get_response_called)
+    self.assertEqual(0, self.poller.error_count)
 
     # Read back the contents of the fake output file. It should be the
     # equivalent JSON representation we wrote out from the mock.
@@ -109,10 +92,17 @@
       output = ''.join(line.rstrip() for line in f)
       self.assertEqual(json.dumps(JSON_RESPONSE), output)
 
-  def testRequestStatsFailureToCreateOutputFile(self):
+  def testRequestStatsFailureToCreateDirOutput(self):
     self.poller.paths_to_statfiles = {'fake/url': '/root/cannotwrite'}
-    result = self.poller.RequestStats()
-    self.assertEqual(False, result)
+    self.poller.RequestStats()
+    self.assertTrue(self.poller.error_count > 0)
+
+  def testRequestStatsFailedToGetResponse(self):
+    self.poller.paths_to_statfiles = {'fake/url': self.output_file}
+    self.poller.generate_empty_response = True
+    self.poller.RequestStats()
+    self.assertEqual(True, self.poller.get_response_called)
+    self.assertTrue(self.poller.error_count > 0)
 
   def testCachedRequestStats(self):
     # Set the "last_response" as our mock output. This should mean we do not
@@ -121,9 +111,9 @@
 
     # Create a fake entry in the paths_to_stats map.
     self.poller.paths_to_statfiles = {'fake/url': self.output_file}
-    result = self.poller.RequestStats()
+    self.poller.RequestStats()
     self.assertEqual(True, self.poller.get_response_called)
-    self.assertEqual(True, result)
+    self.assertTrue(self.poller.error_count > 0)
 
     # Read back the contents of the fake output file: It should be empty.
     with open(self.output_file, 'r') as f: