blob: a5dec761dd0791c0c15b85ac642883b2ef42fc5c [file] [log] [blame]
#!/usr/bin/python
#
"""Implementation of the read-only Diagnostics UI."""
__author__ = 'anandkhare@google.com (Anand Khare)'
import datetime
import errno
import hashlib
import json
import mimetypes
import os
import google3
import tornado.web
import tr.cwmptypes
import tr.helpers
import tr.mainloop
import tr.pyinotify
# For unit test overrides.
ONU_STAT_FILE = '/tmp/cwmp/monitoring/onu/onustats.json'
LICENSES_FILE = '/usr/share/LICENSES.zip'
ACTIVEWAN = 'activewan'
AP_DIR = '/tmp/waveguide/signals_json'
SELFSIGNALS_FILE = '/tmp/waveguide/signals_json/self_signals'
APSIGNAL_FILE = '/tmp/waveguide/signals_json/ap_signals'
SOFTWARE_VERSION_FILE = '/etc/version'
MOCAGLOBALJSON = '/tmp/cwmp/monitoring/moca2/globals'
JSON_DEADLINE = 1
def IOLoop():
return tr.mainloop.IOLoopWrapper.instance()
def ReadOnuStats():
"""Read the ONU stat file and create JSON object."""
try:
stats = open(ONU_STAT_FILE).read()
except IOError as e:
if e.errno != errno.ENOENT:
print 'Failed to read onu stat file: %s' % e
return
try:
json_stats = json.loads(stats)
except ValueError as e:
print 'Failed to decode onu stat file: %s' % e
return
return json_stats
def JsonGet(page, ui, update_dictionary):
"""Handle a request to get a new version of a dict when it's available.
Args:
page: A RequestHandler instance for the request being handled.
ui: The TechUI or DiagUI instance that the request is for. Must have a
'data' attribute with a 'checksum' key; this is how we determine when
to reply with a new version of the dictonary. The 'data' attribute is
serialized to JSON and sent as the response when ready.
update_dictionary: A function that will update ui.data with the latest
information.
"""
page.ui = ui
update_dictionary()
if (page.get_argument('checksum') !=
ui.data.get('checksum', None)):
page.set_header('Content-Type', 'application/json')
page.write(json.dumps(ui.data))
page.finish()
else:
def Send(deadline_exceeded):
"""Called by the cwmp watchers when they think new data might be ready.
The data might not actually be updated; in that case, we don't send.
Args:
deadline_exceeded: True if the timeout for answering the request has
arrived. In that case, we send whatever we have.
"""
if (not deadline_exceeded and
page.get_argument('checksum') == ui.data['checksum']):
# callback called, but data didn't change, so do nothing.
return
page.ClearCallbacks()
page.set_header('Content-Type', 'application/json')
page.write(json.dumps(ui.data))
page.finish()
page.callback = lambda: Send(False)
page.deadline = IOLoop().add_timeout(
datetime.timedelta(seconds=JSON_DEADLINE), lambda: Send(True))
ui.callbacklist.append(page.callback)
class _JsonHandler(tornado.web.RequestHandler):
"""Base class for handlers that serve async json files."""
def initialize(self):
self.ui = None
self.callback = None
self.deadline = None
def ClearCallbacks(self):
if self.deadline:
dl = self.deadline
self.deadline = None
IOLoop().remove_timeout(dl)
if self.callback:
cb = self.callback
self.callback = None
self.ui.callbacklist.remove(cb)
def on_connection_close(self):
self.ClearCallbacks()
class TechUIJsonHandler(_JsonHandler):
"""Provides JSON-formatted info for the TechUI."""
@tornado.web.asynchronous
def get(self):
print 'techui GET JSON data for diagnostics page'
JsonGet(self, self.application.techui,
self.application.techui.UpdateTechUIDict)
class TechUIHandler(tornado.web.RequestHandler):
"""Display technical UI."""
def get(self):
self.render('techui_static/index.html',
run_licenseui=self.application.run_licenseui)
class StartIsostreamHandler(tornado.web.RequestHandler):
"""Sends requests to start isostream client on all TV boxes."""
@tornado.web.asynchronous
def post(self):
self.outstanding_requests = 0
self.request_results = {}
tv_ip_addrs = self.application.techui.FindTVBoxes()
for ip_addr in tv_ip_addrs:
http_client = tornado.httpclient.AsyncHTTPClient()
request = tornado.httpclient.HTTPRequest(
url=ip_addr + '/isostream',
method='POST', body='',
request_timeout=2,
headers={
'Cookie': '_xsrf=q',
'X-Csrftoken': 'q'
})
http_client.fetch(
request, self.make_request_handler(ip_addr))
self.outstanding_requests += 1
def make_request_handler(self, ip_addr):
return lambda response: self.handle_request(ip_addr, response)
def handle_request(self, host, response):
try:
response.rethrow()
self.request_results[host] = 'SUCCESS'
except tornado.httpclient.HTTPError as e:
self.request_results[host] = (
'ERROR: %s' % e.code)
finally:
self.outstanding_requests -= 1
if self.outstanding_requests == 0:
self.write(self.request_results)
self.finish()
class IsostreamHandler(tornado.web.RequestHandler):
"""Starts isostream client (usually on TV box)."""
def post(self):
print ('Starting isostream due to incoming request from %s'
% self.request.remote_ip)
isostreaminfo = (self.application.root.Device.IP.Diagnostics
.X_CATAWAMPUS_ORG_Isostream)
isostreaminfo.last_log = None
isostreaminfo.ClientRemoteIP = self.request.remote_ip
isostreaminfo.ClientMbps = 14
isostreaminfo.ClientEnable = True
class IsostreamJsonHandler(_JsonHandler):
"""Provides JSON of the last line of the isostream log (usually on TV box)."""
@tornado.web.asynchronous
def get(self):
self.set_header('Content-Type', 'application/json')
isostreaminfo = (self.application.root.Device.IP.Diagnostics
.X_CATAWAMPUS_ORG_Isostream)
isos_dict = {}
last_log_dict = {}
if isostreaminfo.last_log:
last_log_dict = vars(isostreaminfo.last_log)
isos_dict['last_log'] = last_log_dict
isos_dict['ClientRunning'] = isostreaminfo.ClientRunning
self.write(json.dumps(isos_dict))
try:
self.finish()
except IOError:
# Other end already closed the connection. Not an error.
pass
class IsostreamCombinedHandler(tornado.web.RequestHandler):
"""Gathers isostream JSON data from each connected TV box.
This is usually run on the network box.
"""
@tornado.web.asynchronous
def get(self):
self.outstanding_requests = 0
self.combined_data = {}
tv_ip_addrs = self.application.techui.FindTVBoxes()
for ip_addr in tv_ip_addrs:
http_client = tornado.httpclient.AsyncHTTPClient()
request = tornado.httpclient.HTTPRequest(
url=ip_addr + '/isostream.json',
method='GET',
request_timeout=1
)
http_client.fetch(
request, self.make_request_handler(ip_addr))
self.outstanding_requests += 1
def make_request_handler(self, ip_addr):
return lambda response: self.handle_request(ip_addr, response)
def handle_request(self, host, response):
try:
response.rethrow()
self.combined_data[host] = (
json.loads(response.body))
except tornado.httpclient.HTTPError as he:
self.combined_data[host] = {
'Error': 'HTTPError: %s' % str(he)
}
except TypeError as te:
self.combined_data[host] = {
'Error': 'TypeError: %s unable to parse %s' % (str(te), response.body)
}
finally:
self.outstanding_requests -= 1
if self.outstanding_requests == 0:
self.write(self.combined_data)
self.finish()
class DiagUIJsonHandler(_JsonHandler):
"""Provides JSON-formatted content to be displayed in the UI."""
@tornado.web.asynchronous
def get(self):
print 'diagui GET JSON data for diagnostics page'
JsonGet(self, self.application.diagui,
self.application.diagui.UpdateDiagUIDict)
class DiagnosticsHandler(tornado.web.RequestHandler):
"""Displays the diagnostics UI."""
def get(self):
print 'diagui GET diagnostics HTML page'
self.render('template.html', run_techui=self.application.run_techui,
run_licenseui=self.application.run_licenseui)
class DiagUIRestartHandler(tornado.web.RequestHandler):
"""Restart the network box."""
def get(self):
print 'diagui displaying restart interstitial screen'
self.render('restarting.html')
def post(self):
print 'diagui user requested restart'
self.redirect('/restart')
os.system('(sleep 5; reboot) &')
def LoadJson(filename):
try:
return json.loads(open(filename).read())
except ValueError:
return {} # No json to read
except IOError as e:
if e.errno == errno.ENOENT:
return {} # file doesn't exist, harmless
raise
class TechUI(object):
"""Class for the technical UI."""
def __init__(self, root):
self.data = {'wifi_signal_strength': {},
'moca_signal_strength': {},
'moca_corrected_codewords': {},
'moca_uncorrected_codewords': {},
'moca_bitloading': {},
'moca_nbas': {},
'other_aps': {},
'self_signals': {},
'host_names': {},
'ip_addr': {},
'onu_stats': {},
'softversion': '',
'serialnumber': '',
'checksum': 0}
self.callbacklist = []
self.root = root
if self.root:
for interface in self.root.Device.MoCA.InterfaceList.itervalues():
tr.cwmptypes.AddNotifier(type(interface),
'AssociatedDeviceCount',
lambda _: self.UpdateMocaDict())
landevlist = self.root.InternetGatewayDevice.LANDeviceList
for dev in landevlist.itervalues():
for wlconf in dev.WLANConfigurationList.itervalues():
if hasattr(wlconf, 'SignalsStr'):
tr.cwmptypes.AddNotifier(type(wlconf),
'SignalsStr',
lambda _: self.UpdateWifiDict())
mask = tr.pyinotify.IN_MODIFY
self.ap_wm = tr.pyinotify.WatchManager()
self.ap_notifier = tr.pyinotify.TornadoAsyncNotifier(
self.ap_wm, IOLoop(), callback=lambda _: self.UpdateAPDict())
if os.path.exists(AP_DIR):
self.ap_wm.add_watch(AP_DIR, mask)
def SetTechUIDict(self, key, new_dict):
if key not in self.data:
self.data[key] = new_dict
return True
if self.data[key] != new_dict:
self.data[key] = new_dict
return True
return False
def UpdateMocaDict(self):
"""Updates the dictionary with Moca data from catawampus."""
updated = False
snr = {}
bitloading = {}
corrected_cw = {}
uncorrected_cw = {}
nbas = {}
global_content = LoadJson(MOCAGLOBALJSON)
global_node_id = global_content.get('NodeId', 17) # max nodes is 16
for interface in self.root.Device.MoCA.InterfaceList.itervalues():
for dev in interface.AssociatedDeviceList.itervalues():
if dev.NodeID != global_node_id: # to avoid getting info about self
snr[dev.MACAddress] = dev.X_CATAWAMPUS_ORG_RxSNR_dB
bitloading[dev.MACAddress] = dev.X_CATAWAMPUS_ORG_RxBitloading
nbas[dev.MACAddress] = dev.X_CATAWAMPUS_ORG_RxNBAS
corrected = (dev.X_CATAWAMPUS_ORG_RxPrimaryCwCorrected +
dev.X_CATAWAMPUS_ORG_RxSecondaryCwCorrected)
uncorrected = (dev.X_CATAWAMPUS_ORG_RxPrimaryCwUncorrected +
dev.X_CATAWAMPUS_ORG_RxSecondaryCwUncorrected)
no_errors = (dev.X_CATAWAMPUS_ORG_RxPrimaryCwNoErrors +
dev.X_CATAWAMPUS_ORG_RxSecondaryCwNoErrors)
total = corrected + uncorrected + no_errors
if total != 0:
corrected_cw[dev.MACAddress] = corrected/total
uncorrected_cw[dev.MACAddress] = uncorrected/total
else:
corrected_cw[dev.MACAddress] = 0
uncorrected_cw[dev.MACAddress] = 0
updated = self.SetTechUIDict('moca_signal_strength', snr)
updated = self.SetTechUIDict('moca_corrected_codewords',
corrected_cw) or updated
updated = self.SetTechUIDict('moca_uncorrected_codewords',
uncorrected_cw) or updated
updated = self.SetTechUIDict('moca_bitloading', bitloading) or updated
updated = self.SetTechUIDict('moca_nbas', nbas) or updated
if updated:
self.NotifyUpdatedDict()
def UpdateWifiDict(self):
"""Updates the wifi signal strength dict using catawampus."""
wifi_signal_strengths = {}
landevlist = self.root.InternetGatewayDevice.LANDeviceList
for dev in landevlist.itervalues():
for wlconf in dev.WLANConfigurationList.itervalues():
if hasattr(wlconf, 'signals'):
wifi_signal_strengths.update(wlconf.signals)
if self.SetTechUIDict('wifi_signal_strength', wifi_signal_strengths):
self.NotifyUpdatedDict()
def UpdateAPDict(self):
"""Reads JSON from the access points files and updates the dict."""
# TODO(theannielin): waveguide data should be in cwmp, but it's not,
# so we read it here
updated = False
other_aps = LoadJson(APSIGNAL_FILE)
self_signals = LoadJson(SELFSIGNALS_FILE)
updated = self.SetTechUIDict('other_aps', other_aps)
updated = self.SetTechUIDict('self_signals', self_signals) or updated
if updated:
self.NotifyUpdatedDict()
def UpdateTechUIDict(self):
"""Updates the data dictionary."""
if not self.root:
return
host_names = {}
ip_addr = {}
try:
hostinfo = self.root.Device.Hosts.HostList
except AttributeError:
hostinfo = {}
for host in hostinfo.itervalues():
host_names[host.PhysAddress] = host.HostName
ip_addr[host.PhysAddress] = host.IPAddress
self.data['host_names'] = host_names
self.data['ip_addr'] = ip_addr
deviceinfo = self.root.Device.DeviceInfo
self.data['softversion'] = deviceinfo.SoftwareVersion
self.data['serialnumber'] = deviceinfo.SerialNumber
self.UpdateMocaDict()
self.UpdateWifiDict()
self.UpdateAPDict()
self.UpdateCheckSum()
self.UpdateOnuStats()
def NotifyUpdatedDict(self):
self.UpdateCheckSum()
for i in self.callbacklist[:]:
i()
def UpdateCheckSum(self):
self.data['checksum'] = 0
newchecksum = hashlib.sha1(unicode(
sorted(list(self.data.items()))).encode('utf-8')).hexdigest()
self.data['checksum'] = 'sha1%s' % newchecksum
def FindTVBoxes(self):
tv_ip_addrs = []
if 'host_names' in self.data:
for mac_addr, host_name in self.data['host_names'].iteritems():
if host_name.startswith('GFiberTV'):
tv_ip_addrs.append(self.data['ip_addr'][mac_addr])
return tv_ip_addrs
def UpdateOnuStats(self):
stats = ReadOnuStats()
if stats is not None:
self.SetTechUIDict('onu_stats', stats)
class LicenseUI(object):
"""Class for the License UI."""
def __init__(self, root):
self.root = root
class LicenseUIHandler(tornado.web.RequestHandler):
"""Displays license tab of diagui."""
def get(self):
print 'diagui GET licenses HTML page'
self.render('license_static/index.html',
run_diagui=self.application.run_diagui,
run_techui=self.application.run_techui)
class LicenseFileHandler(tornado.web.RequestHandler):
"""Displays the zipped license file."""
def get(self):
try:
lf = open(LICENSES_FILE).read()
except IOError as e:
if e.errno != errno.ENOENT:
print 'Failed to read onu stat file: %s' % e
return
self.write(lf)
class DiagUI(object):
"""Class for the diagnostics UI."""
def __init__(self, root, cpemach):
self.data = {}
self.root = root
self.cpemach = cpemach
self.pathname = os.path.dirname(__file__)
if self.root:
# TODO(anandkhare): Add notifiers on more parameters using the same format
# as below, as and when they are implemented using types.py.
tr.cwmptypes.AddNotifier(type(self.root.Device.Ethernet),
'InterfaceNumberOfEntries', self.AlertNotifiers)
self.wm = tr.pyinotify.WatchManager()
self.mask = tr.pyinotify.IN_CLOSE_WRITE
self.callbacklist = []
self.notifier = tr.pyinotify.TornadoAsyncNotifier(
self.wm, IOLoop(), callback=self.AlertNotifiers)
self.wdd = self.wm.add_watch(
os.path.join(self.pathname, 'Testdata'), self.mask)
def AlertNotifiers(self, unused_obj):
self.UpdateDiagUIDict()
for i in self.callbacklist[:]:
i()
def UpdateCheckSum(self):
self.data['checksum'] = 0
newchecksum = hashlib.sha1(unicode(
sorted(list(self.data.items()))).encode('utf-8')).hexdigest()
self.data['checksum'] = newchecksum
def UpdateDiagUIDict(self):
"""Updates the dictionary and checksum value."""
if not self.root:
return
self.data = {}
self.data['subnetmask'] = ''
deviceinfo = self.root.Device.DeviceInfo
tempstatus = deviceinfo.TemperatureStatus
landevlist = self.root.InternetGatewayDevice.LANDeviceList
etherlist = self.root.Device.Ethernet.InterfaceList
if self.cpemach and self.cpemach.last_success_response:
self.data['acs'] = 'OK (%s)' % self.cpemach.last_success_response
else:
self.data['acs'] = 'Never contacted'
self.data['softversion'] = deviceinfo.SoftwareVersion
self.data['serialnumber'] = deviceinfo.SerialNumber
self.data['uptime'] = deviceinfo.UpTime
t = dict()
try:
for sensor in tempstatus.TemperatureSensorList.itervalues():
t[sensor.Name] = sensor.Value
except AttributeError:
pass
else:
self.data['temperature'] = t
wan_addrs = dict()
lan_addrs = dict()
for inter in self.root.Device.IP.InterfaceList.itervalues():
t = wan_addrs if inter.Name in ['wan0', 'wan0.2'] else lan_addrs
for ip4 in inter.IPv4AddressList.itervalues():
# Static IPs show up even if there is no address.
if ip4.IPAddress is not None:
t[ip4.IPAddress] = '(%s)' % ip4.Status
self.data['subnetmask'] = ip4.SubnetMask
for ip6 in inter.IPv6AddressList.itervalues():
if ip6.IPAddress[:4] != 'fe80':
t[ip6.IPAddress] = '(%s)' % ip6.Status
self.data['lanip'] = lan_addrs
self.data['wanip'] = wan_addrs
wan_mac = dict()
lan_mac = dict()
t = dict()
for interface in etherlist.itervalues():
if interface.Name in ['wan0', 'wan0.2']:
wan_mac[interface.MACAddress] = '(%s)' % interface.Status
else:
lan_mac[interface.MACAddress] = '(%s)' % interface.Status
self.data['lanmac'] = lan_mac
self.data['wanmac'] = wan_mac
wlan = dict()
wpa = dict()
self.data['ssid5'] = ''
for dev in landevlist.itervalues():
for wlconf in dev.WLANConfigurationList.itervalues():
# Convert the channel to an int here. It is returned as a string.
try:
ch = int(wlconf.Channel)
except ValueError:
print ('wlconf.Channel returned a non-integer value: %s' %
(wlconf.Channel,))
continue
if ch in range(1, 12):
self.data['ssid24'] = wlconf.SSID
if wlconf.WPAAuthenticationMode == 'PSKAuthentication':
wpa['2.4 GHz'] = '(Configured)'
wlan[wlconf.BSSID] = '(2.4 GHz) (%s)' % wlconf.Status
else:
self.data['ssid5'] = wlconf.SSID
if wlconf.WPAAuthenticationMode == 'PSKAuthentication':
wpa['5 GHz'] = '(Configured)'
wlan[wlconf.BSSID] = '(5 GHz) (%s)' % wlconf.Status
self.data['wirelesslan'] = wlan
self.data['wpa2'] = wpa
if 'ssid24' in self.data and 'ssid5' in self.data:
if self.data['ssid5'] == self.data['ssid24']:
self.data['ssid5'] = '(same)'
try:
self.data['upnp'] = self.root.UPnP.Device.Enable
except AttributeError:
self.data['upnp'] = 'Off'
try:
dns = self.root.DNS.SD.ServiceList
except AttributeError:
pass
else:
for serv in dns.itervalues():
self.data['dyndns'] = serv.InstanceName
self.data['domain'] = serv.Domain
# We want the 'connected' field to be a boolean, but Activewan
# returns either the empty string, or the name of the active wan
# interface.
self.data['connected'] = not not tr.helpers.Activewan(ACTIVEWAN)
self.UpdateOnuStats()
self.UpdateCheckSum()
def UpdateOnuStats(self):
stats = ReadOnuStats()
if stats is not None:
self.data.update(stats)
class MainApplication(tornado.web.Application):
"""Defines settings for the server and notifier."""
def __init__(self, root, cpemach, run_diagui=False, run_techui=False,
run_licenseui=False):
self.root = root
self.run_diagui = run_diagui
self.run_techui = run_techui
self.run_licenseui = run_licenseui
if not run_diagui and not run_licenseui:
print 'Either diagui or licenseui must be enabled'
return
if run_diagui:
self.diagui = DiagUI(root, cpemach)
if run_techui:
self.techui = TechUI(root)
if run_licenseui:
self.licenseui = LicenseUI(root)
self.pathname = os.path.dirname(__file__)
staticpath = os.path.join(self.pathname, 'static')
self.settings = {
'static_path': staticpath,
'template_path': self.pathname,
'xsrf_cookies': True,
}
# Who handles root domain requests
if run_diagui:
handlers = [
(r'/', DiagnosticsHandler),
]
else:
handlers = [
(r'/', tornado.web.RedirectHandler,
{'url': '/license/index.html'}),
]
if run_diagui:
handlers += [
(r'/content.json', DiagUIJsonHandler),
(r'/restart', DiagUIRestartHandler),
]
if run_techui:
handlers += [
(r'/tech/?', tornado.web.RedirectHandler,
{'url': '/tech/index.html'}),
(r'/tech/index.html', TechUIHandler),
(r'/tech/(.*)', tornado.web.StaticFileHandler,
{'path': os.path.join(self.pathname, 'techui_static')}),
(r'/techui.json', TechUIJsonHandler),
(r'/startisostream', StartIsostreamHandler),
(r'/isostream', IsostreamHandler),
(r'/isostream.json', IsostreamJsonHandler),
(r'/isostreamcombined.json', IsostreamCombinedHandler),
]
if run_licenseui:
handlers += [
(r'/license/?', tornado.web.RedirectHandler,
{'url': '/license/index.html'}),
(r'/license/index.html', LicenseUIHandler),
(r'/license/LICENSES.zip', LicenseFileHandler),
(r'/license/(.*)', tornado.web.StaticFileHandler,
{'path': os.path.join(self.pathname, 'license_static')}),
]
super(MainApplication, self).__init__(handlers, **self.settings)
mimetypes.add_type('font/ttf', '.ttf')