|
@@ -1,793 +0,0 @@
|
|
-#!/usr/bin/env python3
|
|
|
|
-
|
|
|
|
-# Copyright 2024 by Siegrist(SystemLoesungen) <PSS@ZweierNet.ch>
|
|
|
|
-
|
|
|
|
-# This program is free software: you can redistribute it and/or modify
|
|
|
|
-# it under the terms of the GNU General Public License as published by
|
|
|
|
-# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
-# (at your option) any later version.
|
|
|
|
-#
|
|
|
|
-# This program is distributed in the hope that it will be useful,
|
|
|
|
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
-# GNU General Public License for more details.
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-#import datetime
|
|
|
|
-from datetime import datetime
|
|
|
|
-import os
|
|
|
|
-import errno
|
|
|
|
-import configparser
|
|
|
|
-import time
|
|
|
|
-import signal
|
|
|
|
-import sys
|
|
|
|
-import subprocess
|
|
|
|
-import re
|
|
|
|
-import argparse
|
|
|
|
-from threading import Thread
|
|
|
|
-import json
|
|
|
|
-
|
|
|
|
-import tornado.ioloop
|
|
|
|
-import tornado.web
|
|
|
|
-import tornado.websocket
|
|
|
|
-from tornado.gen import sleep
|
|
|
|
-import asyncio
|
|
|
|
-
|
|
|
|
-VERSION = '0.9.9'
|
|
|
|
-
|
|
|
|
-konfig_file = 'config.cfg'
|
|
|
|
-s_certfile = 'server.crt'
|
|
|
|
-s_keyfile = 'server.key'
|
|
|
|
-ws_method = 'ws'
|
|
|
|
-host_dict = dict()
|
|
|
|
-failed_pingc = dict()
|
|
|
|
-loop_interval = 3
|
|
|
|
-listen_port = 8888
|
|
|
|
-listen_addr = '0.0.0.0'
|
|
|
|
-anz_hosts = 0
|
|
|
|
-
|
|
|
|
-fping_parameter = []
|
|
|
|
-fping_cmd = 'fping'
|
|
|
|
-
|
|
|
|
-def signal_handler(signal, frame):
|
|
|
|
- print('\nCtrl+C, keyboardInterrupt detected!')
|
|
|
|
- sys.exit(0)
|
|
|
|
-signal.signal(signal.SIGINT, signal_handler)
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-config = configparser.ConfigParser(delimiters=('='), comment_prefixes=('#'), allow_no_value=False, strict=True, empty_lines_in_values=False)
|
|
|
|
-def read_config():
|
|
|
|
- try:
|
|
|
|
- if config.read([konfig_file]) != []:
|
|
|
|
- pass
|
|
|
|
- else:
|
|
|
|
- print("Configfile '" + konfig_file + "' not found!")
|
|
|
|
- sys.exit()
|
|
|
|
- except configparser.DuplicateSectionError as e:
|
|
|
|
- print("Error: Duplicate Section '%s' not allowed." % str(e.section))
|
|
|
|
- sys.exit()
|
|
|
|
- except configparser.DuplicateOptionError as e:
|
|
|
|
- print("Error: Duplicate Option '%s' in Section '%s' not allowed." % ( str(e.option), str(e.section) ))
|
|
|
|
- sys.exit()
|
|
|
|
- except configparser.Error as e:
|
|
|
|
- print("Error in Configfile: %s." % str(e))
|
|
|
|
- sys.exit()
|
|
|
|
-
|
|
|
|
- #print(config.sections())
|
|
|
|
- global anz_hosts
|
|
|
|
- for sect in config.sections():
|
|
|
|
- for key in config[sect]:
|
|
|
|
- if (sect == "Main"):
|
|
|
|
- try:
|
|
|
|
- global loop_interval
|
|
|
|
- loop_interval = int(config["Main"]["LoopInterval"])
|
|
|
|
- except KeyError:
|
|
|
|
- pass
|
|
|
|
- try:
|
|
|
|
- global listen_port
|
|
|
|
- listen_port = int(config["Main"]["ListenPort"])
|
|
|
|
- except KeyError:
|
|
|
|
- pass
|
|
|
|
- try:
|
|
|
|
- global listen_addr
|
|
|
|
- listen_addr = str(config["Main"]["ListenAddr"])
|
|
|
|
- except KeyError:
|
|
|
|
- pass
|
|
|
|
- try:
|
|
|
|
- title_add = str(config["Main"]["TitleAdd"])
|
|
|
|
- except KeyError:
|
|
|
|
- config["Main"]["TitleAdd"] = ''
|
|
|
|
- try:
|
|
|
|
- global ws_method
|
|
|
|
- if ( str(config["Main"]["UseHTTPS"]) ):
|
|
|
|
- ws_method = 'wss'
|
|
|
|
- s_certfile = str(config["Main"]["CertFile"])
|
|
|
|
- s_keyfile = str(config["Main"]["KeyFile"])
|
|
|
|
- apath = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
- if not file_exists(s_certfile):
|
|
|
|
- print("CertFile not readable or does not exist: %s" % apath + '/' + s_certfile)
|
|
|
|
- sys.exit()
|
|
|
|
- if not file_exists(s_keyfile):
|
|
|
|
- print("KeyFile not readable or does not exist: %s" % apath + '/' + s_keyfile)
|
|
|
|
- sys.exit()
|
|
|
|
- except KeyError:
|
|
|
|
- pass
|
|
|
|
- else:
|
|
|
|
- #host_dict[key] = config[sect][key]
|
|
|
|
- host_dict[key] = sect.replace(' ','_')
|
|
|
|
- failed_pingc[key] = 0
|
|
|
|
- anz_hosts += 1
|
|
|
|
- if args.v: print(sect + ": " + key + " = " + config[sect][key])
|
|
|
|
- print("Pinging " + str(anz_hosts) + " Devices in total.")
|
|
|
|
-
|
|
|
|
-def cmd_exists(cmd):
|
|
|
|
- path = os.environ["PATH"].split(os.pathsep)
|
|
|
|
-
|
|
|
|
- for prefix in path:
|
|
|
|
- filename = os.path.join(prefix, cmd)
|
|
|
|
- executable = os.access(filename, os.X_OK)
|
|
|
|
- is_not_directory = os.path.isfile(filename)
|
|
|
|
- if executable and is_not_directory:
|
|
|
|
- return True
|
|
|
|
- return False
|
|
|
|
-
|
|
|
|
-def file_exists(file):
|
|
|
|
- filename = os.path.dirname(os.path.abspath(__file__)) + '/' + file
|
|
|
|
- if os.access(filename, os.R_OK):
|
|
|
|
- return True
|
|
|
|
- return False
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-def create_fpingparam():
|
|
|
|
- global fping_parameter
|
|
|
|
- global fping_cmd
|
|
|
|
-
|
|
|
|
- try:
|
|
|
|
- fping_parameter.append(config["Main"]["fpingCommand"])
|
|
|
|
- fping_cmd = config["Main"]["fpingCommand"]
|
|
|
|
- except KeyError:
|
|
|
|
- fping_parameter.append(fping_cmd)
|
|
|
|
-
|
|
|
|
- fping_parameter += ['--quiet', '--vcount=3']
|
|
|
|
-
|
|
|
|
- try:
|
|
|
|
- fping_parameter.append('--interval=' + config["Main"]["Fping-interval"])
|
|
|
|
- except KeyError:
|
|
|
|
- fping_parameter.append('--interval=1')
|
|
|
|
- try:
|
|
|
|
- fping_parameter.append('--period=' + config["Main"]["Fping-period"])
|
|
|
|
- except KeyError:
|
|
|
|
- fping_parameter.append('--period=100')
|
|
|
|
- try:
|
|
|
|
- fping_parameter.append('--size=' + config["Main"]["Fping-size"])
|
|
|
|
- except KeyError:
|
|
|
|
- pass
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-def ping(ip, packets=1, timeout=2):
|
|
|
|
- comd = ['ping', '-c', str(packets), '-w', str(timeout), ip]
|
|
|
|
- res = subprocess.run(comd, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
|
|
- return res.returncode == 0
|
|
|
|
-
|
|
|
|
-def fping(ips):
|
|
|
|
- #comd = ['fping', '--quiet', '--interval=2', '--vcount=3', '--period=100'] + ips
|
|
|
|
- comd = fping_parameter + ips
|
|
|
|
-
|
|
|
|
- res = subprocess.run(comd, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) #, universal_newlines = True)
|
|
|
|
- if args.v:
|
|
|
|
- print("fping finished: " + datetime.now().strftime("%H:%M:%S"))
|
|
|
|
- if res.returncode == 2:
|
|
|
|
- print("fping: any IP addresses were not found !")
|
|
|
|
- elif res.returncode == 3:
|
|
|
|
- print("fping: invalid command line arguments !")
|
|
|
|
- elif res.returncode == 4:
|
|
|
|
- print("fping: system call failure !")
|
|
|
|
-
|
|
|
|
- #print("RET:"+str(res.returncode))
|
|
|
|
- return res.stdout.decode("utf-8")
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-# Pinging Loop
|
|
|
|
-async def PingLoop():
|
|
|
|
- print("PingLoop gestartet.")
|
|
|
|
- # sleep for a moment
|
|
|
|
- while True:
|
|
|
|
- await asyncio.sleep(3)
|
|
|
|
- # Sinnlos wenn niemend zuschaut:
|
|
|
|
- if ( len(BroadcastHandler.clients) < 1 ):
|
|
|
|
- continue
|
|
|
|
-
|
|
|
|
- for host, name in host_dict.items():
|
|
|
|
- #print("ping host: " + host)
|
|
|
|
- if ping(host,1,2):
|
|
|
|
- line = {
|
|
|
|
- "host": host, "status": "OK"
|
|
|
|
- }
|
|
|
|
- BroadcastHandler.broadcast(line)
|
|
|
|
- else:
|
|
|
|
- line = {
|
|
|
|
- "host": host, "status": "FAILED"
|
|
|
|
- }
|
|
|
|
- BroadcastHandler.broadcast(line)
|
|
|
|
-
|
|
|
|
- # End while True
|
|
|
|
-
|
|
|
|
-# Fping Loop
|
|
|
|
-async def FpingLoop():
|
|
|
|
- print("FpingLoop ready.")
|
|
|
|
- ip_list = list(host_dict.keys())
|
|
|
|
- create_fpingparam()
|
|
|
|
-
|
|
|
|
- # check for ping command
|
|
|
|
- if not cmd_exists(fping_cmd):
|
|
|
|
- print("Exit with Error: fping command not found or not executable: '%s'" % fping_cmd)
|
|
|
|
- sys.exit(1)
|
|
|
|
-
|
|
|
|
- if args.v:
|
|
|
|
- tmp_cmd = [] + fping_parameter + ip_list
|
|
|
|
- comd_out = ' '.join(tmp_cmd)
|
|
|
|
- comd_out.replace("'", "")
|
|
|
|
- print('ping command used: "' + comd_out + '"')
|
|
|
|
-
|
|
|
|
- # The Loop
|
|
|
|
- while True:
|
|
|
|
-
|
|
|
|
- # Sinnlos wenn niemend zuschaut:
|
|
|
|
- if ( len(BroadcastHandler.clients) < 1 ):
|
|
|
|
- await asyncio.sleep(3)
|
|
|
|
- continue
|
|
|
|
-
|
|
|
|
- ac_time = datetime.now().strftime("%H:%M:%S")
|
|
|
|
- if args.v: print("call fping: " + ac_time)
|
|
|
|
- ret = fping(ip_list)
|
|
|
|
- rl = ret.splitlines()
|
|
|
|
- json_bc = []
|
|
|
|
- for line in rl:
|
|
|
|
- host,rtims = line.split(' : ')
|
|
|
|
- if ( re.search(r"duplicate", rtims, flags=re.I) ):
|
|
|
|
- #print(host+": "+rtims)
|
|
|
|
- continue
|
|
|
|
- host = host.strip()
|
|
|
|
- rtims = rtims.strip()
|
|
|
|
- ux_sect = 'ux_' + host_dict[host]
|
|
|
|
-
|
|
|
|
- if re.search(r"[\d\.]+ [\d\.]+ [\d\.]+", rtims):
|
|
|
|
- #print(host+" OK")
|
|
|
|
- json_bc.append({
|
|
|
|
- "host": host, "status": "OK", "rtt": rtims, "actime": ac_time, "sect": ux_sect
|
|
|
|
- })
|
|
|
|
- failed_pingc[host] = 0
|
|
|
|
- #BroadcastHandler.broadcast(line)
|
|
|
|
- elif re.search(r"[\d\.]+ [\d\.]+ -", rtims) or re.search(r"[\d\.]+ - [\d\.]+", rtims) or re.search(r"- [\d\.]+ [\d\.]+", rtims):
|
|
|
|
- #print(host+" OK")
|
|
|
|
- json_bc.append({
|
|
|
|
- "host": host, "status": "OK", "rtt": rtims, "actime": ac_time, "sect": ux_sect
|
|
|
|
- })
|
|
|
|
- failed_pingc[host] = 0
|
|
|
|
- #BroadcastHandler.broadcast(line)
|
|
|
|
- elif re.search(r"- - -", rtims):
|
|
|
|
- #print(host+" FAILED")
|
|
|
|
- if failed_pingc[host] < int(config["Main"]["FlashThreshold"]):
|
|
|
|
- json_bc.append({
|
|
|
|
- "host": host, "status": "NEWFAIL", "rtt": rtims, "actime": ac_time, "sect": ux_sect
|
|
|
|
- })
|
|
|
|
- failed_pingc[host] += 1
|
|
|
|
- else:
|
|
|
|
- json_bc.append({
|
|
|
|
- "host": host, "status": "FAILED", "rtt": rtims, "actime": ac_time, "sect": ux_sect
|
|
|
|
- })
|
|
|
|
- failed_pingc[host] = int(config["Main"]["FlashThreshold"]) + 1
|
|
|
|
- #BroadcastHandler.broadcast(line)
|
|
|
|
- elif re.search(r"[\d\.]+", rtims):
|
|
|
|
- #print(host+" HALB")
|
|
|
|
- json_bc.append({
|
|
|
|
- "host": host, "status": "HALB", "rtt": rtims, "actime": ac_time, "sect": ux_sect
|
|
|
|
- })
|
|
|
|
- failed_pingc[host] = 0
|
|
|
|
- #BroadcastHandler.broadcast(line)
|
|
|
|
- else:
|
|
|
|
- print(host+" ???????????????????")
|
|
|
|
-
|
|
|
|
- #print(json.dumps(json_bc, separators=(',', ':'), ensure_ascii=False))
|
|
|
|
- BroadcastHandler.broadcast(json_bc)
|
|
|
|
-
|
|
|
|
- # sleep for Config-LoopInterval
|
|
|
|
- if ( len(BroadcastHandler.clients) >= 1 ):
|
|
|
|
- await asyncio.sleep(loop_interval)
|
|
|
|
- # End while True
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-class BroadcastHandler(tornado.websocket.WebSocketHandler):
|
|
|
|
- clients = []
|
|
|
|
-
|
|
|
|
- def open(self):
|
|
|
|
- o_time = datetime.now().strftime("%d.%m.%y %H:%M")
|
|
|
|
- if not re.search(r"^/websocket$", self.request.uri):
|
|
|
|
- log_msg = '"Bad request uri"'
|
|
|
|
- print("%s: Client %s: WebSocket Open-Error: %s " % (o_time, self.request.remote_ip, log_msg))
|
|
|
|
- self.close(1111)
|
|
|
|
- return
|
|
|
|
- BroadcastHandler.clients.append(self)
|
|
|
|
- print("%s: Client %s: WebSocket opened: %s " % (o_time, self.request.remote_ip, self.request)) # oder "self.request.remote_ip" etc. / oder "self.__dict__"
|
|
|
|
-
|
|
|
|
- def on_close(self):
|
|
|
|
- BroadcastHandler.clients.remove(self)
|
|
|
|
- c_time = datetime.now().strftime("%d.%m.%y %H:%M")
|
|
|
|
- if ( self.close_code ):
|
|
|
|
- print("%s: Client %s: WebSocket closed: %s (close_code: %d)" % (c_time, self.request.remote_ip, self.request, self.close_code))
|
|
|
|
- else:
|
|
|
|
- print("%s: Client %s: WebSocket undefined closed." % (c_time, self.request.remote_ip))
|
|
|
|
-
|
|
|
|
-
|
|
|
|
- @classmethod
|
|
|
|
- def broadcast(cls, message):
|
|
|
|
- errored = []
|
|
|
|
- for client in cls.clients:
|
|
|
|
- #print("Broadcasted: %s" % message)
|
|
|
|
- #print("Broadcasted: %s" % client.request.remote_ip)
|
|
|
|
- try:
|
|
|
|
- client.write_message(json.dumps(message, separators=(',', ':'), ensure_ascii=False))
|
|
|
|
- #print("Broadcasted: %s" % message)
|
|
|
|
- except tornado.websocket.WebSocketClosedError:
|
|
|
|
- print("Error sending Broadcast message: WebSocketClosedError() raised")
|
|
|
|
- errored.append(client)
|
|
|
|
- except tornado.websocket.Exception as e:
|
|
|
|
- print("Error sending message: " + str(e))
|
|
|
|
- errored.append(client)
|
|
|
|
-
|
|
|
|
- for conn in errored:
|
|
|
|
- cls.clients.remove(conn)
|
|
|
|
-
|
|
|
|
-class WWWSocketHandler(tornado.web.RequestHandler):
|
|
|
|
- #@tornado.gen.coroutine
|
|
|
|
- def prepare(self):
|
|
|
|
- #if self.request.protocol == 'http':
|
|
|
|
- # print("mmmm" + self.request.protocol)
|
|
|
|
- pass
|
|
|
|
-
|
|
|
|
- def get(self):
|
|
|
|
- #print(self.request.headers)
|
|
|
|
- #print(self.__dict__)
|
|
|
|
- if not re.search(r"^/$", self.request.uri):
|
|
|
|
- return
|
|
|
|
- html_modal01 = create_html_modal01()
|
|
|
|
- html_tab = create_html_table()
|
|
|
|
- self.write(html_html % (config["Main"]["TitleAdd"], ws_method, self.request.host, html_css, html_tab, html_modal01))
|
|
|
|
- def on_connection_close(self):
|
|
|
|
- print("RequestHandler on_connection_close(): " + str(self.__dict__))
|
|
|
|
-
|
|
|
|
-async def test():
|
|
|
|
- while True:
|
|
|
|
- print("test")
|
|
|
|
- await asyncio.sleep(5)
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-def make_app():
|
|
|
|
- abs_path = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
- web_app = tornado.web.Application([
|
|
|
|
- (r"^/websocket$", BroadcastHandler),
|
|
|
|
- (r"/", WWWSocketHandler),
|
|
|
|
- (r'/css/(.+\.css)', tornado.web.StaticFileHandler, {'path': abs_path + '/'}),
|
|
|
|
- (r'/img/(.+\.gif)', tornado.web.StaticFileHandler, {'path': abs_path + '/'}),
|
|
|
|
- (r'/(favicon.ico)', tornado.web.StaticFileHandler, {'path': abs_path + '/'}),
|
|
|
|
- ],debug=False,websocket_ping_interval=10,websocket_ping_timeout=30)
|
|
|
|
-
|
|
|
|
- if ws_method == 'ws':
|
|
|
|
- # http method in use:
|
|
|
|
- print("Serving HTTP and ws.")
|
|
|
|
- print("Listen on "+listen_addr+" Port "+str(listen_port)+" (http://"+listen_addr+":"+str(listen_port)+"/).")
|
|
|
|
- return web_app
|
|
|
|
-
|
|
|
|
- # https method in use:
|
|
|
|
- print("Serving HTTPS and wss.")
|
|
|
|
- print("Listen on "+listen_addr+" Port "+str(listen_port)+" (https://"+listen_addr+":"+str(listen_port)+"/).")
|
|
|
|
- http_server = tornado.httpserver.HTTPServer(web_app,
|
|
|
|
- ssl_options = {
|
|
|
|
- "certfile": abs_path + '/' + s_certfile,
|
|
|
|
- "keyfile": abs_path + '/' + s_keyfile,
|
|
|
|
- }
|
|
|
|
- )
|
|
|
|
-
|
|
|
|
- return http_server
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-async def main():
|
|
|
|
- app = make_app()
|
|
|
|
- try:
|
|
|
|
- app.listen(int(listen_port), str(listen_addr))
|
|
|
|
- except OSError as e:
|
|
|
|
- if e.errno == errno.EADDRINUSE:
|
|
|
|
- print("listen(): 'EADDRINUSE': " + str(e.args[1]))
|
|
|
|
- sys.exit(1)
|
|
|
|
- else:
|
|
|
|
- raise
|
|
|
|
-
|
|
|
|
- #await asyncio.gather(PingLoop(), return_exceptions=True)
|
|
|
|
-
|
|
|
|
- #await asyncio.gather(PingLoop())
|
|
|
|
- await asyncio.gather(FpingLoop())
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-# ------ Div -------------------------------
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-def create_html_table():
|
|
|
|
- html_tab = ""
|
|
|
|
- html_tab += '<div id="top_header" class="top_header w3-black">'
|
|
|
|
- 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\'"> <span class="w3-text-white w3-monospace"><b>i</b></span> ❔ </span></span>'
|
|
|
|
- html_tab += '<span class="w3-center top_head_tit">PingPanel<span class="top_head_tit_pfix"> ' + config["Main"]["TitleAdd"] + '</span></span>'
|
|
|
|
- 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>'
|
|
|
|
- html_tab += '</div>\n'
|
|
|
|
- html_tab += '<div class="w3-container">'
|
|
|
|
- for sect in config.sections():
|
|
|
|
- if (sect == "Main"):
|
|
|
|
- continue
|
|
|
|
- html_tab += '<div class="w3-container" style="padding: 0;font-size:14px;">'
|
|
|
|
- sect_x = sect.replace(' ','_')
|
|
|
|
- clickId = 'accord_' + sect_x
|
|
|
|
- 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'
|
|
|
|
- html_tab += '<div id="' + clickId + '" class="w3-show">'
|
|
|
|
- for key in config[sect]:
|
|
|
|
- tkey = key.replace('.', '_')
|
|
|
|
- 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;">'
|
|
|
|
- html_tab += '<span class="w3-card-2 tooltiptext">'+config[sect][key]+'<br><span id="ttip_' + tkey + '" class="ttip_small"></span></span>'
|
|
|
|
- html_tab += '<span id="' + tkey + '" class="ip_num">' + key + '</span><br><span id="stat_' + tkey + '" class="w3-center"></span></div>'
|
|
|
|
- html_tab += '</div>\n</div>\n'
|
|
|
|
- html_tab += '</div>\n'
|
|
|
|
-
|
|
|
|
-
|
|
|
|
- return html_tab
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-html_html = '''
|
|
|
|
-<!doctype html>
|
|
|
|
-<html>
|
|
|
|
- <header>
|
|
|
|
- <title>PingPanel %s</title>
|
|
|
|
- <meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
|
- <meta charset="UTF-8">
|
|
|
|
- <script type="text/javascript">
|
|
|
|
- window.onload = start_websocket();
|
|
|
|
- function start_websocket() {
|
|
|
|
- var ws = new WebSocket("%s://%s/websocket");
|
|
|
|
- ws.onmessage = function (evt) {
|
|
|
|
- //let test = evt.data.replace(/'/g, '"'); // needed " by JSON.parse
|
|
|
|
- //var json_arr = JSON.parse(test);
|
|
|
|
- var json_arr = JSON.parse(evt.data);
|
|
|
|
- var cur_actime = "";
|
|
|
|
-
|
|
|
|
- // Colortable counters initialisation
|
|
|
|
- let M_red = new Map();
|
|
|
|
- let M_green = new Map();
|
|
|
|
- let M_yellow = new Map();
|
|
|
|
- for( let i = 0; i < json_arr.length; i++ ) {
|
|
|
|
- if ( M_red.has(json_arr[i].sect) ) {
|
|
|
|
- continue;
|
|
|
|
- } else {
|
|
|
|
- M_red.set(json_arr[i].sect, 0);
|
|
|
|
- M_green.set(json_arr[i].sect, 0);
|
|
|
|
- M_yellow.set(json_arr[i].sect, 0);
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- // DOM Updates
|
|
|
|
- for(let i = 0; i < json_arr.length; i++) {
|
|
|
|
- var data_obj = json_arr[i];
|
|
|
|
- cur_actime = data_obj.actime;
|
|
|
|
- var cur_host = data_obj.host;
|
|
|
|
- var cur_status = data_obj.status;
|
|
|
|
- var cur_sect_ux = data_obj.sect;
|
|
|
|
- var cur_rtt = data_obj.rtt;
|
|
|
|
- cur_rtt = cur_rtt.replace(/ /g,' \/ ');
|
|
|
|
- var uc_cur_host = cur_host.replace(/\./g,'_');
|
|
|
|
- document.getElementById(uc_cur_host).textContent = cur_host;
|
|
|
|
- var stat_id = 'stat_' + uc_cur_host;
|
|
|
|
- var ttip_id = 'ttip_' + uc_cur_host;
|
|
|
|
- if (cur_status == 'OK') {
|
|
|
|
- M_green.set(cur_sect_ux, M_green.get(cur_sect_ux) + 1);
|
|
|
|
- document.getElementById(stat_id).innerHTML = '<img src="/img/green_on.gif">';
|
|
|
|
- document.getElementById(ttip_id).innerHTML = '<b>RTT</b>: '+cur_rtt;
|
|
|
|
- } else if (cur_status == 'FAILED') {
|
|
|
|
- M_red.set(cur_sect_ux, M_red.get(cur_sect_ux) + 1);
|
|
|
|
- document.getElementById(stat_id).innerHTML = '<img src="/img/red_on.gif">';
|
|
|
|
- document.getElementById(ttip_id).innerHTML = '<b>RTT</b>: '+cur_rtt;
|
|
|
|
- } else if (cur_status == 'NEWFAIL') {
|
|
|
|
- M_red.set(cur_sect_ux, M_red.get(cur_sect_ux) + 1);
|
|
|
|
- document.getElementById(stat_id).innerHTML = '<img src="/img/red_anim.gif">';
|
|
|
|
- document.getElementById(ttip_id).innerHTML = '<b>RTT</b>: '+cur_rtt;
|
|
|
|
- } else {
|
|
|
|
- M_yellow.set(cur_sect_ux, M_yellow.get(cur_sect_ux) + 1);
|
|
|
|
- document.getElementById(stat_id).innerHTML = '<img src="/img/yellow_on.gif">';
|
|
|
|
- document.getElementById(ttip_id).innerHTML = '<b>RTT</b>: '+cur_rtt;
|
|
|
|
- }
|
|
|
|
- } // End for
|
|
|
|
- document.getElementById('cur_actime').innerHTML = cur_actime;
|
|
|
|
- for (let [sect, val] of M_red) {
|
|
|
|
- document.getElementById(sect).innerHTML = '<span class="unreach_r"><span class="red_r">'+M_red.get(sect).toString().padStart(3, " ")+'</span> | <span class="yellow_r">'+M_yellow.get(sect).toString().padStart(3, " ")+'</span> | <span class="green_r">'+M_green.get(sect).toString().padStart(3, " ")+'</span></span>';
|
|
|
|
- }
|
|
|
|
- };
|
|
|
|
-
|
|
|
|
- ws.onclose = function(evt) {
|
|
|
|
- //alert("WebSocket closed.");
|
|
|
|
- document.getElementById('cur_actime').innerHTML = '<span style="color:red;">stopped</span>';
|
|
|
|
- document.getElementById('close_button').style.opacity = "0.3";
|
|
|
|
- //document.getElementById('modal_alert_text').style.display='block';
|
|
|
|
- //document.getElementById('modal_alert_text').innerHTML = 'WebSocket closed.';
|
|
|
|
- };
|
|
|
|
- ws.onerror = function(evt) {
|
|
|
|
- alert("WebSocket closed due to an error.");
|
|
|
|
- document.getElementById('cur_actime').innerHTML = '<span style="color:red;">Error</span>';
|
|
|
|
- document.getElementById('close_button').style.opacity = "0.3";
|
|
|
|
- };
|
|
|
|
- socket_close = (code) => {
|
|
|
|
- document.getElementById('close_button').style.opacity = "0.3";
|
|
|
|
- ws.close(code);
|
|
|
|
- };
|
|
|
|
- };
|
|
|
|
-
|
|
|
|
- function Accordion(id) {
|
|
|
|
- var x = document.getElementById(id); //alert(x); //.previousSibling.getElementsByTagName("*").item(0).innerHTML);
|
|
|
|
- if (x.className.indexOf("w3-show") == -1) {
|
|
|
|
- x.className = "w3-show";
|
|
|
|
- } else {
|
|
|
|
- x.className = x.className.replace("w3-show", "w3-hide");
|
|
|
|
- }
|
|
|
|
- };
|
|
|
|
- </script>
|
|
|
|
- <script>
|
|
|
|
- // modal handling
|
|
|
|
- var modal = document.getElementById('modal_01');
|
|
|
|
- // When the user clicks anywhere outside of the modal, close it
|
|
|
|
- window.onclick = function(event) {
|
|
|
|
- if (event.target == modal) {
|
|
|
|
- modal.style.display = "none";
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- </script>
|
|
|
|
-
|
|
|
|
- <link rel="stylesheet" href="/css/w3.css">
|
|
|
|
- <style>
|
|
|
|
- %s
|
|
|
|
- </style>
|
|
|
|
- </header>
|
|
|
|
- <body>
|
|
|
|
- <div id="main">%s</div>\n
|
|
|
|
- <p><br></p>
|
|
|
|
-
|
|
|
|
- <div id="modal_01" class="w3-modal modal_01" onclick="this.style.display='none'">
|
|
|
|
- <div class="w3-modal-content modal_01_text">
|
|
|
|
- <div class="w3-container">
|
|
|
|
- <h4 style="font-variant:small-caps;letter-spacing:3px;">PingPanel</h4>
|
|
|
|
- %s
|
|
|
|
- </div>
|
|
|
|
- </div>
|
|
|
|
- </div>
|
|
|
|
- </body>
|
|
|
|
-</html>
|
|
|
|
-'''
|
|
|
|
-
|
|
|
|
-html_css = '''
|
|
|
|
-body {
|
|
|
|
- background: #333;
|
|
|
|
- font-family: Verdana, Arial, sans-serif;
|
|
|
|
- color: #ddd;
|
|
|
|
- line-height: 110%;
|
|
|
|
- font-size: 13px;
|
|
|
|
-}
|
|
|
|
-.red { color: red; }
|
|
|
|
-.green { color: #0f0; }
|
|
|
|
-.yellow { color: yellow; }
|
|
|
|
-.red_r { color: red; white-space: pre; text-align: right; font-family: monospace;text-shadow: 0px -0px 0px #f44;}
|
|
|
|
-.green_r { color: #0f0; white-space: pre; text-align: right;; font-family: monospace;}
|
|
|
|
-.yellow_r { color: yellow; white-space: pre; text-align: right;; font-family: monospace;}
|
|
|
|
-.ip_num { font-size: 80%; }
|
|
|
|
-
|
|
|
|
-.si3-dark-grey{color:#fff!important;background-color:#626262!important;text-shadow: 0 0 0px #fff, 0 0 1px #000;}
|
|
|
|
-
|
|
|
|
-.top_header {
|
|
|
|
- text-align: center;
|
|
|
|
- padding: 5px 10px; height: 43px;
|
|
|
|
- box-shadow: 0px 1px 9px 1px #c3d0cd78 !important;
|
|
|
|
- position: sticky;
|
|
|
|
- z-index: 100;
|
|
|
|
- top: 0px;
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-.tbars {
|
|
|
|
- color: #aaa;
|
|
|
|
- font-size: 11px;
|
|
|
|
- font-stretch: semi-expanded;
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-.unreach_r {
|
|
|
|
- background-color:#313131;
|
|
|
|
- padding-left:5px;
|
|
|
|
- padding-right:5px;
|
|
|
|
- letter-spacing: 0px;
|
|
|
|
- border-radius: 2px;
|
|
|
|
- font-weight: bold;
|
|
|
|
- font-size: 12px;
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-.modal_01 {
|
|
|
|
-
|
|
|
|
-}
|
|
|
|
-.modal_01_text {
|
|
|
|
- background-color: #000;
|
|
|
|
- color: #f0f0ca;
|
|
|
|
- line-height: 120%;
|
|
|
|
- font-size: 1.1em;
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-.sbutt {
|
|
|
|
- border: none;
|
|
|
|
- display: inline-block;
|
|
|
|
- vertical-align: middle;
|
|
|
|
- overflow: hidden;
|
|
|
|
- text-decoration: none;
|
|
|
|
- text-align: center;
|
|
|
|
- cursor: pointer;
|
|
|
|
- white-space: nowrap;
|
|
|
|
- border-radius: 4px;
|
|
|
|
- padding: 0px 8px !important;
|
|
|
|
- background-color: #9b9b9b !important;
|
|
|
|
- color: #000 !important;
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-.top_head_tit {
|
|
|
|
- font-size: 22px;
|
|
|
|
- font-variant: small-caps;
|
|
|
|
- letter-spacing: 4px;
|
|
|
|
- top: 0.3em;
|
|
|
|
- position: sticky;
|
|
|
|
-}
|
|
|
|
-.top_head_tit_pfix {
|
|
|
|
- font-size: 0.5em;
|
|
|
|
- font-variant: small-caps;
|
|
|
|
- letter-spacing: 0px;
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-.ttip_small {
|
|
|
|
- font-size: 64%;
|
|
|
|
- text-align: center;
|
|
|
|
- padding-top: 6px;
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-.tooltip {
|
|
|
|
- position: relative;
|
|
|
|
- display: inline-block;
|
|
|
|
- border-bottom: 1px dotted black;
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-.tooltip .tooltiptext {
|
|
|
|
- font-family: Verdana, 'PT Sans Caption', Arial, sans-serif;
|
|
|
|
- visibility: hidden;
|
|
|
|
- width: 170px;
|
|
|
|
- background-color: black;
|
|
|
|
- color: #fff;
|
|
|
|
- text-align: center;
|
|
|
|
- font-size: 17px;
|
|
|
|
- border-radius: 6px;
|
|
|
|
- padding: 5px 5px;
|
|
|
|
- position: absolute;
|
|
|
|
- z-index: 1;
|
|
|
|
- top: 120%;
|
|
|
|
- left: 50%;
|
|
|
|
- margin-left: -60px;
|
|
|
|
- line-height: 1.1em;
|
|
|
|
- text-shadow: 0 0 0 #a8a8a8 !important;
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-.tooltip .tooltiptext::after {
|
|
|
|
- content: "";
|
|
|
|
- position: absolute;
|
|
|
|
- bottom: 100%;
|
|
|
|
- left: 50%;
|
|
|
|
- margin-left: -5px;
|
|
|
|
- border-width: 5px;
|
|
|
|
- border-style: solid;
|
|
|
|
- border-color: transparent transparent black transparent;
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-.tooltip:hover .tooltiptext {
|
|
|
|
- visibility: visible;
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-.sticky {
|
|
|
|
- position: fixed;
|
|
|
|
- top: 0;
|
|
|
|
- width: 100%;
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-.sticky + .content {
|
|
|
|
- padding-top: 50px;
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-'''
|
|
|
|
-
|
|
|
|
-def create_html_modal01():
|
|
|
|
- html_modal = '''
|
|
|
|
-<p style="font-size:1.2em;font-variant:small-caps;letter-spacing:3px;font-style:italic;text-decoration:underline">Info</p>
|
|
|
|
- Config File:
|
|
|
|
- ''' + konfig_file + '''<br>
|
|
|
|
- Total number of Devices: _NUM_HOSTS_ <br>
|
|
|
|
- LoopInterval:
|
|
|
|
- ''' + str(loop_interval) + ''' s<br>
|
|
|
|
- FlashThreshold:
|
|
|
|
- ''' + str(config["Main"]["FlashThreshold"]) + ''' rounds<br>
|
|
|
|
- fping:
|
|
|
|
- ''' + fping_parameter[2].replace('--','') + '''<br>
|
|
|
|
- fping:
|
|
|
|
- ''' + fping_parameter[3].replace('--','') + ''' ms<br>
|
|
|
|
- fping:
|
|
|
|
- ''' + fping_parameter[4].replace('--','') + ''' ms<br>
|
|
|
|
-<br>
|
|
|
|
-
|
|
|
|
-<p style="font-size:1.2em;font-variant:small-caps;letter-spacing:3px;font-style:italic;text-decoration:underline">Help</p>
|
|
|
|
-The PingPanel board provides the header and the configured, expandable and collapsible sections with the corresponding devices to be monitored.<br>
|
|
|
|
-
|
|
|
|
-Among other things, the header contains the current time (<span class="green">green</span>) of the last ping run
|
|
|
|
-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>
|
|
|
|
-Pressing the "Stop" button terminates the connection to the server and, if no other client is active, the server pauses the ping rounds.<p>
|
|
|
|
-
|
|
|
|
-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>
|
|
|
|
-The section bars can be expanded and collapsed by clicking or tapping on them.<br>
|
|
|
|
-Consolidated status information <img src="/img/stat-inf.gif"> is divided into <span class="red">failed</span>,
|
|
|
|
-<span class="yellow">partially answered</span> and <span class="green">answered</span> ping requests.<br>
|
|
|
|
-
|
|
|
|
-Each monitored device displays its IP address and status. By hovering over or tapping on it its comment
|
|
|
|
-is displayed and the three ping RTT's of each ping round is reported.<p>
|
|
|
|
-
|
|
|
|
-A <span class="red">flashing red</span> status light indicates that the device has recently not responded during 'FlashThreshold' ping rounds.<br>
|
|
|
|
-A <span class="red">red</span> status light indicates that the device has not answered any of the three pings.<br>
|
|
|
|
-A <span class="yellow">yellow</span> status light indicates that the device has answered one of three pings.<br>
|
|
|
|
-A <span class="green">green</span> status light indicates that the device has answered to all three or at least two pings.<br>
|
|
|
|
-<p></p>
|
|
|
|
-<p> </p>
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-
|
|
|
|
- '''
|
|
|
|
-
|
|
|
|
- html_modal = html_modal.replace('_NUM_HOSTS_', str(anz_hosts))
|
|
|
|
-
|
|
|
|
- return html_modal
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-# -------- MAIN ------------------------------
|
|
|
|
-if __name__ == "__main__":
|
|
|
|
-
|
|
|
|
- print("PingPanel v"+VERSION+" by PSS -- started at " + datetime.now().strftime("%F %H:%M:%S"))
|
|
|
|
- # Commandline parsing
|
|
|
|
- parser = argparse.ArgumentParser(description='PingPanel V'+VERSION+"\n2024 by sigi",
|
|
|
|
- formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
|
|
- parser.add_argument('-c', help="Alternative configuration file (standard: config.cfg)", type=str, metavar='config-file')
|
|
|
|
- parser.add_argument('-v', help="Verbose mode", action="store_true")
|
|
|
|
- args = parser.parse_args()
|
|
|
|
- if args.c:
|
|
|
|
- konfig_file = args.c
|
|
|
|
- print("Configfile: " + konfig_file)
|
|
|
|
-
|
|
|
|
-
|
|
|
|
- # Config
|
|
|
|
- read_config()
|
|
|
|
-
|
|
|
|
-
|
|
|
|
- # create the app
|
|
|
|
- asyncio.run(main())
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-# ------
|
|
|
|
-
|
|
|
|
-
|
|
|