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: