add hbc and prepare for package
This commit is contained in:
@@ -7,5 +7,4 @@ __pycache__/
|
|||||||
.venv/
|
.venv/
|
||||||
test/
|
test/
|
||||||
build/
|
build/
|
||||||
dist/
|
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
+122
-169
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# $Id: hbc,v 1.9 2012/03/29 02:08:36 andreas Exp $
|
# $Id: hbc,v 1.9 2012/03/29 02:08:36 andreas Exp $
|
||||||
# NEW
|
# NEW
|
||||||
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import socket
|
import socket
|
||||||
@@ -18,6 +19,7 @@ import subprocess
|
|||||||
import syslog
|
import syslog
|
||||||
import codecs
|
import codecs
|
||||||
|
|
||||||
|
from .config import load_config
|
||||||
|
|
||||||
PORT = 50003
|
PORT = 50003
|
||||||
INTERVAL = 10
|
INTERVAL = 10
|
||||||
@@ -30,6 +32,17 @@ running = True
|
|||||||
dorestart = False
|
dorestart = False
|
||||||
warned1 = False
|
warned1 = False
|
||||||
|
|
||||||
|
msgonly = False
|
||||||
|
helpflag = False
|
||||||
|
verbose = False
|
||||||
|
fdaemon = False
|
||||||
|
daemonized = False
|
||||||
|
optlist = []
|
||||||
|
msgboot = {}
|
||||||
|
home = os.environ["HOME"]
|
||||||
|
configfile = "%s/.hbrc" % home
|
||||||
|
cmdargs = []
|
||||||
|
iam = socket.gethostname()
|
||||||
|
|
||||||
def log(msg):
|
def log(msg):
|
||||||
if fdaemon:
|
if fdaemon:
|
||||||
@@ -460,181 +473,121 @@ def daemonize(
|
|||||||
os.dup2(so.fileno(), sys.stdout.fileno())
|
os.dup2(so.fileno(), sys.stdout.fileno())
|
||||||
os.dup2(se.fileno(), sys.stderr.fileno())
|
os.dup2(se.fileno(), sys.stderr.fileno())
|
||||||
|
|
||||||
|
#
|
||||||
msgonly = False
|
# Main program
|
||||||
helpflag = False
|
#
|
||||||
verbose = False
|
def build_parser():
|
||||||
fdaemon = False
|
parser = argparse.ArgumentParser(
|
||||||
daemonized = False
|
prog="hbc",
|
||||||
optlist = []
|
description="HeartBeatClient - send a heatbeat message to a HeartBeatDaemon",
|
||||||
args = []
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
msgboot = {}
|
|
||||||
home = os.environ["HOME"]
|
|
||||||
configfile = "%s/.hbrc" % home
|
|
||||||
cmdargs = []
|
|
||||||
iam = socket.gethostname()
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
optlist, args = getopt.getopt(sys.argv[1:], "bc:dhm:n:v")
|
|
||||||
except:
|
|
||||||
helpflag = True
|
|
||||||
|
|
||||||
for o, a in optlist:
|
|
||||||
if o == "-b":
|
|
||||||
msgboot["boot"] = 1
|
|
||||||
elif o == "-c":
|
|
||||||
configfile = a
|
|
||||||
cmdargs += [o, a]
|
|
||||||
elif o == "-d":
|
|
||||||
fdaemon = True
|
|
||||||
cmdargs += [o]
|
|
||||||
elif o == "-h":
|
|
||||||
helpflag = True
|
|
||||||
elif o == "-m":
|
|
||||||
msgboot["service"] = "service"
|
|
||||||
msgboot["msg"] = a
|
|
||||||
msgonly = True
|
|
||||||
elif o == "-n":
|
|
||||||
iam = a
|
|
||||||
cmdargs += [o, a]
|
|
||||||
elif o == "-v":
|
|
||||||
verbose = True
|
|
||||||
cmdargs += [o]
|
|
||||||
|
|
||||||
|
|
||||||
cmdargs += args
|
|
||||||
if verbose:
|
|
||||||
print("cmdargs for restart are %s" % cmdargs)
|
|
||||||
|
|
||||||
if helpflag:
|
|
||||||
print("hbc HeartBeatClient")
|
|
||||||
print("usage: hbc [-bdhv] [-c configfile] [-m msg][host1 [..]]")
|
|
||||||
print()
|
|
||||||
print(" -b indicate machine boot")
|
|
||||||
print(" -c configfile")
|
|
||||||
print(" -d daemonize")
|
|
||||||
print(" -h this help")
|
|
||||||
print(" -m send a message")
|
|
||||||
print(" -v verbose")
|
|
||||||
print()
|
|
||||||
print(
|
|
||||||
""" config file can contain
|
|
||||||
hb_hosts=('host1', 'host2', ..._
|
|
||||||
hb_port=50003
|
|
||||||
interval=20
|
|
||||||
logfile=...
|
|
||||||
logfmt={|test|msg}
|
|
||||||
grace=SECONDS
|
|
||||||
reportstrict={True|False}
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
|
parser.add_argument("-b", "--boot", action="store_true", help="Send a boot message")
|
||||||
|
parser.add_argument("-c", "--config", dest="configfile", help="Config file path (YAML)")
|
||||||
|
parser.add_argument("-m", "--message", dest="message", help="Send a message")
|
||||||
|
parser.add_argument("-n", "--name", dest="name", help="Name to use in heartbeat message")
|
||||||
|
parser.add_argument("-f", "--daemon", action="store_true", help="Run in daemon mode")
|
||||||
|
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
|
||||||
|
parser.add_argument("-x", "--debug", action="count", default=0, help="Increase debug level")
|
||||||
|
parser.add_argument("hosts", nargs="+", help="Heartbeat daemon hosts to send to")
|
||||||
|
return parser
|
||||||
|
|
||||||
sys.exit(1)
|
def main(argv=None):
|
||||||
|
global msgonly, helpflag, verbose, fdaemon, daemonized, optlist, msgboot, home, configfile, cmdargs, iam, hb_port, conns, interval, hb_hosts
|
||||||
|
parser = build_parser()
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
config = load_config(args.configfile)
|
||||||
|
|
||||||
#
|
# Apply CLI overrides
|
||||||
# set defaults
|
if args.boot:
|
||||||
|
msgboot["boot"] = 1
|
||||||
|
if args.message:
|
||||||
|
msgboot["service"] = "service"
|
||||||
|
msgboot["msg"] = args.message
|
||||||
|
msgonly = True
|
||||||
|
if args.name:
|
||||||
|
iam = args.name
|
||||||
|
if args.daemon:
|
||||||
|
fdaemon = True
|
||||||
|
if args.verbose:
|
||||||
|
verbose = True
|
||||||
|
if args.debug:
|
||||||
|
config.setdefault("debug", 0)
|
||||||
|
config["debug"] += args.debug
|
||||||
|
|
||||||
hb_port = PORT
|
cmdargs += argv
|
||||||
interval = INTERVAL
|
|
||||||
hb_hosts = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
f = open(configfile, "r")
|
|
||||||
if verbose:
|
if verbose:
|
||||||
print("notice: using config file %s" % configfile)
|
print("cmdargs for restart are %s" % cmdargs)
|
||||||
except:
|
|
||||||
if verbose:
|
#
|
||||||
print("warning: running without config file: %s" % configfile)
|
# set defaults
|
||||||
f = None
|
|
||||||
|
hb_hosts = args.hosts
|
||||||
|
hb_port = config.get("hb_port", PORT)
|
||||||
|
interval = config.get("interval", INTERVAL)
|
||||||
|
|
||||||
if f:
|
#
|
||||||
while 1:
|
if verbose:
|
||||||
l = f.readline()
|
print("notice: hb_hosts: %s" % str(hb_hosts))
|
||||||
if len(l) == 0:
|
print("notice: hb_port: %s" % hb_port)
|
||||||
|
print("notice: interval: %s" % interval)
|
||||||
|
print("notice: iam: %s" % iam)
|
||||||
|
print("notice: msgonly: %s" % msgonly)
|
||||||
|
print("notice: msgboot: %s" % msgboot)
|
||||||
|
|
||||||
|
if not msgonly:
|
||||||
|
msgboot["interval"] = interval
|
||||||
|
|
||||||
|
conns = {}
|
||||||
|
while True:
|
||||||
|
if verbose:
|
||||||
|
log("create connections")
|
||||||
|
createConnections(hb_hosts)
|
||||||
|
if len(conns) != 0:
|
||||||
break
|
break
|
||||||
r = l[:-1].split("=")
|
if verbose:
|
||||||
if r[0] == "hb_hosts":
|
log("no connections yet, sleep a bit")
|
||||||
hb_hosts = eval(r[1])
|
time.sleep(2)
|
||||||
if verbose:
|
|
||||||
print("notice: cfg hb_hosts: %s" % hb_hosts)
|
|
||||||
elif r[0] == "interval":
|
|
||||||
interval = eval(r[1])
|
|
||||||
elif r[0] == "hb_port":
|
|
||||||
hb_port = eval(r[1])
|
|
||||||
elif r[0] == "name":
|
|
||||||
iam = eval(r[1])
|
|
||||||
if verbose:
|
|
||||||
print("name set to %s" % iam)
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
if len(args) != 0:
|
|
||||||
hb_hosts = args
|
|
||||||
|
|
||||||
|
|
||||||
if len(hb_hosts) == 0:
|
|
||||||
print("no hb server specified")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
#
|
|
||||||
if verbose:
|
|
||||||
print("notice: hb_hosts: %s" % str(hb_hosts))
|
|
||||||
print("notice: hb_port: %s" % hb_port)
|
|
||||||
print("notice: interval: %s" % interval)
|
|
||||||
print("notice: iam: %s" % iam)
|
|
||||||
print("notice: msgonly: %s" % msgonly)
|
|
||||||
print("notice: msgboot: %s" % msgboot)
|
|
||||||
|
|
||||||
if not msgonly:
|
|
||||||
msgboot["interval"] = interval
|
|
||||||
|
|
||||||
|
|
||||||
conns = {}
|
|
||||||
while True:
|
|
||||||
if verbose:
|
if verbose:
|
||||||
log("create connections")
|
log("%s connections created" % (len(conns)))
|
||||||
createConnections(hb_hosts)
|
|
||||||
if len(conns) != 0:
|
if len(msgboot) > 0:
|
||||||
break
|
if verbose:
|
||||||
|
print("on boot")
|
||||||
|
msgboot["acks"] = 0
|
||||||
|
for conn in conns:
|
||||||
|
conns[conn].sendto(msgboot)
|
||||||
|
|
||||||
|
if msgonly:
|
||||||
|
if verbose:
|
||||||
|
print("msgboot done msgonly=%s" % msgonly)
|
||||||
|
closeall()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
#
|
||||||
|
syslog.openlog("hbc", syslog.LOG_PID, syslog.LOG_DAEMON)
|
||||||
|
if fdaemon:
|
||||||
|
print("daemoinizing.")
|
||||||
|
daemonize()
|
||||||
|
daemonized = True
|
||||||
|
syslog.syslog(syslog.LOG_ERR, "starting heartbeat to %s" % ",".join(hb_hosts))
|
||||||
|
|
||||||
|
signal.signal(signal.SIGTERM, handler)
|
||||||
|
running = True
|
||||||
|
try:
|
||||||
|
process()
|
||||||
|
except Exception as e:
|
||||||
|
syslogtrace("process")
|
||||||
|
if verbose:
|
||||||
|
print("err: process exit: %s" % e)
|
||||||
|
|
||||||
if verbose:
|
if verbose:
|
||||||
log("no connections yet, sleep a bit")
|
log("main: cleanup")
|
||||||
time.sleep(2)
|
cleanup()
|
||||||
|
if dorestart:
|
||||||
if verbose:
|
restart()
|
||||||
log("%s connections created" % (len(conns)))
|
|
||||||
|
if __name__ == "__main__":
|
||||||
if len(msgboot) > 0:
|
main()
|
||||||
if verbose:
|
|
||||||
print("on boot")
|
|
||||||
msgboot["acks"] = 0
|
|
||||||
for conn in conns:
|
|
||||||
conns[conn].sendto(msgboot)
|
|
||||||
|
|
||||||
if msgonly:
|
|
||||||
if verbose:
|
|
||||||
print("msgboot done msgonly=%s" % msgonly)
|
|
||||||
closeall()
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
#
|
|
||||||
syslog.openlog("hbc", syslog.LOG_PID, syslog.LOG_DAEMON)
|
|
||||||
if fdaemon:
|
|
||||||
print("daemoinizing.")
|
|
||||||
daemonize()
|
|
||||||
daemonized = True
|
|
||||||
syslog.syslog(syslog.LOG_ERR, "starting heartbeat to %s" % ",".join(hb_hosts))
|
|
||||||
|
|
||||||
signal.signal(signal.SIGTERM, handler)
|
|
||||||
running = True
|
|
||||||
try:
|
|
||||||
process()
|
|
||||||
except Exception as e:
|
|
||||||
syslogtrace("process")
|
|
||||||
if verbose:
|
|
||||||
print("err: process exit: %s" % e)
|
|
||||||
|
|
||||||
if verbose:
|
|
||||||
log("main: cleanup")
|
|
||||||
cleanup()
|
|
||||||
if dorestart:
|
|
||||||
restart()
|
|
||||||
+6
-2
@@ -131,10 +131,14 @@ async def start(
|
|||||||
return web.Response(text="restart request")
|
return web.Response(text="restart request")
|
||||||
|
|
||||||
async def live(request):
|
async def live(request):
|
||||||
# render template from templates/live.html using Jinja2
|
# render template from hbd/templates/live.html using Jinja2
|
||||||
env = jinja2.Environment(loader=jinja2.FileSystemLoader(config.get("templates_dir", "templates")))
|
# Resolve templates directory relative to the hbd package
|
||||||
|
pkg_dir = os.path.dirname(__file__)
|
||||||
|
templates_dir = config.get("templates_dir", os.path.join(pkg_dir, "templates"))
|
||||||
|
env = jinja2.Environment(loader=jinja2.FileSystemLoader(templates_dir))
|
||||||
host = config.get("hb_host", "localhost")
|
host = config.get("hb_host", "localhost")
|
||||||
extra_scripts = config.get("http_extra_scripts", "")
|
extra_scripts = config.get("http_extra_scripts", "")
|
||||||
|
host = request.host.split(":")[0]
|
||||||
heartbeat_ws_url = f"ws://{host}:{config.get('ws_port', 50005)}/hbd"
|
heartbeat_ws_url = f"ws://{host}:{config.get('ws_port', 50005)}/hbd"
|
||||||
tmpl = env.get_template("live.html")
|
tmpl = env.get_template("live.html")
|
||||||
body = tmpl.render(
|
body = tmpl.render(
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# install hbd/hbc from wheel and create symlinks for hbd and hbc in /usr/local/bin
|
||||||
|
|
||||||
|
set -e
|
||||||
|
pip install --upgrade --force-reinstall --no-deps --find-links https://github.com/andreas-h/heartbeat/releases/latest/download/ heartbeat-hbd
|
||||||
+3
-2
@@ -9,6 +9,7 @@ from . import __version__
|
|||||||
|
|
||||||
from . import udp
|
from . import udp
|
||||||
from . import hbdclass
|
from . import hbdclass
|
||||||
|
|
||||||
from . import ws as ws_mod
|
from . import ws as ws_mod
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -73,7 +74,7 @@ async def _run_async(config):
|
|||||||
|
|
||||||
# prepare runtime dependencies
|
# prepare runtime dependencies
|
||||||
import threading
|
import threading
|
||||||
from . import hbdclass
|
# from . import hbdclass
|
||||||
from . import http as http_mod
|
from . import http as http_mod
|
||||||
from . import dns as dns_mod
|
from . import dns as dns_mod
|
||||||
from . import notify as notify_mod
|
from . import notify as notify_mod
|
||||||
@@ -254,7 +255,7 @@ def load_pickled_hosts(config, hbdclass):
|
|||||||
lastfm = ["", "", ""]
|
lastfm = ["", "", ""]
|
||||||
pickf.close()
|
pickf.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(("load pickled failed: %s" % e))
|
logger.exception("load pickled failed: %s", e)
|
||||||
os.unlink(pickfile)
|
os.unlink(pickfile)
|
||||||
hbdclass.Connection.htab = {}
|
hbdclass.Connection.htab = {}
|
||||||
for h in list(hbdclass.Host.hosts.keys()):
|
for h in list(hbdclass.Host.hosts.keys()):
|
||||||
|
|||||||
@@ -51,6 +51,40 @@
|
|||||||
th:not(.sorttable_sorted):not(.sorttable_sorted_reverse):not(.sorttable_nosort):after {
|
th:not(.sorttable_sorted):not(.sorttable_sorted_reverse):not(.sorttable_nosort):after {
|
||||||
content: " \2195";
|
content: " \2195";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Modal for connection status messages */
|
||||||
|
.connection-modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-modal.show {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-modal-content {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #888;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-modal-content p {
|
||||||
|
margin: 10px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var cnt = 0;
|
var cnt = 0;
|
||||||
@@ -156,6 +190,11 @@
|
|||||||
ws_hbd.onopen = function () {
|
ws_hbd.onopen = function () {
|
||||||
// Web Socket is connected, send data using send()
|
// Web Socket is connected, send data using send()
|
||||||
console.log("ws connect");
|
console.log("ws connect");
|
||||||
|
// Hide modal window if visible
|
||||||
|
var modal = document.getElementById("connectionModal");
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.remove("show");
|
||||||
|
}
|
||||||
ws_hbd.send("heartbeat_web");
|
ws_hbd.send("heartbeat_web");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -179,6 +218,11 @@
|
|||||||
ws_hbd.onclose = function (event) {
|
ws_hbd.onclose = function (event) {
|
||||||
/* console.log(event); */
|
/* console.log(event); */
|
||||||
console.log("Connection is closed, reopening");
|
console.log("Connection is closed, reopening");
|
||||||
|
// Show modal window
|
||||||
|
var modal = document.getElementById("connectionModal");
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.add("show");
|
||||||
|
}
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
WS_Connect();
|
WS_Connect();
|
||||||
}, 3000);
|
}, 3000);
|
||||||
@@ -222,6 +266,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'foot.html' %}
|
{% include 'foot.html' %}
|
||||||
|
|
||||||
|
<!-- Connection status modal -->
|
||||||
|
<div id="connectionModal" class="connection-modal">
|
||||||
|
<div class="connection-modal-content">
|
||||||
|
<p>⚠️ Connection is closed, reopening...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
setup();
|
setup();
|
||||||
</script>
|
</script>
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
"""UDP listener and datagram processing."""
|
"""UDP listener and datagram processing."""
|
||||||
import asyncio
|
import asyncio
|
||||||
from compression import zlib
|
import zlib
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|||||||
+380
@@ -0,0 +1,380 @@
|
|||||||
|
"""
|
||||||
|
host and connection class shared between hbd and
|
||||||
|
the websit's heartbeat.py
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import copy
|
||||||
|
import queue
|
||||||
|
|
||||||
|
num = 0
|
||||||
|
|
||||||
|
MAXRTTS = 10
|
||||||
|
|
||||||
|
DEBUG = 2
|
||||||
|
|
||||||
|
|
||||||
|
def log(host, m):
|
||||||
|
if DEBUG:
|
||||||
|
print("class log: %s %s" % (host, m))
|
||||||
|
|
||||||
|
|
||||||
|
class Connection:
|
||||||
|
# map of addrs to names
|
||||||
|
|
||||||
|
htab = {}
|
||||||
|
UNKNOWN = "unknown"
|
||||||
|
UP = "up"
|
||||||
|
DOWN = "down"
|
||||||
|
OVERDUE = "overdue"
|
||||||
|
|
||||||
|
def __init__(self, host, cid, addr, afam):
|
||||||
|
self.host = host
|
||||||
|
self.cid = cid
|
||||||
|
if addr[0:7] == "::ffff:":
|
||||||
|
addr = addr[7:]
|
||||||
|
self.addr = addr
|
||||||
|
self.afam = afam
|
||||||
|
self.rtts = [0]
|
||||||
|
self.lastbeat = time.time()
|
||||||
|
self.statetime = self.lastbeat
|
||||||
|
self.deltastatetime = "computed"
|
||||||
|
self.state = Connection.UNKNOWN
|
||||||
|
|
||||||
|
if host:
|
||||||
|
Connection.htab[addr] = self.host.name
|
||||||
|
if self.host.isDynDns():
|
||||||
|
log(self.host.name, "dns update %s" % self.addr)
|
||||||
|
Host.dnsQ.put((self.host.name, self.addr))
|
||||||
|
|
||||||
|
def registerDns(self):
|
||||||
|
Host.dnsQ.put((self.host.name, self.addr))
|
||||||
|
|
||||||
|
def clearstate(self):
|
||||||
|
d = {}
|
||||||
|
d["addr"] = ""
|
||||||
|
d["rtt"] = ""
|
||||||
|
d["lastbeat"] = ""
|
||||||
|
d["state"] = ""
|
||||||
|
d["statetime"] = ""
|
||||||
|
d["deltastatetime"] = ""
|
||||||
|
d["rttstate"] = ""
|
||||||
|
return d
|
||||||
|
|
||||||
|
def statedict(self, Null=False):
|
||||||
|
d = self.clearstate()
|
||||||
|
now = time.time()
|
||||||
|
if not Null:
|
||||||
|
d["addr"] = self.addr
|
||||||
|
if self.rtts[-1]:
|
||||||
|
d["rtt"] = "%0.1f" % self.rtts[-1]
|
||||||
|
elif self.state == Connection.UNKNOWN:
|
||||||
|
d["rtt"] = ""
|
||||||
|
else:
|
||||||
|
d["rtt"] = "?"
|
||||||
|
d["lastbeat"] = self.lastbeat
|
||||||
|
if self.state == Connection.OVERDUE:
|
||||||
|
d["state"] = "<b>%s</b>" % self.state
|
||||||
|
else:
|
||||||
|
d["state"] = self.state
|
||||||
|
if self.state == Connection.UP:
|
||||||
|
d["rttstate"] = d["rtt"]
|
||||||
|
elif self.state == Connection.OVERDUE:
|
||||||
|
d["rttstate"] = ""
|
||||||
|
else:
|
||||||
|
d["rttstate"] = d["state"]
|
||||||
|
d["statetime"] = time.strftime(
|
||||||
|
"%Y-%m-%d %H:%M:%S", time.localtime(self.statetime)
|
||||||
|
)
|
||||||
|
delta = now - self.statetime
|
||||||
|
|
||||||
|
if self.state == Connection.UNKNOWN:
|
||||||
|
d["deltastatetime"] = ""
|
||||||
|
elif delta > 86400:
|
||||||
|
# d['deltastatetime'] = time.strftime("%d %H:%M:%S", time.gmtime(delta))
|
||||||
|
d["deltastatetime"] = "%0.1f days" % (delta / 86400.0)
|
||||||
|
elif delta > 3600:
|
||||||
|
# d['deltastatetime'] = time.strftime("%H:%M:%S", time.gmtime(delta))
|
||||||
|
d["deltastatetime"] = time.strftime("%k:%M hrs", time.gmtime(delta))
|
||||||
|
# d['deltastatetime'] = "%0.1f hrs" % (delta / 3600.)
|
||||||
|
elif delta > 60:
|
||||||
|
# d['deltastatetime'] = time.strftime("%M:%S", time.gmtime(delta))
|
||||||
|
d["deltastatetime"] = time.strftime("%M:%S mins", time.gmtime(delta))
|
||||||
|
# d['deltastatetime'] = "%0.1f mins" % (delta / 60.)
|
||||||
|
else:
|
||||||
|
# d['deltastatetime'] = time.strftime("%S", time.gmtime(delta))
|
||||||
|
d["deltastatetime"] = "%i secs" % (delta)
|
||||||
|
if self.state == Connection.UNKNOWN and now - self.lastbeat > 86400 * 10:
|
||||||
|
d = self.clearstate()
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
||||||
|
def headerdict(self, afam):
|
||||||
|
d = {}
|
||||||
|
d["addr"] = "%s Addr" % afam
|
||||||
|
d["rtt"] = "Latencey"
|
||||||
|
d["lastbeat"] = "Last Contact"
|
||||||
|
d["state"] = "State"
|
||||||
|
d["statetime"] = "Last State"
|
||||||
|
d["rttstate"] = "Reach"
|
||||||
|
d["deltastatetime"] = "Last State"
|
||||||
|
return d
|
||||||
|
|
||||||
|
def jsons(self):
|
||||||
|
return json.dumps(self.__dict__)
|
||||||
|
|
||||||
|
# set new state, return number of secs in previous state
|
||||||
|
def newstate(self, state, now, when=0):
|
||||||
|
self.state = state
|
||||||
|
delta = now - when
|
||||||
|
s = delta - self.statetime
|
||||||
|
self.statetime = delta
|
||||||
|
return s
|
||||||
|
|
||||||
|
def getstate(self):
|
||||||
|
return self.state
|
||||||
|
|
||||||
|
def newaddr(self, addr, rtt, now):
|
||||||
|
self.lastbeat = now
|
||||||
|
self.rtts.append(rtt)
|
||||||
|
if len(self.rtts) > MAXRTTS:
|
||||||
|
del self.rtts[0]
|
||||||
|
|
||||||
|
if self.addr == addr:
|
||||||
|
r = None
|
||||||
|
else:
|
||||||
|
r = "changed from %s to %s" % (self.addr, addr)
|
||||||
|
try:
|
||||||
|
del Connection.htab[self.addr]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self.addr = addr
|
||||||
|
Connection.htab[addr] = self.host.name
|
||||||
|
if self.host.isDynDns():
|
||||||
|
Host.dnsQ.put((self.host.name, self.addr))
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
class Host:
|
||||||
|
# Table of Hosts
|
||||||
|
hosts = {}
|
||||||
|
dnsQ = queue.Queue()
|
||||||
|
|
||||||
|
def __init__(self, name):
|
||||||
|
global num
|
||||||
|
self.name = name
|
||||||
|
if name:
|
||||||
|
num += 1
|
||||||
|
Host.hosts[name] = self
|
||||||
|
self.num = num
|
||||||
|
self.dyn = False
|
||||||
|
self.watched = False
|
||||||
|
self.upcount = 0
|
||||||
|
self.interval = 0
|
||||||
|
self.doesack = -1
|
||||||
|
self.cmds = []
|
||||||
|
self.cver = 0
|
||||||
|
self.connections = {}
|
||||||
|
self.hdwcounts = [[0, 0], [0, 0], [0, 0]]
|
||||||
|
|
||||||
|
def statedict(self):
|
||||||
|
d = {}
|
||||||
|
d["name"] = self.name
|
||||||
|
if self.dyn:
|
||||||
|
d["name"] += "*"
|
||||||
|
if self.watched:
|
||||||
|
d["name"] = "<b>%s</b>" % d["name"]
|
||||||
|
d["dyn"] = str(self.dyn)
|
||||||
|
d["ver"] = str(self.cver)
|
||||||
|
d["num"] = self.num
|
||||||
|
for c in ["IPv4", "IPv6"]:
|
||||||
|
if c in self.connections:
|
||||||
|
cs = self.connections[c].statedict()
|
||||||
|
else:
|
||||||
|
cs = ubConnection.statedict(True)
|
||||||
|
for csv in cs:
|
||||||
|
d["%s.%s" % (c, csv)] = cs[csv]
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
||||||
|
def headerdict(self):
|
||||||
|
d = {}
|
||||||
|
d["name"] = "Name"
|
||||||
|
d["dyn"] = "Dyn"
|
||||||
|
d["ver"] = "Ver"
|
||||||
|
d["num"] = "??"
|
||||||
|
for c in ["IPv4", "IPv6"]:
|
||||||
|
cs = ubConnection.headerdict(c)
|
||||||
|
for csv in cs:
|
||||||
|
d["%s.%s" % (c, csv)] = cs[csv]
|
||||||
|
return d
|
||||||
|
|
||||||
|
def registerDns(self):
|
||||||
|
for af in self.connections:
|
||||||
|
self.connections[af].registerDns()
|
||||||
|
|
||||||
|
def stateinfo(self):
|
||||||
|
ddict = {}
|
||||||
|
for d in self.__dict__:
|
||||||
|
if d == "connections":
|
||||||
|
cl = []
|
||||||
|
for c in self.connections:
|
||||||
|
# dirty ugly hack: fix conn to host backpointer
|
||||||
|
cld = copy.deepcopy(self.connections[c].__dict__)
|
||||||
|
cld["host"] = cld["host"].name
|
||||||
|
cl.append(cld)
|
||||||
|
ddict[d] = cl
|
||||||
|
else:
|
||||||
|
ddict[d] = self.__dict__[d]
|
||||||
|
return ddict
|
||||||
|
|
||||||
|
def jsons(self):
|
||||||
|
return json.dumps(self.stateinfo())
|
||||||
|
|
||||||
|
def setcver(self, cver):
|
||||||
|
self.cver = cver
|
||||||
|
|
||||||
|
def isDynDns(self):
|
||||||
|
return self.dyn
|
||||||
|
|
||||||
|
def isIPv4(self, addr):
|
||||||
|
if isinstance(addr, tuple):
|
||||||
|
return addr[0].find(".") > 0
|
||||||
|
else:
|
||||||
|
return addr.find(".") > 0
|
||||||
|
|
||||||
|
def conndata(self, cid, addr, rtt, now):
|
||||||
|
if addr[0:7] == "::ffff:":
|
||||||
|
addr = addr[7:]
|
||||||
|
if self.isIPv4(addr):
|
||||||
|
afam = "IPv4"
|
||||||
|
else:
|
||||||
|
afam = "IPv6"
|
||||||
|
|
||||||
|
if afam not in self.connections:
|
||||||
|
self.connections[afam] = Connection(self, cid, addr, afam)
|
||||||
|
|
||||||
|
conn = self.connections[afam]
|
||||||
|
res = conn.newaddr(addr, rtt, now)
|
||||||
|
return conn, res
|
||||||
|
|
||||||
|
# called when reloading class from pickle, add new fields here
|
||||||
|
def fixup(self):
|
||||||
|
for c in ["IPv4", "IPv6"]:
|
||||||
|
if c in self.connections:
|
||||||
|
addr = self.connections[c].addr
|
||||||
|
if addr[0:7] == "::ffff:":
|
||||||
|
addr = addr[7:]
|
||||||
|
self.connections[c].addr = addr
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
# def dispstate(self):
|
||||||
|
# if self.state in ["down", "overdue"]:
|
||||||
|
# state = "<b>%s</b>" % self.state
|
||||||
|
# elif self.state in ["up", "UP"]:
|
||||||
|
# state = ""
|
||||||
|
# for x in list(self.connections.keys()):
|
||||||
|
# try:
|
||||||
|
# state += " %5.1f" % (self.connections[x].rtts[-1])
|
||||||
|
# except:
|
||||||
|
# state += " %5s" % (self.connections[x].rtts[-1])
|
||||||
|
# elif self.state in ["unknown", "UNKNOWN"]:
|
||||||
|
# state = ""
|
||||||
|
# else:
|
||||||
|
# state = "%s" % self.state
|
||||||
|
# return state
|
||||||
|
|
||||||
|
def dispstats(self):
|
||||||
|
if self.doesack != -1:
|
||||||
|
if self.upcount > 0:
|
||||||
|
# return "(%0.1f%%) %s %s %s " % ((self.doesack * 100.0) / self.upcount, self.doesack, self.upcount, self.hdwcounts)
|
||||||
|
r = ""
|
||||||
|
for v in range(3):
|
||||||
|
a, u = self.hdwcounts[v]
|
||||||
|
if (self.upcount - u) != 0:
|
||||||
|
vs = "%0.0f" % (
|
||||||
|
100.0 - (((self.doesack - a) * 100.0) / (self.upcount - u))
|
||||||
|
)
|
||||||
|
if vs == "0":
|
||||||
|
vs = ""
|
||||||
|
else:
|
||||||
|
vs = "-"
|
||||||
|
r += '<td align="right">%s</td>' % vs
|
||||||
|
return r
|
||||||
|
else:
|
||||||
|
return "<td>(%s)</td><td></td><td></td>" % (self.doesack)
|
||||||
|
return '<td align="right">N/A</td><td></td<td></td>>'
|
||||||
|
|
||||||
|
hostfields_long = [
|
||||||
|
"name",
|
||||||
|
"IPv4.addr",
|
||||||
|
"IPv4.state",
|
||||||
|
("IPv4.rtt", 'style="text-align: right;"'),
|
||||||
|
("IPv4.statetime", 'style="text-align: right;"'),
|
||||||
|
"IPv6.addr",
|
||||||
|
"IPv6.state",
|
||||||
|
("IPv6.rtt", 'style="text-align: right;"'),
|
||||||
|
("IPv6.statetime", 'style="text-align: right;"'),
|
||||||
|
"ver",
|
||||||
|
]
|
||||||
|
|
||||||
|
hostfields_short = [
|
||||||
|
"name",
|
||||||
|
("IPv4.rttstate", 'style="text-align: right;"'),
|
||||||
|
("IPv4.deltastatetime", 'style="text-align: right;"'),
|
||||||
|
("IPv6.rttstate", 'style="text-align: right;"'),
|
||||||
|
("IPv6.deltastatetime", 'style="text-align: right;"'),
|
||||||
|
]
|
||||||
|
|
||||||
|
def gene(self, tag, v, attrib=None):
|
||||||
|
if attrib:
|
||||||
|
a = " %s" % attrib
|
||||||
|
else:
|
||||||
|
a = ""
|
||||||
|
return "<%s%s>%s</%s>" % (tag, a, v, tag)
|
||||||
|
|
||||||
|
def htmltable(self, tag, hd, short):
|
||||||
|
if short:
|
||||||
|
hostfields = Host.hostfields_short
|
||||||
|
else:
|
||||||
|
hostfields = Host.hostfields_long
|
||||||
|
h = []
|
||||||
|
for f in hostfields:
|
||||||
|
if isinstance(f, tuple):
|
||||||
|
h.append(self.gene(tag, hd[f[0]], f[1]))
|
||||||
|
else:
|
||||||
|
h.append(self.gene(tag, hd[f]))
|
||||||
|
return self.gene("tr", "\n".join(h))
|
||||||
|
|
||||||
|
def buildhosttable(self, short=False):
|
||||||
|
if DEBUG > 1:
|
||||||
|
print("DBG buildhosttable: start")
|
||||||
|
res = []
|
||||||
|
res.append('<table id="ntable" class="sortable">')
|
||||||
|
res.append(ubHost.htmltable("th", ubHost.headerdict(), short))
|
||||||
|
hosts_sorted = list(Host.hosts.keys())
|
||||||
|
if len(hosts_sorted):
|
||||||
|
hosts_sorted.sort()
|
||||||
|
for h in hosts_sorted:
|
||||||
|
res.append(ubHost.htmltable("td", Host.hosts[h].statedict(), short))
|
||||||
|
res.append("</table>")
|
||||||
|
if DEBUG > 1:
|
||||||
|
print("DBG buildhosttable: %s" % res)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def buildmsgtable(self, msgs):
|
||||||
|
res = []
|
||||||
|
le = max(40 - len(Host.hosts), 3)
|
||||||
|
res.append("<h4>Log of Events</h4>")
|
||||||
|
for m in msgs[len(msgs) - le:]:
|
||||||
|
res.append("%s<BR>" % m)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
# create fake "unbound objects", remove in Python 3.0
|
||||||
|
ubHost = Host(None)
|
||||||
|
ubConnection = Connection(None, "", "", "")
|
||||||
+4
-1
@@ -10,7 +10,6 @@ readme = "README.md"
|
|||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
license = { text = "MIT" }
|
license = { text = "MIT" }
|
||||||
keywords = ["heartbeat", "monitoring", "dns", "websocket"]
|
keywords = ["heartbeat", "monitoring", "dns", "websocket"]
|
||||||
|
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "heartbeat contributors" }
|
{ name = "heartbeat contributors" }
|
||||||
]
|
]
|
||||||
@@ -38,7 +37,11 @@ dev = [
|
|||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
hbd = "hbd.cli:main"
|
hbd = "hbd.cli:main"
|
||||||
|
hbc = "hbd.hbc:main"
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["."]
|
where = ["."]
|
||||||
include = ["hbd*"]
|
include = ["hbd*"]
|
||||||
|
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
"hbd" = ["*.yaml", "static/*", "static/*/*", "templates/*"]
|
||||||
Reference in New Issue
Block a user