PingPanel.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896
  1. #!/usr/bin/env python3
  2. # Copyright 2024 by Siegrist(SystemLoesungen) <PSS@ZweierNet.ch>
  3. # This program is free software: you can redistribute it and/or modify
  4. # it under the terms of the GNU General Public License as published by
  5. # the Free Software Foundation, either version 3 of the License, or
  6. # (at your option) any later version.
  7. #
  8. # This program is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU General Public License for more details.
  12. #import datetime
  13. from datetime import datetime
  14. import os
  15. import errno
  16. import configparser
  17. import time
  18. import signal
  19. import sys
  20. import subprocess
  21. import re
  22. import argparse
  23. from threading import Thread
  24. import json
  25. import tornado.ioloop
  26. import tornado.web
  27. import tornado.websocket
  28. from tornado.gen import sleep
  29. import asyncio
  30. VERSION = '0.9.12'
  31. konfig_file = 'config.cfg'
  32. s_certfile = 'server.crt'
  33. s_keyfile = 'server.key'
  34. ws_method = 'ws'
  35. host_dict = dict()
  36. failed_pingc = dict()
  37. g_since = dict()
  38. y_since = dict()
  39. r_since = dict()
  40. g_timbar = dict()
  41. y_timbar = dict()
  42. r_timbar = dict()
  43. loop_interval = 3
  44. listen_port = 8888
  45. listen_addr = '0.0.0.0'
  46. anz_hosts = 0
  47. bar_duration = 3600
  48. fping_parameter = []
  49. fping_cmd = 'fping'
  50. def signal_handler(signal, frame):
  51. print('\nCtrl+C, keyboardInterrupt detected!')
  52. sys.exit(0)
  53. signal.signal(signal.SIGINT, signal_handler)
  54. config = configparser.ConfigParser(delimiters=('='), comment_prefixes=('#'), allow_no_value=False, strict=True, empty_lines_in_values=False)
  55. def read_config():
  56. try:
  57. if config.read([konfig_file]) != []:
  58. pass
  59. else:
  60. print("Configfile '" + konfig_file + "' not found!")
  61. sys.exit()
  62. except configparser.DuplicateSectionError as e:
  63. print("Error: Duplicate Section '%s' not allowed." % str(e.section))
  64. sys.exit()
  65. except configparser.DuplicateOptionError as e:
  66. print("Error: Duplicate Option '%s' in Section '%s' not allowed." % ( str(e.option), str(e.section) ))
  67. sys.exit()
  68. except configparser.Error as e:
  69. print("Error in Configfile: %s." % str(e))
  70. sys.exit()
  71. #print(config.sections())
  72. global anz_hosts
  73. for sect in config.sections():
  74. for key in config[sect]:
  75. if (sect == "Main"):
  76. try:
  77. global loop_interval
  78. loop_interval = int(config["Main"]["LoopInterval"])
  79. except KeyError:
  80. pass
  81. try:
  82. global listen_port
  83. listen_port = int(config["Main"]["ListenPort"])
  84. except KeyError:
  85. pass
  86. try:
  87. global listen_addr
  88. listen_addr = str(config["Main"]["ListenAddr"])
  89. except KeyError:
  90. pass
  91. try:
  92. title_add = str(config["Main"]["TitleAdd"])
  93. except KeyError:
  94. config["Main"]["TitleAdd"] = ''
  95. try:
  96. global ws_method
  97. if ( str(config["Main"]["UseHTTPS"]) ):
  98. ws_method = 'wss'
  99. s_certfile = str(config["Main"]["CertFile"])
  100. s_keyfile = str(config["Main"]["KeyFile"])
  101. apath = os.path.dirname(os.path.abspath(__file__))
  102. if not file_exists(s_certfile):
  103. print("CertFile not readable or does not exist: %s" % apath + '/' + s_certfile)
  104. sys.exit()
  105. if not file_exists(s_keyfile):
  106. print("KeyFile not readable or does not exist: %s" % apath + '/' + s_keyfile)
  107. sys.exit()
  108. except KeyError:
  109. pass
  110. try:
  111. global bar_duration
  112. bar_duration = restrict(int(config["Main"]["DurationBarSpan"]), 1, 5184000)
  113. except KeyError:
  114. pass
  115. else:
  116. #host_dict[key] = config[sect][key]
  117. host_dict[key] = sect.replace(' ','_')
  118. failed_pingc[key] = 0
  119. anz_hosts += 1
  120. if args.v: print(sect + ": " + key + " = " + config[sect][key])
  121. print("Pinging " + str(anz_hosts) + " Devices in total.")
  122. def cmd_exists(cmd):
  123. path = os.environ["PATH"].split(os.pathsep)
  124. for prefix in path:
  125. filename = os.path.join(prefix, cmd)
  126. executable = os.access(filename, os.X_OK)
  127. is_not_directory = os.path.isfile(filename)
  128. if executable and is_not_directory:
  129. return True
  130. return False
  131. def file_exists(file):
  132. filename = os.path.dirname(os.path.abspath(__file__)) + '/' + file
  133. if os.access(filename, os.R_OK):
  134. return True
  135. return False
  136. def set_since(host,now,set,unset1,unset2):
  137. unset1[host]=None
  138. unset2[host]=None
  139. if set.get(host):
  140. pass
  141. else:
  142. set[host] = now
  143. def restrict(value, lower, upper):
  144. return lower if value < lower else upper if value > upper else value
  145. def create_fpingparam():
  146. global fping_parameter
  147. global fping_cmd
  148. try:
  149. fping_parameter.append(config["Main"]["fpingCommand"])
  150. fping_cmd = config["Main"]["fpingCommand"]
  151. except KeyError:
  152. fping_parameter.append(fping_cmd)
  153. fping_parameter += ['--quiet', '--vcount=3']
  154. try:
  155. fping_parameter.append('--interval=' + config["Main"]["Fping-interval"])
  156. except KeyError:
  157. fping_parameter.append('--interval=1')
  158. try:
  159. fping_parameter.append('--period=' + config["Main"]["Fping-period"])
  160. except KeyError:
  161. fping_parameter.append('--period=100')
  162. try:
  163. fping_parameter.append('--size=' + config["Main"]["Fping-size"])
  164. except KeyError:
  165. fping_parameter.append('--size=56')
  166. pass
  167. def ping(ip, packets=1, timeout=2):
  168. comd = ['ping', '-c', str(packets), '-w', str(timeout), ip]
  169. res = subprocess.run(comd, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
  170. return res.returncode == 0
  171. def fping(ips):
  172. comd = fping_parameter + ips
  173. res = subprocess.run(comd, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) #, universal_newlines = True)
  174. if args.v:
  175. print("fping finished: " + datetime.now().strftime("%H:%M:%S"))
  176. if res.returncode == 2:
  177. print("fping: any IP addresses were not found !")
  178. elif res.returncode == 3:
  179. print("fping: invalid command line arguments !")
  180. elif res.returncode == 4:
  181. print("fping: system call failure !")
  182. #print("RET:"+str(res.returncode))
  183. return res.stdout.decode("utf-8")
  184. # Pinging Loop
  185. async def PingLoop():
  186. print("PingLoop gestartet.")
  187. # sleep for a moment
  188. while True:
  189. await asyncio.sleep(3)
  190. # Sinnlos wenn niemend zuschaut:
  191. if ( len(BroadcastHandler.clients) < 1 ):
  192. continue
  193. for host, name in host_dict.items():
  194. #print("ping host: " + host)
  195. if ping(host,1,2):
  196. line = {
  197. "host": host, "status": "OK"
  198. }
  199. BroadcastHandler.broadcast(line)
  200. else:
  201. line = {
  202. "host": host, "status": "FAILED"
  203. }
  204. BroadcastHandler.broadcast(line)
  205. # End while True
  206. # Fping Loop
  207. async def FpingLoop():
  208. print("FpingLoop ready.")
  209. ip_list = list(host_dict.keys())
  210. create_fpingparam()
  211. # check for ping command
  212. if not cmd_exists(fping_cmd):
  213. print("Exit with Error: fping command not found or not executable: '%s'" % fping_cmd)
  214. sys.exit(1)
  215. if args.v:
  216. tmp_cmd = [] + fping_parameter + ip_list
  217. comd_out = ' '.join(tmp_cmd)
  218. comd_out.replace("'", "")
  219. print('ping command used: "' + comd_out + '"')
  220. # The Loop
  221. while True:
  222. # Sinnlos wenn niemend zuschaut:
  223. if ( len(BroadcastHandler.clients) < 1 ):
  224. await asyncio.sleep(3)
  225. continue
  226. ac_now = datetime.now()
  227. ac_time = ac_now.strftime("%H:%M:%S")
  228. ac_timed = ac_now.strftime("%a %d %H:%M:%S")
  229. tnow = int(ac_now.timestamp())
  230. if args.v: print("call fping: " + ac_time)
  231. ret = fping(ip_list)
  232. rl = ret.splitlines()
  233. json_bc = []
  234. cnt_ok = 0
  235. cnt_halb = 0
  236. cnt_fail = 0
  237. for line in rl:
  238. host,rtims = line.split(' : ')
  239. if ( re.search(r"duplicate", rtims, flags=re.I) ):
  240. #print(host+": "+rtims)
  241. continue
  242. host = host.strip()
  243. rtims = rtims.strip()
  244. #print(host+": "+rtims)
  245. ux_sect = 'ux_' + host_dict[host]
  246. if re.search(r"[\d\.]+ [\d\.]+ [\d\.]+", rtims):
  247. #print(host+" OK")
  248. cnt_ok += 1
  249. set_since(host, ac_timed, set=g_since,unset1=y_since,unset2=r_since)
  250. set_since(host, tnow, set=g_timbar,unset1=y_timbar,unset2=r_timbar)
  251. g_timb = restrict(int(100-(100 * ( (tnow - g_timbar[host]) / bar_duration))+1), 0, 100)
  252. json_bc.append({
  253. "host": host, "status": "OK", "rtt": rtims, "actime": ac_time, "sect": ux_sect, "since": g_since[host], "timbar": g_timb
  254. })
  255. failed_pingc[host] = 0
  256. #BroadcastHandler.broadcast(line)
  257. elif re.search(r"[\d\.]+ [\d\.]+ -", rtims) or re.search(r"[\d\.]+ - [\d\.]+", rtims) or re.search(r"- [\d\.]+ [\d\.]+", rtims):
  258. #print(host+" OK")
  259. cnt_ok += 1
  260. set_since(host, ac_timed, set=g_since,unset1=y_since,unset2=r_since)
  261. set_since(host, tnow, set=g_timbar,unset1=y_timbar,unset2=r_timbar)
  262. g_timb = restrict(int(100-(100 * ( (tnow - g_timbar[host]) / bar_duration))+1), 0, 100)
  263. json_bc.append({
  264. "host": host, "status": "OK", "rtt": rtims, "actime": ac_time, "sect": ux_sect, "since": g_since[host], "timbar": g_timb
  265. })
  266. failed_pingc[host] = 0
  267. #BroadcastHandler.broadcast(line)
  268. elif re.search(r"- - -", rtims):
  269. #print(host+" FAILED")
  270. cnt_fail += 1
  271. set_since(host, ac_timed, set=r_since,unset1=y_since,unset2=g_since)
  272. set_since(host, tnow, set=r_timbar,unset1=y_timbar,unset2=g_timbar)
  273. r_timb = restrict(int(100-(100 * ( (tnow - r_timbar[host]) / bar_duration))+1), 0, 100)
  274. if failed_pingc[host] < int(config["Main"]["FlashThreshold"]):
  275. json_bc.append({
  276. "host": host, "status": "NEWFAIL", "rtt": rtims, "actime": ac_time, "sect": ux_sect, "since": r_since[host], "timbar": r_timb
  277. })
  278. failed_pingc[host] += 1
  279. else:
  280. json_bc.append({
  281. "host": host, "status": "FAILED", "rtt": rtims, "actime": ac_time, "sect": ux_sect, "since": r_since[host], "timbar": r_timb
  282. })
  283. failed_pingc[host] = int(config["Main"]["FlashThreshold"]) + 1
  284. #BroadcastHandler.broadcast(line)
  285. elif re.search(r"[\d\.]+", rtims):
  286. #print(host+" HALB")
  287. cnt_halb += 1
  288. set_since(host, ac_timed, set=y_since,unset1=r_since,unset2=g_since)
  289. set_since(host, tnow, set=y_timbar,unset1=r_timbar,unset2=g_timbar)
  290. y_timb = restrict(int(100-(100 * ( (tnow - y_timbar[host]) / bar_duration))+1), 0, 100)
  291. json_bc.append({
  292. "host": host, "status": "HALB", "rtt": rtims, "actime": ac_time, "sect": ux_sect, "since": y_since[host], "timbar": y_timb
  293. })
  294. failed_pingc[host] = 0
  295. #BroadcastHandler.broadcast(line)
  296. else:
  297. print(host+" ???????????????????")
  298. if args.v: print("STAT(%d hosts, interval:%s ms, period:%s ms): green=%d / yellow=%d / red=%d / ping-round: %d sec" % (anz_hosts, config["Main"]["Fping-interval"], config["Main"]["Fping-period"], cnt_ok, cnt_halb, cnt_fail, int(datetime.now().timestamp()) - int(ac_now.timestamp())) )
  299. #print(g_timbar)
  300. #print(y_timbar)
  301. #print(r_timbar)
  302. #print(json.dumps(json_bc, separators=(',', ':'), ensure_ascii=False))
  303. BroadcastHandler.broadcast(json_bc)
  304. # sleep for Config-LoopInterval
  305. if ( len(BroadcastHandler.clients) >= 1 ):
  306. await asyncio.sleep(loop_interval)
  307. # End while True
  308. class BroadcastHandler(tornado.websocket.WebSocketHandler):
  309. clients = []
  310. def open(self):
  311. o_time = datetime.now().strftime("%d.%m.%y %H:%M")
  312. if not re.search(r"^/websocket$", self.request.uri):
  313. log_msg = '"Bad request uri"'
  314. print("%s: Client %s: WebSocket Open-Error: %s " % (o_time, self.request.remote_ip, log_msg))
  315. self.close(1111)
  316. return
  317. BroadcastHandler.clients.append(self)
  318. print("%s: Client %s: WebSocket opened: %s " % (o_time, self.request.remote_ip, self.request)) # oder "self.request.remote_ip" etc. / oder "self.__dict__"
  319. def on_close(self):
  320. BroadcastHandler.clients.remove(self)
  321. c_time = datetime.now().strftime("%d.%m.%y %H:%M")
  322. if ( self.close_code ):
  323. print("%s: Client %s: WebSocket closed: %s (close_code: %d)" % (c_time, self.request.remote_ip, self.request, self.close_code))
  324. else:
  325. print("%s: Client %s: WebSocket undefined closed." % (c_time, self.request.remote_ip))
  326. @classmethod
  327. def broadcast(cls, message):
  328. errored = []
  329. for client in cls.clients:
  330. #print("Broadcasted: %s" % message)
  331. #print("Broadcasted: %s" % client.request.remote_ip)
  332. try:
  333. client.write_message(json.dumps(message, separators=(',', ':'), ensure_ascii=False))
  334. #print("Broadcasted: %s" % message)
  335. except tornado.websocket.WebSocketClosedError:
  336. print("Error sending Broadcast message: WebSocketClosedError() raised")
  337. errored.append(client)
  338. except tornado.websocket.Exception as e:
  339. print("Error sending message: " + str(e))
  340. errored.append(client)
  341. for conn in errored:
  342. cls.clients.remove(conn)
  343. class WWWSocketHandler(tornado.web.RequestHandler):
  344. #@tornado.gen.coroutine
  345. def prepare(self):
  346. #if self.request.protocol == 'http':
  347. # print("mmmm" + self.request.protocol)
  348. pass
  349. def get(self):
  350. #print(self.request.headers)
  351. #print(self.__dict__)
  352. if not re.search(r"^/$", self.request.uri):
  353. return
  354. html_modal01 = create_html_modal01()
  355. html_tab = create_html_table()
  356. self.write(html_html % (config["Main"]["TitleAdd"], ws_method, self.request.host, html_css, html_tab, html_modal01))
  357. def on_connection_close(self):
  358. print("RequestHandler on_connection_close(): " + str(self.__dict__))
  359. async def test():
  360. while True:
  361. print("test")
  362. await asyncio.sleep(5)
  363. def make_app():
  364. abs_path = os.path.dirname(os.path.abspath(__file__))
  365. web_app = tornado.web.Application([
  366. (r"^/websocket$", BroadcastHandler),
  367. (r"/", WWWSocketHandler),
  368. (r'/css/(.+\.css)', tornado.web.StaticFileHandler, {'path': abs_path + '/'}),
  369. (r'/img/(.+\.gif)', tornado.web.StaticFileHandler, {'path': abs_path + '/'}),
  370. (r'/(favicon.ico)', tornado.web.StaticFileHandler, {'path': abs_path + '/'}),
  371. ],debug=False,websocket_ping_interval=10,websocket_ping_timeout=30)
  372. if ws_method == 'ws':
  373. # http method in use:
  374. print("Serving HTTP and ws.")
  375. print("Listen on "+listen_addr+" Port "+str(listen_port)+" (http://"+listen_addr+":"+str(listen_port)+"/).")
  376. return web_app
  377. # https method in use:
  378. print("Serving HTTPS and wss.")
  379. print("Listen on "+listen_addr+" Port "+str(listen_port)+" (https://"+listen_addr+":"+str(listen_port)+"/).")
  380. http_server = tornado.httpserver.HTTPServer(web_app,
  381. ssl_options = {
  382. "certfile": abs_path + '/' + s_certfile,
  383. "keyfile": abs_path + '/' + s_keyfile,
  384. }
  385. )
  386. return http_server
  387. async def main():
  388. app = make_app()
  389. try:
  390. app.listen(int(listen_port), str(listen_addr))
  391. except OSError as e:
  392. if e.errno == errno.EADDRINUSE:
  393. print("listen(): 'EADDRINUSE': " + str(e.args[1]))
  394. sys.exit(1)
  395. else:
  396. raise
  397. #await asyncio.gather(PingLoop(), return_exceptions=True)
  398. #await asyncio.gather(PingLoop())
  399. await asyncio.gather(FpingLoop())
  400. # ------ Div -------------------------------
  401. def create_html_table():
  402. html_tab = ""
  403. html_tab += '<div id="top_header" class="top_header w3-black">'
  404. html_tab += '<span class="w3-left tbars" style="text-align: left;">V'+VERSION+' by <a href="https://pss.zweiernet.ch" target="_blank">PSS</a><br><span style="font-size:1em;opacity:0.8;background-color:#4e4ec4;cursor:pointer;" onclick="document.getElementById(\'modal_01\').style.display=\'block\'">&nbsp;<span class="w3-text-white w3-monospace"><b>i</b></span> &#x2754; </span></span>'
  405. html_tab += '<span class="w3-center top_head_tit">PingPanel<span class="top_head_tit_pfix"> ' + config["Main"]["TitleAdd"] + '</span></span>'
  406. html_tab += '<span class="w3-right tbars"><span id="cur_actime" style="text-align: right;color: #22ff00;font-weight: bold;"> </span><br><button id="close_button" onclick="socket_close(1000)" class="sbutt">stop</button><br></span>'
  407. html_tab += '</div>\n'
  408. html_tab += '<div class="w3-container">'
  409. for sect in config.sections():
  410. if (sect == "Main"):
  411. continue
  412. html_tab += '<div class="w3-container" style="padding: 0;font-size:14px;">'
  413. sect_x = sect.replace(' ','_')
  414. clickId = 'accord_' + sect_x
  415. html_tab += '<br><div onclick="Accordion(\'' + clickId + '\')" class="w3-container w3-teal" style="padding: 3px 10px;margin-top: 5px;box-shadow: 0 2px 5px 0 #04040424,0 2px 10px 0 #040404c4;letter-spacing: 2px;cursor: pointer;text-shadow: 0 0 0px #fff;">' + sect + '<span id="ux_' + sect_x + '" class="w3-right"></span></div>\n'
  416. html_tab += '<div id="' + clickId + '" class="w3-show">'
  417. for key in config[sect]:
  418. tkey = key.replace('.', '_')
  419. html_tab += '<div class="w3-left si3-dark-grey w3-border w3-border-teal tooltip" style="text-align: center;margin: 1px; min-width: 13ch; padding: 4px min(3px, 1vw) !important;box-shadow:3px 2px 4px #000 !important;">'
  420. html_tab += '<span id="tbar_' + tkey + '" class="time-bar-wrapper"><span class="time-bar"></span></span>'
  421. html_tab += '<span class="w3-card-2 tooltiptext">'+config[sect][key]+'<br><span id="ttip_' + tkey + '" class="ttip_small"></span></span>'
  422. html_tab += '<span id="' + tkey + '" class="ip_num">' + key + '</span><br><span id="stat_' + tkey + '" class="w3-center"></span></div>'
  423. html_tab += '</div>\n</div>\n'
  424. html_tab += '</div>\n'
  425. return html_tab
  426. html_html = '''
  427. <!doctype html>
  428. <html>
  429. <header>
  430. <title>PingPanel %s</title>
  431. <meta name="viewport" content="width=device-width, initial-scale=1">
  432. <meta charset="UTF-8">
  433. <script type="text/javascript">
  434. window.onload = start_websocket();
  435. function start_websocket() {
  436. var ws = new WebSocket("%s://%s/websocket");
  437. ws.onmessage = function (evt) {
  438. //let test = evt.data.replace(/'/g, '"'); // needed " by JSON.parse
  439. //var json_arr = JSON.parse(test);
  440. var json_arr = JSON.parse(evt.data);
  441. var cur_actime = "";
  442. // Colortable counters initialisation
  443. let M_red = new Map();
  444. let M_green = new Map();
  445. let M_yellow = new Map();
  446. for( let i = 0; i < json_arr.length; i++ ) {
  447. if ( M_red.has(json_arr[i].sect) ) {
  448. continue;
  449. } else {
  450. M_red.set(json_arr[i].sect, 0);
  451. M_green.set(json_arr[i].sect, 0);
  452. M_yellow.set(json_arr[i].sect, 0);
  453. }
  454. }
  455. // DOM Updates
  456. for(let i = 0; i < json_arr.length; i++) {
  457. var data_obj = json_arr[i];
  458. cur_actime = data_obj.actime;
  459. var cur_host = data_obj.host;
  460. var cur_status = data_obj.status;
  461. var cur_sect_ux = data_obj.sect;
  462. var cur_rtt = data_obj.rtt;
  463. var cur_since = data_obj.since;
  464. var cur_timbar = data_obj.timbar;
  465. cur_rtt = cur_rtt.replace(/ /g,' \/ ');
  466. var uc_cur_host = cur_host.replace(/\./g,'_');
  467. document.getElementById(uc_cur_host).textContent = cur_host;
  468. var stat_id = 'stat_' + uc_cur_host;
  469. var ttip_id = 'ttip_' + uc_cur_host;
  470. var tbar_id = 'tbar_' + uc_cur_host;
  471. var tbarsel = document.getElementById(tbar_id).querySelector('.time-bar');
  472. if (cur_status == 'OK') {
  473. M_green.set(cur_sect_ux, M_green.get(cur_sect_ux) + 1);
  474. document.getElementById(stat_id).innerHTML = '<img src="/img/green_on.gif">';
  475. document.getElementById(ttip_id).innerHTML = '<b>RTT</b>: '+cur_rtt+'<br><span style="color:#0f0;">since </span>: '+cur_since;
  476. tbarsel.style.width = cur_timbar+'%%';
  477. tbarsel.style.background = '#0f0';
  478. } else if (cur_status == 'FAILED') {
  479. M_red.set(cur_sect_ux, M_red.get(cur_sect_ux) + 1);
  480. document.getElementById(stat_id).innerHTML = '<img src="/img/red_on.gif">';
  481. document.getElementById(ttip_id).innerHTML = '<b>RTT</b>: '+cur_rtt+'<br><span style="color:red;">since </span>: '+cur_since;
  482. tbarsel.style.width = cur_timbar+'%%';
  483. tbarsel.style.background = 'red';
  484. } else if (cur_status == 'NEWFAIL') {
  485. M_red.set(cur_sect_ux, M_red.get(cur_sect_ux) + 1);
  486. document.getElementById(stat_id).innerHTML = '<img src="/img/red_anim.gif">';
  487. document.getElementById(ttip_id).innerHTML = '<b>RTT</b>: '+cur_rtt+'<br><span style="color:red;">since </span>: '+cur_since;
  488. tbarsel.style.width = cur_timbar+'%%';
  489. tbarsel.style.background = 'red';
  490. } else {
  491. M_yellow.set(cur_sect_ux, M_yellow.get(cur_sect_ux) + 1);
  492. document.getElementById(stat_id).innerHTML = '<img src="/img/yellow_on.gif">';
  493. document.getElementById(ttip_id).innerHTML = '<b>RTT</b>: '+cur_rtt+'<br><span style="color:yellow;">since </span>: '+cur_since;
  494. tbarsel.style.width = cur_timbar+'%%';
  495. tbarsel.style.background = 'yellow';
  496. }
  497. } // End for
  498. document.getElementById('cur_actime').innerHTML = cur_actime;
  499. //document.querySelector('.time-bar').style.width = "20px";
  500. for (let [sect, val] of M_red) {
  501. let tmp_r = '<span class="unreach_r">';
  502. tmp_r += (M_red.get(sect) == 0) ? '<span class="red_r opaq_r">'+M_red.get(sect).toString().padStart(3, " ")+'</span> | '
  503. : '<span class="red_r">'+M_red.get(sect).toString().padStart(3, " ")+'</span> | ';
  504. tmp_r += (M_yellow.get(sect) == 0) ? '<span class="yellow_r opaq_r">'+M_yellow.get(sect).toString().padStart(3, " ")+'</span> | '
  505. : '<span class="yellow_r">'+M_yellow.get(sect).toString().padStart(3, " ")+'</span> | ';
  506. tmp_r += (M_green.get(sect) == 0) ? '<span class="green_r opaq_r">'+M_green.get(sect).toString().padStart(3, " ")+'</span>'
  507. : '<span class="green_r">'+M_green.get(sect).toString().padStart(3, " ")+'</span>';
  508. tmp_r += '</span>';
  509. document.getElementById(sect).innerHTML = tmp_r;
  510. }
  511. };
  512. ws.onclose = function(evt) {
  513. //alert("WebSocket closed.");
  514. document.getElementById('cur_actime').innerHTML = '<span style="color:red;">stopped</span>';
  515. document.getElementById('close_button').style.opacity = "0.3";
  516. //document.getElementById('modal_alert_text').style.display='block';
  517. //document.getElementById('modal_alert_text').innerHTML = 'WebSocket closed.';
  518. };
  519. ws.onerror = function(evt) {
  520. alert("WebSocket closed due to an error.");
  521. document.getElementById('cur_actime').innerHTML = '<span style="color:red;">Error</span>';
  522. document.getElementById('close_button').style.opacity = "0.3";
  523. };
  524. socket_close = (code) => {
  525. document.getElementById('close_button').style.opacity = "0.3";
  526. ws.close(code);
  527. };
  528. };
  529. function Accordion(id) {
  530. var x = document.getElementById(id);
  531. if (x.className.indexOf("w3-show") == -1) {
  532. x.className = "w3-show";
  533. } else {
  534. x.className = x.className.replace("w3-show", "w3-hide");
  535. }
  536. };
  537. </script>
  538. <script>
  539. // modal handling
  540. var modal = document.getElementById('modal_01');
  541. // When the user clicks anywhere outside of the modal, close it
  542. window.onclick = function(event) {
  543. if (event.target == modal) {
  544. modal.style.display = "none";
  545. }
  546. }
  547. </script>
  548. <link rel="stylesheet" href="/css/w3.css">
  549. <style>
  550. %s
  551. </style>
  552. </header>
  553. <body>
  554. <div id="main">%s</div>\n
  555. <p><br></p>
  556. <div id="modal_01" class="w3-modal modal_01" onclick="this.style.display='none'">
  557. <div class="w3-modal-content modal_01_text">
  558. <div class="w3-container">
  559. <h4 style="font-variant:small-caps;letter-spacing:3px;">PingPanel</h4>
  560. %s
  561. </div>
  562. </div>
  563. </div>
  564. </body>
  565. </html>
  566. '''
  567. html_css = '''
  568. body {
  569. background: #333;
  570. font-family: Verdana, Arial, sans-serif;
  571. color: #ddd;
  572. line-height: 110%;
  573. font-size: 13px;
  574. }
  575. .red { color: red; }
  576. .green { color: #0f0; }
  577. .yellow { color: yellow; }
  578. .red_r { color: red; white-space: pre; text-align: right; font-family: monospace; width: 2em; display: inline-block;}
  579. .green_r { color: #0f0; white-space: pre; text-align: right;; font-family: monospace; width: 2em; display: inline-block;}
  580. .yellow_r { color: yellow; white-space: pre; text-align: right;; font-family: monospace; width: 2em; display: inline-block;}
  581. .ip_num { font-size: 80%; }
  582. .opaq_r { opacity: 0.2; }
  583. .si3-dark-grey{color:#fff!important;background-color:#626262!important;text-shadow: 0 0 0px #fff, 0 0 1px #000;}
  584. .top_header {
  585. text-align: center;
  586. padding: 5px 10px; height: 43px;
  587. box-shadow: 0px 1px 9px 1px #c3d0cd78 !important;
  588. position: sticky;
  589. z-index: 100;
  590. top: 0px;
  591. }
  592. .tbars {
  593. color: #aaa;
  594. font-size: 11px;
  595. font-stretch: semi-expanded;
  596. }
  597. .unreach_r {
  598. background-color:#313131;
  599. padding-left:5px;
  600. padding-right:5px;
  601. letter-spacing: 0px;
  602. border-radius: 2px;
  603. font-weight: bold;
  604. font-size: 12px;
  605. }
  606. .modal_01 {
  607. }
  608. .modal_01_text {
  609. background-color: #000;
  610. color: #f0f0ca;
  611. line-height: 120%;
  612. font-size: 1.1em;
  613. }
  614. .sbutt {
  615. border: none;
  616. display: inline-block;
  617. vertical-align: middle;
  618. overflow: hidden;
  619. text-decoration: none;
  620. text-align: center;
  621. cursor: pointer;
  622. white-space: nowrap;
  623. border-radius: 4px;
  624. padding: 0px 8px !important;
  625. background-color: #9b9b9b !important;
  626. color: #000 !important;
  627. }
  628. .top_head_tit {
  629. font-size: 22px;
  630. font-variant: small-caps;
  631. letter-spacing: 4px;
  632. top: 0.3em;
  633. position: sticky;
  634. }
  635. .top_head_tit_pfix {
  636. font-size: 0.5em;
  637. font-variant: small-caps;
  638. letter-spacing: 0px;
  639. }
  640. .ttip_small {
  641. font-size: 64%;
  642. text-align: center;
  643. padding-top: 6px;
  644. }
  645. .tooltip {
  646. position: relative;
  647. display: inline-block;
  648. border-bottom: 1px dotted black;
  649. }
  650. .tooltip .tooltiptext {
  651. font-family: Verdana, 'PT Sans Caption', Arial, sans-serif;
  652. visibility: hidden;
  653. width: 170px;
  654. background-color: black;
  655. color: #fff;
  656. text-align: center;
  657. font-size: 17px;
  658. border-radius: 6px;
  659. padding: 5px 5px;
  660. position: absolute;
  661. z-index: 1;
  662. top: 120%;
  663. left: 50%;
  664. margin-left: -60px;
  665. line-height: 1.1em;
  666. text-shadow: 0 0 0 #a8a8a8 !important;
  667. }
  668. .tooltip .tooltiptext::after {
  669. content: "";
  670. position: absolute;
  671. bottom: 100%;
  672. left: 50%;
  673. margin-left: -5px;
  674. border-width: 5px;
  675. border-style: solid;
  676. border-color: transparent transparent black transparent;
  677. }
  678. .tooltip:hover .tooltiptext {
  679. visibility: visible;
  680. }
  681. .sticky {
  682. position: fixed;
  683. top: 0;
  684. width: 100%;
  685. }
  686. .sticky + .content {
  687. padding-top: 50px;
  688. }
  689. .time-bar-wrapper {
  690. width: 100%;
  691. height:2px;
  692. position:absolute;
  693. top:0;
  694. left:0;
  695. background-color: inherit;
  696. }
  697. .time-bar {
  698. width:0;
  699. height: inherit;
  700. background-color: yellow;
  701. display: block;
  702. }
  703. '''
  704. def create_html_modal01():
  705. html_modal = '''
  706. <p style="font-size:1.2em;font-variant:small-caps;letter-spacing:3px;font-style:italic;text-decoration:underline">Info</p>
  707. Config File:
  708. ''' + konfig_file + '''<br>
  709. Total number of Devices: _NUM_HOSTS_ <br>
  710. LoopInterval:
  711. ''' + str(loop_interval) + ''' s<br>
  712. FlashThreshold:
  713. ''' + str(config["Main"]["FlashThreshold"]) + ''' rounds<br>
  714. DurationBarSpan:
  715. ''' + str(bar_duration) + ''' seconds<br>
  716. fping:
  717. ''' + fping_parameter[2].replace('--','') + '''<br>
  718. fping:
  719. ''' + fping_parameter[3].replace('--','') + ''' ms<br>
  720. fping:
  721. ''' + fping_parameter[4].replace('--','') + ''' ms<br>
  722. fping:
  723. ''' + fping_parameter[5].replace('--','') + ''' bytes<br>
  724. <br>
  725. <p style="font-size:1.2em;font-variant:small-caps;letter-spacing:3px;font-style:italic;text-decoration:underline">Help</p>
  726. The PingPanel board provides the header and the configured, expandable and collapsible sections with the corresponding devices to be monitored.<br>
  727. Among other things, the header contains the current time (<span class="green">green</span>) of the last ping run
  728. or the status "stopped" (<span class="red">red</span>) if the connection to the server has been lost or the "Stop" button below has been pressed.<br>
  729. Pressing the "Stop" button terminates the connection to the server and, if no other client is active, the server pauses the ping rounds.<p>
  730. The blue-green <span class="w3-teal">section bars</span> contain the name of the section and consolidated information about the status of the devices in this section.<br>
  731. The section bars can be expanded and collapsed by clicking or tapping on them.<br>
  732. Consolidated status information <img src="/img/stat-inf.gif"> is divided into <span class="red">failed</span>,
  733. <span class="yellow">partially answered</span> and <span class="green">answered</span> ping requests.<br>
  734. Each monitored device displays its IP address and status. By hovering over or tapping on it its comment
  735. is displayed and the three ping RTT's of each ping round is reported.<p>
  736. A <span class="red">flashing red</span> status light indicates that the device has recently not responded during 'FlashThreshold' ping rounds.<br>
  737. A <span class="red">red</span> status light indicates that the device has not answered any of the three pings.<br>
  738. A <span class="yellow">yellow</span> status light indicates that the device has answered one of three pings.<br>
  739. A <span class="green">green</span> status light indicates that the device has answered to all three or at least two pings.<br>
  740. <p></p>
  741. <p> </p>
  742. '''
  743. html_modal = html_modal.replace('_NUM_HOSTS_', str(anz_hosts))
  744. return html_modal
  745. # -------- MAIN ------------------------------
  746. if __name__ == "__main__":
  747. print("PingPanel v"+VERSION+" by PSS -- started at " + datetime.now().strftime("%F %H:%M:%S"))
  748. # Commandline parsing
  749. parser = argparse.ArgumentParser(description='PingPanel V'+VERSION+"\n2024 by sigi",
  750. formatter_class=argparse.RawDescriptionHelpFormatter)
  751. parser.add_argument('-c', help="Alternative configuration file (standard: config.cfg)", type=str, metavar='config-file')
  752. parser.add_argument('-v', help="Verbose mode", action="store_true")
  753. args = parser.parse_args()
  754. if args.c:
  755. konfig_file = args.c
  756. print("Configfile: " + konfig_file)
  757. # Config
  758. read_config()
  759. # create the app
  760. asyncio.run(main())
  761. # ------