blob: cc38875728808210a1864efe74ea69b54b16ec8c [file] [log] [blame]
#!/usr/bin/python
# Copyright 2015 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""JsonPoll reads a response from a socket and writes it to disk."""
__author__ = 'cgibson@google.com (Chris Gibson)'
import errno
import json
import os
import socket
import sys
import tempfile
import time
import urllib2
import options
optspec = """
jsonpoll [options]
--
host= host to connect to [localhost]
port= port to connect to [8080]
i,interval= poll interval in seconds [15]
"""
class JsonPoll(object):
"""Periodically poll a web server to request stats."""
# The directory that JSON files will be written to. Note that changing this
# path to another filesystem other than /tmp will affect the atomicity of the
# os.rename() when moving files into the final destination.
OUTPUT_DIR = '/tmp/glaukus/'
# The time to wait before giving up on blocking connection operations.
_SOCKET_TIMEOUT_SECS = 15
def __init__(self, host, port, interval):
self.hostport = 'http://%s:%d' % (host, port)
# The time to wait between requests in seconds.
self.poll_interval_secs = interval
self.api_modem_output_file = os.path.join(self.OUTPUT_DIR, 'modem.json')
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 _FlatObject(self, base, obj, out):
"""Open json object to a list of strings.
Args:
base: is a string that has a base name for a key.
obj: is a JSON object that will be flatten.
out: is an output where strings will be appended.
Example:
data = {"a": 1, "b": { "c": 2, "d": 3}}
out = []
_FlatObject('', data, out)
out will be equal to
[u'/a=1', u'/b/c=2', u'/b/d=3']
"""
for k, v in obj.items():
name = base + '/' + k
if isinstance(v, dict):
self._FlatObject(name, v, out)
else:
val = '%s=' % name
out.append(val + str(v))
def WriteToStderr(self, msg, is_json=False):
"""Write a message to stderr."""
if is_json:
flat_data = []
self._FlatObject('', msg, flat_data)
# Make the json easier to parse from the logs.
for s in flat_data:
sys.stderr.write('%s\n' % s)
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():
url = '%s/%s' % (self.hostport, path)
tmpfile = ''
try:
response = self.GetHttpResponse(url)
if not response:
self.WriteToStderr('Failed to get response from glaukus: %s\n' % url)
continue
elif self.last_response == response:
self.WriteToStderr('Skip file write as content has not changed.\n')
continue
self.last_response = response
with tempfile.NamedTemporaryFile(delete=False) as fd:
if not self.CreateDirs(os.path.dirname(output_file)):
self.WriteToStderr('Failed to create output directory: %s\n' %
os.path.dirname(output_file))
continue
tmpfile = fd.name
fd.write(json.dumps(response))
fd.flush()
os.fsync(fd.fileno())
try:
os.rename(tmpfile, output_file)
except OSError as ex:
self.WriteToStderr('Failed to move %s to %s: %s\n' % (
tmpfile, output_file, ex))
continue
finally:
if os.path.exists(tmpfile):
os.unlink(tmpfile)
def ParseJSONFromResponse(self, response):
try:
json_resp = json.loads(response)
except UnicodeDecodeError as ex:
self.WriteToStderr('Non-UTF8 character in HTTP response: %s', ex)
return None
except ValueError as ex:
self.WriteToStderr('Failed to parse JSON from HTTP response: %s', ex)
return None
return json_resp
def GetHttpResponse(self, url):
"""Creates a request and retrieves the response from a web server."""
try:
handle = urllib2.urlopen(url, timeout=self._SOCKET_TIMEOUT_SECS)
response = self.ParseJSONFromResponse(handle.read())
except socket.timeout as ex:
self.WriteToStderr('Connection to %s timed out after %d seconds: %s\n'
% (url, self._SOCKET_TIMEOUT_SECS, ex))
return None
except urllib2.URLError as ex:
self.WriteToStderr('Connection to %s failed: %s\n' % (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.
if response is not None:
self.WriteToStderr(response, is_json=True)
return response
def CreateDirs(self, dir_to_create):
"""Recursively creates directories."""
try:
os.makedirs(dir_to_create)
except os.error as ex:
if ex.errno == errno.EEXIST:
return True
self.WriteToStderr('Failed to create directory: %s\n' % ex)
return False
return True
def RunForever(self):
while True:
self.RequestStats()
time.sleep(self.poll_interval_secs)
def main():
o = options.Options(optspec)
(opt, unused_flags, unused_extra) = o.parse(sys.argv[1:])
poller = JsonPoll(opt.host, opt.port, opt.interval)
poller.RunForever()
if __name__ == '__main__':
main()