You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
484 lines
16 KiB
Plaintext
484 lines
16 KiB
Plaintext
#!/usr/bin/env seiscomp-python
|
|
|
|
from __future__ import print_function
|
|
from getopt import getopt, GetoptError
|
|
from time import time, gmtime
|
|
from datetime import datetime
|
|
import os, sys, signal, glob, re
|
|
from seiscomp.myconfig import MyConfig
|
|
import seiscomp.slclient
|
|
import seiscomp.kernel, seiscomp.config
|
|
|
|
usage_info = """
|
|
Usage:
|
|
slmon [options]
|
|
|
|
SeedLink monitor creating static web pages
|
|
|
|
Options:
|
|
-h, --help display this help message
|
|
-c ini_setup = arg
|
|
-s ini_stations = arg
|
|
-t refresh = float(arg) # XXX not yet used
|
|
-v verbose = 1
|
|
|
|
Examples:
|
|
Start slmon from the command line
|
|
slmon -c $SEISCOMP_ROOT/var/lib/slmon/config.ini
|
|
|
|
Restart slmon in order to update the web pages. Use crontab entries for
|
|
automatic restart, e.g.:
|
|
*/3 * * * * /home/sysop/seiscomp/bin/seiscomp check slmon >/dev/null 2>&1
|
|
"""
|
|
|
|
def usage(exitcode=0):
|
|
sys.stderr.write(usage_info)
|
|
exit(exitcode)
|
|
|
|
try:
|
|
seiscompRoot=os.environ["SEISCOMP_ROOT"]
|
|
except:
|
|
print("\nSEISCOMP_ROOT must be defined - EXIT\n", file=sys.stderr)
|
|
usage(exitcode=2)
|
|
|
|
ini_stations = os.path.join(seiscompRoot,'var/lib/slmon/stations.ini')
|
|
ini_setup = os.path.join(seiscompRoot,'var/lib/slmon/config.ini')
|
|
|
|
regexStreams = re.compile("[SLBVEH][HNLG][ZNE123]")
|
|
|
|
verbose = 0
|
|
|
|
class Module(seiscomp.kernel.Module):
|
|
def __init__(self, env):
|
|
seiscomp.kernel.Module.__init__(self, env, env.moduleName(__file__))
|
|
|
|
def printCrontab(self):
|
|
print("3 * * * * %s/bin/seiscomp check slmon >/dev/null 2>&1" % (self.env.SEISCOMP_ROOT))
|
|
|
|
class Status:
|
|
|
|
def __repr__(self):
|
|
return "%2s %-5s %2s %3s %1s %s %s" % \
|
|
(self.net, self.sta, self.loc, self.cha, self.typ, \
|
|
str(self.last_data), str(self.last_feed))
|
|
class StatusDict(dict):
|
|
|
|
def __init__(self, source=None):
|
|
if source:
|
|
self.read(source)
|
|
|
|
def fromSlinkTool(self,server="",stations=["GE_MALT","GE_MORC","GE_IBBN"]):
|
|
# later this shall use XML
|
|
cmd = "slinktool -nd 10 -nt 10 -Q %s" % server
|
|
print(cmd)
|
|
f = os.popen(cmd)
|
|
# regex = re.compile("[SLBVEH][HNLG][ZNE123]")
|
|
regex = regexStreams
|
|
for line in f:
|
|
net_sta = line[:2].strip() + "_" + line[3:8].strip()
|
|
if not net_sta in stations:
|
|
continue
|
|
typ = line[16]
|
|
if typ != "D":
|
|
continue
|
|
cha = line[12:15].strip()
|
|
if not regex.match(cha):
|
|
continue
|
|
|
|
d = Status()
|
|
d.net = line[ 0: 2].strip()
|
|
d.sta = line[ 3: 8].strip()
|
|
d.loc = line[ 9:11].strip()
|
|
d.cha = line[12:15]
|
|
d.typ = line[16]
|
|
d.last_data = seiscomp.slclient.timeparse(line[47:70])
|
|
d.last_feed = d.last_data
|
|
sec = "%s_%s" % (d.net, d.sta)
|
|
sec = "%s.%s.%s.%s.%c" % (d.net, d.sta, d.loc, d.cha, d.typ)
|
|
self[sec] = d
|
|
|
|
def read(self, source):
|
|
if type(source) == str:
|
|
source = file(source)
|
|
if type(source) == file:
|
|
source = source.readlines()
|
|
if type(source) != list:
|
|
raise TypeError('cannot read from %s' % str(type(source)))
|
|
|
|
for line in source:
|
|
d = Status()
|
|
d.net = line[ 0: 2]
|
|
d.sta = line[ 3: 8].strip()
|
|
d.loc = line[ 9:11].strip()
|
|
d.cha = line[12:15]
|
|
d.typ = line[16]
|
|
d.last_data = seiscomp.slclient.timeparse(line[18:41])
|
|
d.last_feed = seiscomp.slclient.timeparse(line[42:65])
|
|
if d.last_feed < d.last_data:
|
|
d.last_feed = d.last_data
|
|
sec = "%s_%s:%s.%s.%c" % (d.net, d.sta, d.loc, d.cha, d.typ)
|
|
self[sec] = d
|
|
|
|
def write(self, f):
|
|
if type(f) is str:
|
|
f = file(f, "w")
|
|
lines = []
|
|
for key in list(self.keys()):
|
|
lines.append(str(self[key]))
|
|
lines.sort()
|
|
f.write('\n'.join(lines)+'\n')
|
|
|
|
|
|
def colorLegend(htmlfile):
|
|
htmlfile.write("<p><center>Latencies:<br>\n" \
|
|
"<table cellpadding='2' cellspacing='1' border='0'" \
|
|
" bgcolor='#000000'>\n<tr>\n" \
|
|
"<td bgcolor='#cc99ff'> <30 m </td>\n" \
|
|
"<td bgcolor='#3399ff'> < 1 h </td>\n" \
|
|
"<td bgcolor='#00ff00'> < 2 h </td>\n" \
|
|
"<td bgcolor='#ffff00'> < 6 h </td>\n" \
|
|
"<td bgcolor='#ff9966'> < 1 d </td>\n" \
|
|
"<td bgcolor='#ff3333'> < 2 d </td>\n" \
|
|
"<td bgcolor='#ffcccc'> < 3 d </td>\n" \
|
|
"<td bgcolor='#cccccc'> < 4 d </td>\n" \
|
|
"<td bgcolor='#999999'> < 5 d </td>\n" \
|
|
"<td bgcolor='#666666'> > 5 d </td>\n" \
|
|
"</tr>\n</table>\n</center></p>\n")
|
|
|
|
# encodes an email address so that it cannot (easily) be extracted
|
|
# from the web page. This is meant to be a spam protection.
|
|
def encode(txt): return ''.join(["&#%d;" % ord(c) for c in txt])
|
|
|
|
def total_seconds(td): return td.seconds + (td.days*86400)
|
|
|
|
def pageTrailer(htmlfile, config):
|
|
|
|
htmlfile.write("<hr>\n" \
|
|
"<table width='99%%' cellpaddding='2' cellspacing='1' border='0'>\n" \
|
|
"<tr>\n<td>Last updated %04d/%02d/%02d %02d:%02d:%02d UTC</td>\n" \
|
|
" <td align='right'><a href='%s' " \
|
|
"target='_top'>%s</a></td>\n</tr>\n" \
|
|
"</table>\n</body></html>\n" % (gmtime()[:6] + (config['setup']['linkurl'],) + (config['setup']['linkname'],)) )
|
|
|
|
def getColor(delta):
|
|
delay = total_seconds(delta)
|
|
if delay >432000: return '#666666'
|
|
if delay >345600: return '#999999'
|
|
if delay >259200: return '#cccccc'
|
|
if delay >172800: return '#ffcccc'
|
|
if delay > 86400: return '#ff3333'
|
|
elif delay > 21600: return '#ff9966'
|
|
elif delay > 7200: return '#ffff00'
|
|
elif delay > 3600: return '#00ff00'
|
|
elif delay > 1800: return '#3399ff'
|
|
else: return '#cc99ff'
|
|
|
|
TDdummy = "<td align='center' bgcolor='%s'><tt>n/a</tt></td>"
|
|
|
|
def TDf(delta, col="#ffffff"):
|
|
if delta is None: return TDdummy % col
|
|
|
|
t = total_seconds(delta)
|
|
|
|
if t > 86400: x = "%.1f d" % (t/86400.)
|
|
elif t > 7200: x = "%.1f h" % (t/3600.)
|
|
elif t > 120: x = "%.1f m" % (t/60.)
|
|
else: x = "%.1f s" % (t)
|
|
return "<td align='right' bgcolor='%s'><tt> %s </tt></td>" % \
|
|
(col,x)
|
|
|
|
def TDt(t, col="#ffffff"):
|
|
if t is None: return TDdummy % col
|
|
|
|
x = t.strftime("%Y/%m/%d %H:%M:%S")
|
|
return "<td align='center' bgcolor='%s'><tt> %s </tt></td>" % \
|
|
(col,x)
|
|
|
|
def myrename(name1, name2):
|
|
|
|
# fault-tolerant rename that doesn't cause an exception if it fails, which
|
|
# may happen e.g. if the target is on a non-reachable NFS directory
|
|
try:
|
|
os.rename(name1, name2)
|
|
except OSError:
|
|
print("failed to rename(%s,%s)" % (name1, name2), file=sys.stderr)
|
|
|
|
|
|
def makeMainHTML(config):
|
|
|
|
global status
|
|
|
|
now = datetime.utcnow()
|
|
|
|
stations = []
|
|
|
|
streams = [ x for x in list(status.keys()) if regexStreams.search(x) ]
|
|
|
|
streams.sort()
|
|
|
|
tmp_rt = []
|
|
tmp_du = []
|
|
|
|
for label in streams:
|
|
lat1 = now - status[label].last_data # XXX
|
|
lat2 = now - status[label].last_feed # XXX
|
|
lat3 = lat1-lat2 # XXX
|
|
if lat3 == 0.: lat3 = lat2 = None
|
|
|
|
if label[-2]=='.' and label[-1] in "DE":
|
|
label = label[:-2]
|
|
n,s,x,x = label.split(".")
|
|
if s in stations: continue # avoid duplicates for different locations
|
|
stations.append(s)
|
|
|
|
net_sta = "%s_%s" % (n,s)
|
|
line = "<tr bgcolor='#ffffff'><td><tt> %s <a " \
|
|
"href='%s.html'>%s</a> </td>%s%s%s</tr>" \
|
|
% (n, net_sta, s, TDf(lat1, getColor(lat1)),
|
|
TDf(lat2, getColor(lat2)),
|
|
TDf(lat3, getColor(lat3)))
|
|
if config.station[net_sta]['type'][:4] == 'real':
|
|
tmp_rt.append(line)
|
|
else: tmp_du.append(line)
|
|
makeStatHTML(net_sta, config)
|
|
|
|
try: os.makedirs(config['setup']['wwwdir'])
|
|
except: pass
|
|
|
|
temp = "%s/tmp.html" % config['setup']['wwwdir']
|
|
dest = "%s/index.html" % config['setup']['wwwdir']
|
|
|
|
table_begin = """
|
|
<table cellpaddding='2' cellspacing='1' border='0' bgcolor='#000000'>
|
|
<tr>
|
|
<th bgcolor='#ffffff' rowspan='2' align='center'>Station</th>
|
|
<th bgcolor='#ffffff' colspan='3' align='center'>Latencies</th>
|
|
</tr>
|
|
<tr>
|
|
<th bgcolor='#ffffff' align='center'>Data</th>
|
|
<th bgcolor='#ffffff' align='center'>Feed</th>
|
|
<th bgcolor='#ffffff' align='center'>Diff.</th>
|
|
</tr>
|
|
"""
|
|
table_end = """
|
|
</table>
|
|
"""
|
|
|
|
htmlfile = open(temp, "w")
|
|
htmlfile.write("""<html>
|
|
<head>
|
|
<title>%s</title>
|
|
<meta http-equiv='refresh' content='%d'>
|
|
<link rel='SHORTCUT ICON' href='%s'>
|
|
</head>
|
|
<body bgcolor='#ffffff'>
|
|
<center><font size='+2'>%s</font></center>\n""" % \
|
|
( config['setup']['title'], int(config['setup']['refresh']),
|
|
config['setup']['icon'], config['setup']['title']))
|
|
|
|
|
|
htmlfile.write("<center><table cellpaddding='5' cellspacing='5'><tr>\n")
|
|
if len(tmp_rt):
|
|
htmlfile.write("<td valign='top' align='center'>\n" \
|
|
"<font size='+1'>Real-time stations<font>\n</td>\n")
|
|
if len(tmp_du):
|
|
htmlfile.write("<td valign='top' align='center'>\n" \
|
|
"<font size='+1'>Dial-up stations<font>\n</td>\n")
|
|
htmlfile.write("</tr><tr>")
|
|
if len(tmp_rt):
|
|
htmlfile.write("<td valign='top' align='center'>\n")
|
|
htmlfile.write(table_begin)
|
|
htmlfile.write("\n".join(tmp_rt))
|
|
htmlfile.write(table_end)
|
|
htmlfile.write("</td>\n")
|
|
if len(tmp_du):
|
|
htmlfile.write("<td valign='top' align='center'>\n")
|
|
htmlfile.write(table_begin)
|
|
htmlfile.write("\n".join(tmp_du))
|
|
htmlfile.write(table_end)
|
|
htmlfile.write("</td>\n")
|
|
htmlfile.write("</tr></table></center>\n")
|
|
|
|
colorLegend(htmlfile)
|
|
pageTrailer(htmlfile, config)
|
|
htmlfile.close()
|
|
myrename(temp, dest)
|
|
|
|
|
|
def makeStatHTML(net_sta, config):
|
|
global status
|
|
|
|
try: os.makedirs(config['setup']['wwwdir'])
|
|
except: pass
|
|
|
|
temp = "%s/tmp2.html" % config['setup']['wwwdir']
|
|
dest = "%s/%s.html" % ( config['setup']['wwwdir'], net_sta)
|
|
|
|
htmlfile = open(temp, "w")
|
|
htmlfile.write("""<html>
|
|
<head>
|
|
<title>%s - Station %s</title>
|
|
<meta http-equiv='refresh' content='%d'>
|
|
<link rel='SHORTCUT ICON' href='%s'>
|
|
</head>
|
|
<body bgcolor='#ffffff'>
|
|
<center><font size='+2'>%s - Station %s</font>\n""" % \
|
|
( config['setup']['title'], net_sta, int(config['setup']['refresh']),
|
|
config['setup']['icon'],
|
|
config['setup']['title'], net_sta.split("_")[-1]))
|
|
|
|
try:
|
|
name = config.station[net_sta]['info']
|
|
htmlfile.write("<br><font size='+1'>%s</font>" % name)
|
|
except: pass
|
|
htmlfile.write("</center>\n")
|
|
|
|
if 'text' in config.station[net_sta]:
|
|
htmlfile.write("<P>%s</P>\n" % config.station[net_sta]['text'])
|
|
|
|
htmlfile.write("""<p><center>
|
|
<table cellpadding='2' cellspacing='1' border='0' bgcolor='#000000'>
|
|
<tr>
|
|
<th bgcolor='#ffffff' align='center' rowspan='2'>Station/<br>Channel</th>
|
|
<th bgcolor='#ffffff' align='center' colspan='2'>Data</th>
|
|
<th bgcolor='#ffffff' align='center' colspan='2'>Feed</th>
|
|
<th bgcolor='#ffffff' align='center' rowspan='2'>Diff.</th>
|
|
</tr>
|
|
<tr>
|
|
<th bgcolor='#ffffff' align='center'>Last Sample</th>
|
|
<th bgcolor='#ffffff' align='center'>Latency</th>
|
|
<th bgcolor='#ffffff' align='center'>Last Received</th>
|
|
<th bgcolor='#ffffff' align='center'>Latency</th>
|
|
</tr>""")
|
|
|
|
now = datetime.utcnow()
|
|
|
|
netsta2=net_sta.replace("_",".")
|
|
streams = [ x for x in list(status.keys()) if x.find(netsta2)==0 ]
|
|
streams.sort()
|
|
for label in streams:
|
|
tim1 = status[label].last_data
|
|
tim2 = status[label].last_feed
|
|
|
|
lat1, lat2, lat3 = now-tim1, now-tim2, tim2-tim1
|
|
col1, col2, col3 = getColor(lat1), getColor(lat2), getColor(lat3)
|
|
if lat1==lat2: lat2 = lat3 = None
|
|
if label[-2]=='.' and label[-1] in "DE":
|
|
label = label[:-2]
|
|
n,s,loc,c = label.split(".")
|
|
c = ("%s.%s" % (loc,c)).strip(".")
|
|
htmlfile.write("<tr bgcolor='#ffffff'><td>" \
|
|
"<tt> %s %s </td>%s%s%s%s%s</tr>\n" \
|
|
% (s, c, TDt(tim1, col1), TDf(lat1, col1),
|
|
TDt(tim2, col2), TDf(lat2, col2),
|
|
TDf(lat3, col3)))
|
|
|
|
htmlfile.write("</table></p>\n")
|
|
colorLegend(htmlfile)
|
|
|
|
htmlfile.write("<p>\nHow to <a href='http://geofon.gfz-potsdam.de/waveform/status/latency.php' target='_blank'>interpret</a> " \
|
|
"these numbers?<br>\n")
|
|
if 'liveurl' in config['setup']:
|
|
# substitute '%s' in live_url by station name
|
|
url = config['setup']['liveurl'] % s
|
|
htmlfile.write("View a <a href='%s' target='_blank'>live seismogram</a> of "
|
|
"station %s</center>\n" % (url, s))
|
|
htmlfile.write("</p>\n")
|
|
pageTrailer(htmlfile, config)
|
|
htmlfile.close()
|
|
myrename(temp, dest)
|
|
|
|
def read_ini():
|
|
global config, ini_setup, ini_stations
|
|
print("\nreading setup config from '%s'" % ini_setup)
|
|
if not os.path.isfile(ini_setup):
|
|
print("[error] setup config '%s' does not exist" % ini_setup, file=sys.stderr)
|
|
usage(exitcode=2)
|
|
|
|
config = MyConfig(ini_setup)
|
|
print("reading station config from '%s'" % ini_stations)
|
|
if not os.path.isfile(ini_stations):
|
|
print("[error] station config '%s' does not exist" % ini_stations, file=sys.stderr)
|
|
usage(exitcode=2)
|
|
config.station = MyConfig(ini_stations)
|
|
|
|
def SIGINT_handler(signum, frame):
|
|
global status
|
|
print("received signal #%d => will write status file and exit" % signum)
|
|
# status.write("status.tab")
|
|
sys.exit(0)
|
|
|
|
try:
|
|
opts, args = getopt(sys.argv[1:], "c:s:t:hv")
|
|
except GetoptError:
|
|
print("\nUnknown option in "+str(sys.argv[1:])+" - EXIT.", file=sys.stderr)
|
|
usage(exitcode=2)
|
|
|
|
for flag, arg in opts:
|
|
if flag == "-c": ini_setup = arg
|
|
if flag == "-s": ini_stations = arg
|
|
if flag == "-t": refresh = float(arg) # XXX not yet used
|
|
if flag == "-h": usage(exitcode=0)
|
|
if flag == "-v": verbose = 1
|
|
|
|
|
|
signal.signal(signal.SIGHUP, SIGINT_handler)
|
|
signal.signal(signal.SIGINT, SIGINT_handler)
|
|
signal.signal(signal.SIGQUIT, SIGINT_handler)
|
|
signal.signal(signal.SIGTERM, SIGINT_handler)
|
|
|
|
read_ini()
|
|
|
|
cha = "???"
|
|
loc = ""
|
|
|
|
s = config.station
|
|
net_sta = ["%s_%s" % (s[k]['net'],s[k]['sta']) for k in s]
|
|
s_arg = ','.join(net_sta)
|
|
streams = [ (s[k]['net'],s[k]['sta'],loc,cha) for k in s ]
|
|
|
|
|
|
if 'server' in config['setup']:
|
|
server = config['setup']['server']
|
|
else: server = "localhost"
|
|
|
|
#def read_initial(config):
|
|
#
|
|
# for s in config.station:
|
|
# print s,glob.glob("/home/dcop/seedlink/%s/segments/*" % s)
|
|
# for f in glob.glob("/home/dcop/seedlink/%s/segments/*" % s):
|
|
# print f
|
|
#
|
|
#read_initial(config)
|
|
|
|
|
|
#print "reading initial time windows from file 'status.tab'"
|
|
#status = StatusDict("status.tab")
|
|
status = StatusDict()
|
|
#if verbose: status.write(sys.stderr)
|
|
|
|
|
|
print("generating output to '%s'" % config['setup']['wwwdir'])
|
|
|
|
print("getting initial time windows from SeedLink server '%s'" % server)
|
|
status.fromSlinkTool(server, stations=net_sta)
|
|
if verbose: status.write(sys.stderr)
|
|
|
|
nextTimeGenerateHTML = time()
|
|
|
|
print("setting up connection to SeedLink server '%s'" % server)
|
|
|
|
input = seiscomp.slclient.Input(server, streams)
|
|
for rec in input:
|
|
id = '.'.join([rec.net, rec.sta, rec.loc, rec.cha, rec.rectype])
|
|
# if not id in status: continue # XXX XXX XXX
|
|
try:
|
|
status[id].last_data = rec.end_time
|
|
status[id].last_feed = datetime.utcnow()
|
|
except:
|
|
continue
|
|
|
|
if time() > nextTimeGenerateHTML:
|
|
makeMainHTML(config)
|
|
nextTimeGenerateHTML = time() + int(config['setup']['refresh'])
|