PingPanel.py 28 KB

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