You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1584 lines
62 KiB
1584 lines
62 KiB
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import base64
|
|
import json
|
|
import logging
|
|
import logging.config
|
|
import logging.config
|
|
import os
|
|
import re
|
|
import socket
|
|
import socketserver
|
|
import subprocess
|
|
import sys
|
|
import threading
|
|
import time
|
|
import urllib.parse
|
|
from datetime import timedelta
|
|
from http import HTTPStatus
|
|
from http.server import BaseHTTPRequestHandler
|
|
from optparse import OptionParser
|
|
|
|
try: # make sure that (unsupported) Python2 can fail gracefully
|
|
import configparser
|
|
from urllib.request import urlopen
|
|
from urllib.error import HTTPError
|
|
except ImportError:
|
|
pass
|
|
|
|
|
|
if sys.version_info < (3, 5, 0):
|
|
print("Python2 not supported! Please run with Python3.5+")
|
|
sys.exit(1)
|
|
|
|
CTYPE_HTML = "text/html"
|
|
CTYPE_JSON = "application/json"
|
|
BOARD_NAME = "RaspiBlitz"
|
|
BOARD_VERSION = "0.93"
|
|
NETWORK_FILE = "/home/admin/.network"
|
|
BITCOIN_HOME = "/home/bitcoin"
|
|
IF_NAME = "eth0"
|
|
TIMEOUT = 10
|
|
|
|
CRYPTO_CURRENCIES = {
|
|
"bitcoin": {
|
|
"title": "Bitcoin",
|
|
"cli": "bitcoin-cli",
|
|
"daemon": "bitcoind",
|
|
"testnet_dir": "testnet3",
|
|
"mainnet_port": 8333,
|
|
"testnet_port": 18333
|
|
},
|
|
"litecoin": {
|
|
"title": "Litecoin",
|
|
"cli": "litecoin-cli",
|
|
"daemon": "litecoind",
|
|
"testnet_dir": "testnet3", # ?!
|
|
"mainnet_port": 9333,
|
|
"testnet_port": 19333
|
|
}
|
|
}
|
|
|
|
logger = logging.getLogger()
|
|
|
|
|
|
def setup_logging(default_path='infoblitz_logging.json'):
|
|
"""Setup logging configuration"""
|
|
path = default_path
|
|
if os.path.exists(path):
|
|
with open(path, 'rt') as f:
|
|
config = json.load(f)
|
|
logging.config.dictConfig(config)
|
|
else: # is infoblitz_logging.json does not exist use the following default log setup
|
|
default_config_as_json = """
|
|
{
|
|
"version": 1,
|
|
"disable_existing_loggers": false,
|
|
"formatters": {
|
|
"simple": {
|
|
"format": "%(asctime)s (%(threadName)-10s) %(name)s - %(levelname)s - %(message)s"
|
|
},
|
|
"extended": {
|
|
"format": "%(asctime)s (%(threadName)-10s) %(name)s - %(levelname)s - %(module)s:%(lineno)d - %(message)s"
|
|
}
|
|
|
|
},
|
|
|
|
"handlers": {
|
|
"console": {
|
|
"class": "logging.StreamHandler",
|
|
"level": "ERROR",
|
|
"formatter": "simple",
|
|
"stream": "ext://sys.stdout"
|
|
},
|
|
|
|
"file_handler": {
|
|
"class": "logging.handlers.RotatingFileHandler",
|
|
"level": "DEBUG",
|
|
"formatter": "extended",
|
|
"filename": "infoblitz.log",
|
|
"maxBytes": 10485760,
|
|
"backupCount": 2,
|
|
"encoding": "utf8"
|
|
}
|
|
},
|
|
|
|
"loggers": {
|
|
"infoblitz": {
|
|
"level": "INFO",
|
|
"handlers": ["console", "file_handler"],
|
|
"propagate": "no"
|
|
}
|
|
},
|
|
|
|
"root": {
|
|
"level": "DEBUG",
|
|
"handlers": ["console", "file_handler"]
|
|
}
|
|
}
|
|
"""
|
|
config = json.loads(default_config_as_json)
|
|
logging.config.dictConfig(config)
|
|
|
|
|
|
def sigint_handler(signum, frame):
|
|
print('CTRL+C pressed - exiting!')
|
|
sys.exit(0)
|
|
|
|
|
|
def _red(string):
|
|
return "\033[91m{}\033[00m".format(string)
|
|
|
|
|
|
def _green(string):
|
|
return "\033[92m{}\033[00m".format(string)
|
|
|
|
|
|
def _yellow(string):
|
|
return "\033[93m{}\033[00m".format(string)
|
|
|
|
|
|
def _gray(string):
|
|
return "\033[97m{}\033[00m".format(string)
|
|
|
|
|
|
def _cyan(string):
|
|
return "\033[96m{}\033[00m".format(string)
|
|
|
|
|
|
def _purple(string):
|
|
return "\033[95m{}\033[00m".format(string)
|
|
|
|
|
|
def clear():
|
|
# check and make call for specific operating system
|
|
if os.name == 'posix':
|
|
_ = os.system('clear') # Linux and Mac OS
|
|
|
|
|
|
def get_ipv4_addresses(ifname):
|
|
"""get_ipv4_addresses("eth0")"""
|
|
ip_addresses = []
|
|
_res = subprocess.check_output(["ip", "-4", "addr", "show", "dev", "{}".format(ifname), "scope", "global", "up"])
|
|
for line in _res.split(b"\n"):
|
|
match = re.match(b".+inet (.+)/.+", line)
|
|
if match:
|
|
ip_addresses.append(match.groups()[0].decode('utf-8'))
|
|
return ip_addresses
|
|
|
|
|
|
def get_ipv6_addresses(ifname):
|
|
"""get_ipv6_addresses("eth0")"""
|
|
ip_addresses = []
|
|
_res = subprocess.check_output(["ip", "-6", "addr", "show", "dev", "{}".format(ifname), "scope", "global", "up"])
|
|
for line in _res.split(b"\n"):
|
|
match = re.match(b".+inet6 (.+)/.+", line)
|
|
if match and b"mngtmpaddr" not in line:
|
|
ip_addresses.append(match.groups()[0].decode('utf-8'))
|
|
return ip_addresses
|
|
|
|
|
|
def port_check(address="127.0.0.1", port=8080, timeout=1.0):
|
|
if not isinstance(port, int):
|
|
return False
|
|
|
|
if not 0 < port < 65535:
|
|
return False
|
|
|
|
s = socket.socket()
|
|
s.settimeout(timeout)
|
|
is_open = False
|
|
try:
|
|
s.connect((address, port))
|
|
is_open = True
|
|
except Exception as err:
|
|
logger.warning("Something's wrong with {}:{}. Exception is {}".format(address, port, err))
|
|
finally:
|
|
s.close()
|
|
return is_open
|
|
|
|
|
|
def run_user(cmd, shell=True, timeout=None):
|
|
if shell: # shell is potentially considered a security risk (command injection when taking user input)
|
|
if not isinstance(cmd, str):
|
|
raise ValueError("cmd to execute must be passed in a single string when shell is True")
|
|
|
|
if cmd.split(" ")[0] == "sudo":
|
|
timeout = None
|
|
else:
|
|
if not isinstance(cmd, list):
|
|
raise ValueError("cmd to execute must be passed in as list of strings when shell is False")
|
|
|
|
if cmd[0] == "sudo":
|
|
timeout = None
|
|
|
|
try:
|
|
# subprocess.run requires Python3.5+
|
|
p = subprocess.run(cmd,
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
universal_newlines=True, shell=shell, timeout=timeout)
|
|
|
|
if p.returncode: # non-zero
|
|
result = p.stderr
|
|
success = False
|
|
timed_out = False
|
|
else:
|
|
result = p.stdout
|
|
success = True
|
|
timed_out = False
|
|
|
|
except subprocess.TimeoutExpired:
|
|
result = None
|
|
success = False
|
|
timed_out = True
|
|
|
|
return result, success, timed_out
|
|
|
|
|
|
class QuietBaseHTTPRequestHandler(BaseHTTPRequestHandler):
|
|
"""Quiet http request handler
|
|
Subclasses SimpleHTTPRequestHandler in order to overwrite the log_message
|
|
method, letting us reduce output generated by the handler. Only standard
|
|
messages are overwritten, so errors will still be displayed.
|
|
"""
|
|
|
|
def __init__(self, request, client_address, server, board=None, board_lock=None):
|
|
super().__init__(request, client_address, server)
|
|
self.board = board
|
|
self.board_lock = board_lock
|
|
|
|
def do_GET(self):
|
|
parts = urllib.parse.urlsplit(self.path)
|
|
|
|
if parts.path.endswith('/favicon.ico'):
|
|
ctype = 'image/x-icon'
|
|
content = bytes(base64.b64decode(
|
|
"AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAA"
|
|
"AAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoJiIKKCYiWgAAAAAAAAAA"
|
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
|
"AAAoJiIgKCYiuygmIhgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAoJiJDKCYi7SgmIlIAAAAAAAAAAAAA"
|
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoJiJz"
|
|
"KCYi/SgmIqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
|
"AAAAAAAAAAAAACgmIgooJiKmKCYi/ygmIuAoJiIOAAAAAAAAAAAAAAAA"
|
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgmIh8oJiLPKCYi/ygm"
|
|
"Iv4oJiI/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
|
"AAAAACgmIkEoJiLrKCYi/ygmIv8oJiKMAAAAAAAAAAAAAAAAAAAAAAAA"
|
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAACgmInAoJiL8KCYi/ygmIv8oJiL/"
|
|
"KCYiySgmIpwoJiJzKCYiKQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgm"
|
|
"IhYoJiJyKCYinCgmIsIoJiL8KCYi/ygmIv8oJiL/KCYinygmIgkAAAAA"
|
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoJiJTKCYi/ygm"
|
|
"Iv8oJiL5KCYiaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
|
"AAAAAAAAAAAoJiIeKCYi7ygmIv8oJiLjKCYiNwAAAAAAAAAAAAAAAAAA"
|
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoJiIDKCYixCgmIv8oJiK+"
|
|
"KCYiFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
|
"AAAAAAAAKCYigigmIv8oJiKJKCYiAwAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKCYiPigmIvAoJiJSAAAAAAAA"
|
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
|
"KCYiEigmIrooJiInAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAACgmIlooJiIMAAAAAAAAAAAAAAAA"
|
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//8AAP/3"
|
|
"AAD/7wAA/88AAP8fAAD+PwAA/D8AAPgfAAD4DwAA/j8AAPx/AAD4/wAA"
|
|
"8f8AAPf/AADv/wAA//8AAA=="
|
|
))
|
|
|
|
elif not parts.path.endswith('/'):
|
|
# redirect browser - doing basically what apache does
|
|
self.send_response(HTTPStatus.MOVED_PERMANENTLY)
|
|
new_parts = (parts[0], parts[1], parts[2] + '/',
|
|
parts[3], parts[4])
|
|
new_url = urllib.parse.urlunsplit(new_parts)
|
|
self.send_header("Location", new_url)
|
|
self.end_headers()
|
|
return None
|
|
|
|
elif parts.path.endswith('/json/'):
|
|
ctype = CTYPE_JSON
|
|
|
|
with self.board_lock:
|
|
# dict_content = {"hello": "world",
|
|
# "version": self.board.version.val,
|
|
# "lnd_external": self.board.lnd_external.val}
|
|
|
|
json_content = json.loads(json.dumps(self.board.all_metrics()))
|
|
content = bytes(json.dumps(json_content), "UTF-8")
|
|
|
|
else:
|
|
ctype = CTYPE_HTML
|
|
content = bytes("<html><head><title>RaspiBlitz Info Dashboard</title></head>", "UTF-8")
|
|
content += bytes("<body><h1>RaspiBlitz Info Dashboard</h1>", "UTF-8")
|
|
content += bytes("<p>The Dashboard Version is: v{}</p>".format(self.board.version.val), "UTF-8")
|
|
content += bytes("<p>The API Endpoint (JSON) is located here: <a href=\"/json/\">/json/</a></p>", "UTF-8")
|
|
content += bytes("</body></html>", "UTF-8")
|
|
|
|
self.send_response(200)
|
|
self.send_header("Content-type", ctype)
|
|
self.send_header("Content-Length", len(content))
|
|
self.end_headers()
|
|
|
|
self.wfile.write(content)
|
|
|
|
def log_message(self, *args):
|
|
"""Overwrite so messages are not logged to STDOUT"""
|
|
pass
|
|
|
|
def log_request(self, code='-', size='-'):
|
|
"""Log an accepted request.
|
|
|
|
This is called by send_response().
|
|
|
|
"""
|
|
if isinstance(code, HTTPStatus):
|
|
code = code.value
|
|
logger.debug("{} - - [{}] \"{}\" {} {}".format(self.address_string(), self.log_date_time_string(),
|
|
self.requestline, str(code), str(size)))
|
|
|
|
|
|
class ThreadedHTTPServer(object):
|
|
"""Runs BaseHTTPServer in a thread
|
|
Lets you start and stop an instance of SimpleHTTPServer.
|
|
"""
|
|
def __init__(self, host, port, board=None, board_lock=None, name=None):
|
|
"""Prepare thread and socket server
|
|
Creates the socket server that will use the HTTP request handler. Also
|
|
prepares the thread to run the serve_forever method of the socket
|
|
server as a daemon once it is started
|
|
"""
|
|
request_handler = QuietBaseHTTPRequestHandler
|
|
request_handler.board = board
|
|
request_handler.board_lock = board_lock
|
|
|
|
socketserver.TCPServer.allow_reuse_address = True
|
|
self.server = socketserver.TCPServer((host, port), request_handler)
|
|
self.server_thread = threading.Thread(name=name, target=self.server.serve_forever)
|
|
self.server_thread.daemon = True
|
|
|
|
def __enter__(self):
|
|
self.start()
|
|
return self
|
|
|
|
def __exit__(self, type, value, traceback):
|
|
self.stop()
|
|
|
|
def start(self):
|
|
"""Start the HTTP server
|
|
Starts the serve_forever method of Socket running the request handler
|
|
as a daemon thread
|
|
"""
|
|
self.server_thread.start()
|
|
|
|
def stop(self):
|
|
"""Stop the HTTP server
|
|
Stops the server and cleans up the port assigned to the socket
|
|
"""
|
|
self.server.shutdown()
|
|
self.server.server_close()
|
|
|
|
|
|
# Benefit of using class instead of function: Can use clean signature instead of kwargs..!
|
|
class DashboardPrinter(threading.Thread):
|
|
def __init__(self, group=None, target=None, name="DB_Printer",
|
|
board=None, board_lock=None, interval=None,
|
|
daemon=True, args=(), kwargs=None, ):
|
|
super().__init__(group, target, name, daemon=daemon, args=args, kwargs=kwargs)
|
|
self.board = board
|
|
self.board_lock = board_lock
|
|
self.interval = interval
|
|
|
|
def run(self):
|
|
while True:
|
|
|
|
start = time.time()
|
|
with self.board_lock:
|
|
end = time.time()
|
|
logger.info("Getting print lock took: {:.3f} seconds".format(end - start))
|
|
|
|
clear()
|
|
self.board.display()
|
|
|
|
time.sleep(self.interval)
|
|
|
|
|
|
class DashboardUpdater(threading.Thread):
|
|
def __init__(self, group=None, target=None, name="DB_Updater",
|
|
board=None, board_lock=None, interval=None,
|
|
daemon=True, args=(), kwargs=None, ):
|
|
super().__init__(group, target, name, daemon=daemon, args=args, kwargs=kwargs)
|
|
self.board = board
|
|
self.board_lock = board_lock
|
|
self.interval = interval
|
|
|
|
def run(self):
|
|
while True:
|
|
logger.debug("Updating Dashboard")
|
|
total_start = time.time()
|
|
|
|
start = time.time()
|
|
with self.board_lock:
|
|
end = time.time()
|
|
logger.debug("Getting update1 lock took: {:.3f} seconds".format(end - start))
|
|
|
|
self.board.update_load()
|
|
self.board.update_uptime()
|
|
self.board.update_cpu_temp()
|
|
self.board.update_memory()
|
|
self.board.update_storage()
|
|
self.board.update_ip_network_data()
|
|
time.sleep(0.05)
|
|
|
|
start = time.time()
|
|
with self.board_lock:
|
|
end = time.time()
|
|
logger.debug("Getting update2 lock took: {:.3f} seconds".format(end - start))
|
|
|
|
self.board.update_network()
|
|
|
|
self.board.update_bitcoin_dir()
|
|
self.board.read_bitcoin_config()
|
|
|
|
self.board.update_chain()
|
|
time.sleep(0.05)
|
|
|
|
start = time.time()
|
|
with self.board_lock:
|
|
end = time.time()
|
|
logger.debug("Getting update3 lock took: {:.3f} seconds".format(end - start))
|
|
|
|
self.board.update_bitcoin_binaries()
|
|
self.board.check_bitcoind_is_running()
|
|
self.board.update_bitcoin_daemon_version()
|
|
self.board.update_bitcoin_data()
|
|
time.sleep(0.05)
|
|
|
|
start = time.time()
|
|
with self.board_lock:
|
|
end = time.time()
|
|
logger.debug("Getting update4 lock took: {:.3f} seconds".format(end - start))
|
|
|
|
self.board.update_lnd_dirs()
|
|
self.board.read_lnd_config()
|
|
self.board.check_lnd_is_running()
|
|
self.board.update_lnd_wallet_is_locked()
|
|
self.board.update_lnd_alias()
|
|
self.board.update_lnd_data()
|
|
time.sleep(0.05)
|
|
|
|
start = time.time()
|
|
with self.board_lock:
|
|
end = time.time()
|
|
logger.debug("Getting update5 lock took: {:.3f} seconds".format(end - start))
|
|
|
|
self.board.update_public_ip()
|
|
self.board.update_bitcoin_public_port()
|
|
self.board.check_public_ip_lnd_port()
|
|
self.board.check_public_ip_bitcoin_port()
|
|
time.sleep(0.05)
|
|
|
|
total_end = time.time()
|
|
logger.info("Dashboard Value Update took: {:.3f} seconds".format(total_end - total_start))
|
|
|
|
time.sleep(self.interval)
|
|
|
|
|
|
class Metric(object):
|
|
STYLES = ["default", "red", "green", "yellow", "gray", "cyan"]
|
|
|
|
def __init__(self, val=None, txt=None, prefix=None, suffix=None, style="default", allow_empty=False):
|
|
self.val = val # "raw" value of Metric
|
|
self._txt = txt # text of "raw" value intended for printing to console (e.g. Memory in MiB instead of Bytes)
|
|
self.prefix = prefix
|
|
self.suffix = suffix
|
|
|
|
if style not in self.STYLES:
|
|
raise ValueError("unknown style!")
|
|
self.style = style
|
|
|
|
self.allow_empty = allow_empty # when this is False (default) "prefix + n/a + suffix" will be printed
|
|
|
|
@property
|
|
def txt(self):
|
|
if self._txt:
|
|
return self._txt
|
|
elif self._txt == "":
|
|
return ""
|
|
else:
|
|
if self.val:
|
|
return "{}".format(self.val)
|
|
else:
|
|
return None
|
|
|
|
@txt.setter
|
|
def txt(self, value):
|
|
self._txt = value
|
|
|
|
def __repr__(self):
|
|
if self.val:
|
|
return "<{0}: {1}>".format(self.__class__.__name__, self.val)
|
|
return "<{0}: n/a>".format(self.__class__.__name__)
|
|
|
|
def __str__(self):
|
|
return self.apply_style(string=self.to_txt(), style=self.style)
|
|
|
|
def apply_style(self, string, style=None):
|
|
if not style:
|
|
style = "default"
|
|
if "n/a" in string:
|
|
return _purple(string)
|
|
elif string:
|
|
if style == "red":
|
|
return _red(string)
|
|
elif style == "green":
|
|
return _green(string)
|
|
elif style == "yellow":
|
|
return _yellow(string)
|
|
elif style == "gray":
|
|
return _gray(string)
|
|
elif style == "cyan":
|
|
return _cyan(string)
|
|
else:
|
|
return string
|
|
else:
|
|
if self.allow_empty:
|
|
return ""
|
|
else:
|
|
return _purple(string)
|
|
|
|
def to_dct(self):
|
|
dct = dict()
|
|
# copy dict except for _txt and allow_empty
|
|
for k, v in self.__dict__.items():
|
|
if k in ["_txt", "allow_empty"]:
|
|
continue
|
|
dct.update({k: v})
|
|
# add txt representation
|
|
dct.update({"txt": self.to_txt()})
|
|
return dct
|
|
|
|
def to_txt(self):
|
|
if self.prefix is None:
|
|
prefix = ""
|
|
else:
|
|
prefix = self.prefix
|
|
|
|
if self.suffix is None:
|
|
suffix = ""
|
|
else:
|
|
suffix = self.suffix
|
|
|
|
if self.txt:
|
|
return "{0}{1}{2}".format(prefix, self.txt, suffix)
|
|
else:
|
|
if self.allow_empty:
|
|
return ""
|
|
else:
|
|
return "{0}n/a{1}".format(prefix, suffix)
|
|
|
|
|
|
class Dashboard(object):
|
|
def __init__(self, currency, interface=IF_NAME, timeout=TIMEOUT):
|
|
self.currency = CRYPTO_CURRENCIES[currency]
|
|
|
|
# Attributes that are used internally but not displayed directly
|
|
#
|
|
self.interface = interface
|
|
self.timeout = timeout
|
|
|
|
self.ipv4_addresses = list()
|
|
self.lpv6_addresses = list()
|
|
|
|
self.bitcoin_dir = None
|
|
self.lnd_dir = None
|
|
self.lnd_macaroon_dir = None
|
|
|
|
self.bitcoin_config = None
|
|
self.lnd_config = None
|
|
|
|
self.bitcoin_daemon = None
|
|
self.bitcoin_cli = None
|
|
|
|
self.bitcoin_local_adresses = list()
|
|
|
|
self.lnd_is_running = False
|
|
self.lnd_is_syned = False
|
|
self.lnd_wallet_is_locked = True
|
|
|
|
# Dashboard Metrics (all values that are displayed somewhere) - in use
|
|
#
|
|
self.name = Metric()
|
|
self.version = Metric()
|
|
|
|
# System data
|
|
self.load_one = Metric()
|
|
self.load_five = Metric()
|
|
self.load_fifteen = Metric()
|
|
self.cpu_temp = Metric(suffix="°C")
|
|
self.memory_total = Metric(suffix="M", style="green")
|
|
self.memory_avail = Metric(suffix="M", style="green")
|
|
|
|
# Storage
|
|
self.sd_total_abs = Metric(suffix="G", style="green")
|
|
self.sd_free_abs = Metric(suffix="G", style="green")
|
|
self.sd_free = Metric(suffix="%", style="green")
|
|
self.hdd_total_abs = Metric(suffix="G", style="green")
|
|
self.hdd_free_abs = Metric(suffix="G", style="green")
|
|
self.hdd_free = Metric(suffix="%", style="green")
|
|
|
|
# IP Network/Traffic Info
|
|
self.local_ip = Metric(style="green")
|
|
self.network_tx = Metric()
|
|
self.network_rx = Metric()
|
|
|
|
self.public_ip = Metric(style="green")
|
|
self.public_bitcoin_port = Metric(style="green")
|
|
self.public_bitcoin_port_status = Metric(allow_empty=True)
|
|
|
|
# Bitcoin / Chain Info
|
|
self.network = Metric(style="default")
|
|
self.chain = Metric("main", suffix="net", style="green")
|
|
|
|
self.bitcoin_cli_version = Metric(style="green")
|
|
self.bitcoin_version = Metric(style="green")
|
|
self.bitcoin_is_running = False
|
|
self.bitcoin_log_msgs = None
|
|
|
|
self.sync_behind = Metric()
|
|
self.sync_percentage = Metric(suffix="%", style="green")
|
|
self.sync_status = Metric()
|
|
|
|
# self.last_block = Metric()
|
|
self.block_height = Metric()
|
|
self.btc_line2 = Metric()
|
|
self.mempool = Metric()
|
|
|
|
# Tor (The Onion Router)
|
|
self.tor_active = Metric(allow_empty=True)
|
|
self.onion_addr = Metric()
|
|
|
|
self.lnd_alias = Metric(style="green")
|
|
self.lnd_version = Metric(style="green")
|
|
self.lnd_lncli_version = Metric(style="green")
|
|
self.lnd_base_msg = Metric(allow_empty=True)
|
|
self.lnd_channel_msg = Metric(allow_empty=True)
|
|
self.lnd_channel_balance = Metric()
|
|
self.lnd_channels_online = Metric()
|
|
self.lnd_channels_total = Metric()
|
|
self.lnd_external = Metric(style="yellow")
|
|
self.public_ip_lnd_port_status = Metric(allow_empty=True)
|
|
|
|
self.lnd_wallet_balance = Metric()
|
|
self.lnd_wallet_lock_status = Metric()
|
|
|
|
# Dashboard Metrics (all values that are displayed somewhere) - currently not in use
|
|
#
|
|
self.uptime = Metric()
|
|
|
|
self.bitcoin_ipv4_reachable = Metric()
|
|
self.bitcoin_ipv4_limited = Metric()
|
|
self.bitcoin_ipv6_reachable = Metric()
|
|
self.bitcoin_ipv6_limited = Metric()
|
|
self.bitcoin_onion_reachable = Metric()
|
|
self.bitcoin_onion_limited = Metric()
|
|
|
|
def __repr__(self):
|
|
return "<{0}: Version: {1}>".format(self.__class__.__name__, self.version)
|
|
|
|
def all_metrics(self):
|
|
"""Introspection: return list of all attributes that are Metric instances"""
|
|
return [{m: getattr(self, m).to_dct()} for m in [a for a in dir(self)] if isinstance(getattr(self, m), Metric)]
|
|
|
|
def update_load(self):
|
|
one, five, fifteen = os.getloadavg()
|
|
|
|
_cpu_count = os.cpu_count()
|
|
self.load_one.val = one
|
|
self.load_one.txt = "{:.2f}".format(self.load_one.val)
|
|
self.load_five.val = five
|
|
self.load_five.txt = "{:.2f}".format(self.load_five.val)
|
|
self.load_fifteen.val = fifteen
|
|
self.load_fifteen.txt = "{:.2f}".format(self.load_fifteen.val)
|
|
|
|
if float(self.load_one.val) < _cpu_count * 0.5:
|
|
self.load_one.style = "green"
|
|
elif float(self.load_one.val) < _cpu_count:
|
|
self.load_one.style = "yellow"
|
|
else:
|
|
self.load_one.style = "red"
|
|
|
|
if float(self.load_five.val) < _cpu_count * 0.5:
|
|
self.load_five.style = "green"
|
|
elif float(self.load_five.val) < _cpu_count:
|
|
self.load_five.style = "yellow"
|
|
else:
|
|
self.load_five.style = "red"
|
|
|
|
if float(self.load_fifteen.val) < _cpu_count * 0.5:
|
|
self.load_fifteen.style = "green"
|
|
elif float(self.load_fifteen.val) < _cpu_count:
|
|
self.load_fifteen.style = "yellow"
|
|
else:
|
|
self.load_fifteen.style = "red"
|
|
|
|
def update_uptime(self):
|
|
if not os.path.exists("/proc/uptime"):
|
|
return
|
|
|
|
with open("/proc/uptime", "r") as f:
|
|
_uptime_seconds = float(f.readline().split()[0])
|
|
|
|
self.uptime.val = int(timedelta(seconds=_uptime_seconds).total_seconds())
|
|
self.uptime.txt = "{}".format(self.uptime.val)
|
|
|
|
def update_cpu_temp(self):
|
|
if not os.path.exists("/sys/class/thermal/thermal_zone0/temp"):
|
|
return
|
|
|
|
with open("/sys/class/thermal/thermal_zone0/temp", "r") as f:
|
|
content = int(f.readline().split("\n")[0])
|
|
self.cpu_temp.val = content / 1000.0
|
|
self.cpu_temp.txt = "{:.0f}".format(self.cpu_temp.val)
|
|
|
|
if self.cpu_temp.val > 80.0:
|
|
self.cpu_temp.style = "red"
|
|
|
|
def update_memory(self):
|
|
if not os.path.exists("/proc/meminfo"):
|
|
return
|
|
|
|
with open("/proc/meminfo", "r") as f:
|
|
content = f.readlines()
|
|
_meminfo = dict((i.split()[0].rstrip(':'), int(i.split()[1])) for i in content)
|
|
|
|
self.memory_total.val = _meminfo['MemTotal'] # e.g. 949440
|
|
self.memory_total.txt = "{:.0f}".format(self.memory_total.val / 1024)
|
|
|
|
self.memory_avail.val = _meminfo['MemAvailable'] # e.g. 457424
|
|
self.memory_avail.txt = "{:.0f}".format(self.memory_avail.val / 1024)
|
|
|
|
if self.memory_avail.val < 100000:
|
|
self.memory_total.style = "yellow"
|
|
self.memory_avail.style = "yellow"
|
|
|
|
def update_storage(self):
|
|
"""use statvfs interface to get free/used disk space
|
|
|
|
statvfs.f_frsize * statvfs.f_blocks # Size of filesystem in bytes
|
|
statvfs.f_frsize * statvfs.f_bfree # Actual number of free bytes
|
|
statvfs.f_frsize * statvfs.f_bavail # Number of free bytes that ordinary users are allowed to use
|
|
"""
|
|
if not os.path.exists("/"):
|
|
return
|
|
|
|
statvfs_sd = os.statvfs('/')
|
|
_sd_total_abs = statvfs_sd.f_frsize * statvfs_sd.f_blocks
|
|
_sd_free_abs = statvfs_sd.f_frsize * statvfs_sd.f_bavail
|
|
_sd_free = _sd_free_abs / _sd_total_abs * 100
|
|
|
|
if not os.path.exists("/mnt/hdd"):
|
|
return
|
|
|
|
statvfs_hdd = os.statvfs("/mnt/hdd")
|
|
_hdd_total_abs = statvfs_hdd.f_frsize * statvfs_hdd.f_blocks
|
|
# _hdd_free_abs = statvfs_hdd.f_frsize * statvfs_hdd.f_bfree
|
|
_hdd_free_abs = statvfs_hdd.f_frsize * statvfs_hdd.f_bavail
|
|
_hdd_free = _hdd_free_abs / _hdd_total_abs * 100
|
|
|
|
self.sd_total_abs.val = _sd_total_abs / 1024.0 / 1024.0 / 1024.0
|
|
self.sd_total_abs.txt = "{:.0f}".format(self.sd_total_abs.val)
|
|
|
|
self.sd_free_abs.val = _sd_free_abs / 1024.0 / 1024.0 / 1024.0
|
|
self.sd_free_abs.txt = "{:.0f}".format(self.sd_free_abs.val)
|
|
|
|
self.sd_free.val = _sd_free
|
|
self.sd_free.txt = "{:.0f}".format(self.sd_free.val)
|
|
|
|
self.hdd_total_abs.val = _hdd_total_abs / 1024.0 / 1024.0 / 1024.0
|
|
self.hdd_total_abs.txt = "{:.0f}".format(self.hdd_total_abs.val)
|
|
|
|
self.hdd_free_abs.val = _hdd_free_abs / 1024.0 / 1024.0 / 1024.0
|
|
self.hdd_free_abs.txt = "{:.0f}".format(self.hdd_free_abs.val)
|
|
|
|
self.hdd_free.val = _hdd_free
|
|
self.hdd_free.txt = "{:.0f}".format(self.hdd_free.val)
|
|
|
|
if self.hdd_free.val < 20:
|
|
self.hdd_free.style = "yellow"
|
|
elif self.hdd_free.val < 10:
|
|
self.hdd_free.style = "red"
|
|
|
|
def update_ip_network_data(self):
|
|
self.ipv4_addresses = get_ipv4_addresses(self.interface)
|
|
self.ipv6_addresses = get_ipv6_addresses(self.interface)
|
|
self.local_ip.val = self.ipv4_addresses[0]
|
|
|
|
if not os.path.exists("/sys/class/net/{0}/statistics/rx_bytes".format(self.interface)):
|
|
return
|
|
|
|
with open("/sys/class/net/{0}/statistics/rx_bytes".format(self.interface), 'r') as f:
|
|
_rx_bytes = float(f.readline().split()[0])
|
|
|
|
if not os.path.exists("/sys/class/net/{0}/statistics/tx_bytes".format(self.interface)):
|
|
return
|
|
|
|
with open("/sys/class/net/{0}/statistics/tx_bytes".format(self.interface), 'r') as f:
|
|
_tx_bytes = float(f.readline().split()[0])
|
|
|
|
if _tx_bytes / 1024.0 / 1024.0 / 1024.0 / 1024.0 > 1:
|
|
_tx_suffix = "TiB"
|
|
_tx_bytes_val = _tx_bytes / 1024.0 / 1024.0 / 1024.0 / 1024.0
|
|
elif _tx_bytes / 1024.0 / 1024.0 / 1024.0 > 1:
|
|
_tx_suffix = "GiB"
|
|
_tx_bytes_val = _tx_bytes / 1024.0 / 1024.0 / 1024.0
|
|
elif _tx_bytes / 1024.0 / 1024.0 > 1:
|
|
_tx_suffix = "MiB"
|
|
_tx_bytes_val = _tx_bytes / 1024.0 / 1024.0
|
|
elif _tx_bytes / 1024.0 > 1:
|
|
_tx_suffix = "KiB"
|
|
_tx_bytes_val = _tx_bytes / 1024.0
|
|
else:
|
|
_tx_suffix = "Byte"
|
|
_tx_bytes_val = _tx_bytes
|
|
|
|
if _rx_bytes / 1024.0 / 1024.0 / 1024.0 / 1024.0 > 1:
|
|
_rx_suffix = "TiB"
|
|
_rx_bytes_val = _rx_bytes / 1024.0 / 1024.0 / 1024.0 / 1024.0
|
|
elif _rx_bytes / 1024.0 / 1024.0 / 1024.0 > 1:
|
|
_rx_suffix = "GiB"
|
|
_rx_bytes_val = _rx_bytes / 1024.0 / 1024.0 / 1024.0
|
|
elif _rx_bytes / 1024.0 / 1024.0 > 1:
|
|
_rx_suffix = "MiB"
|
|
_rx_bytes_val = _rx_bytes / 1024.0 / 1024.0
|
|
elif _rx_bytes / 1024.0 > 1:
|
|
_rx_suffix = "KiB"
|
|
_rx_bytes_val = _rx_bytes / 1024.0
|
|
else:
|
|
_rx_suffix = "Byte"
|
|
_rx_bytes_val = _rx_bytes
|
|
|
|
self.network_rx = Metric(_rx_bytes_val, txt="{:.1f}".format(_rx_bytes_val), suffix=_rx_suffix)
|
|
self.network_tx = Metric(_tx_bytes_val, txt="{:.1f}".format(_tx_bytes_val), suffix=_tx_suffix)
|
|
|
|
def update_network(self):
|
|
# load network (bitcoin, litecoin, ..?!)
|
|
with open(NETWORK_FILE) as f:
|
|
content = f.readline().split("\n")[0]
|
|
if content not in list(CRYPTO_CURRENCIES.keys()):
|
|
raise ValueError("unexpected value in {}: {}".format(NETWORK_FILE, content))
|
|
self.network.val = content
|
|
|
|
if not self.network.val == self.currency["title"].lower():
|
|
raise ValueError("Crypto Currency in {} does not match selection!".format(NETWORK_FILE))
|
|
|
|
def update_bitcoin_dir(self):
|
|
self.bitcoin_dir = "{0}/.{1}".format(BITCOIN_HOME, self.network.val)
|
|
|
|
def read_bitcoin_config(self):
|
|
_bitcoin_conf = "{0}/{1}.conf".format(self.bitcoin_dir, self.network.val)
|
|
if not os.path.exists(_bitcoin_conf):
|
|
logger.warning("{} config not found: {}".format(self.currency["title"], _bitcoin_conf))
|
|
return
|
|
|
|
# need to do a little "hack" here as ConfigParser expects sections which bitcoin.conf does not have
|
|
with open(_bitcoin_conf, 'r') as f:
|
|
_config_string = '[DEFAULT]\n' + f.read()
|
|
|
|
config = configparser.ConfigParser(strict=False)
|
|
config.read_string(_config_string)
|
|
self.bitcoin_config = config # access with self.bitcoin_config["DEFAULT"]...
|
|
|
|
def update_chain(self):
|
|
# get chain (mainnet or testnet)
|
|
try:
|
|
if self.bitcoin_config["DEFAULT"]["testnet"] == "1":
|
|
self.chain.val = "test"
|
|
except KeyError:
|
|
pass # this is expected - if testnet is not present then mainnet is active
|
|
except TypeError as err: # catch if None, expected index/key not present
|
|
logger.warning("Error: {}".format(err))
|
|
|
|
def update_bitcoin_binaries(self):
|
|
cmds = "which {}d".format(self.network.val)
|
|
_bitcoind, success, timed_out = run_user(cmds, timeout=self.timeout)
|
|
if success:
|
|
try:
|
|
self.bitcoin_daemon = _bitcoind.split("\n")[0]
|
|
except IndexError as err:
|
|
logger.warning("Error: {}".format(err))
|
|
else:
|
|
raise Exception("could not find network chain daemin tool: {}d".format(self.network.val))
|
|
|
|
cmds = "which {}-cli".format(self.network.val)
|
|
_bitcoin_cli, success, timed_out = run_user(cmds, timeout=self.timeout)
|
|
if success:
|
|
try:
|
|
self.bitcoin_cli = _bitcoin_cli.split("\n")[0]
|
|
except IndexError as err:
|
|
logger.warning("Error: {}".format(err))
|
|
else:
|
|
raise Exception("could not find network chain cli tool: {}-cli".format(self.network.val))
|
|
|
|
def check_bitcoind_is_running(self):
|
|
# check if bitcoind is running
|
|
cmds = "ps aux | grep -e \"{}.*-daemon\" | grep -v grep | wc -l".format(self.currency['daemon'])
|
|
_bitcoind_running, success, timed_out = run_user(cmds, timeout=self.timeout)
|
|
if success:
|
|
try:
|
|
if _bitcoind_running.split("\n")[0] == "0":
|
|
self.bitcoin_is_running = False
|
|
logger.warning("{} is not running".format(self.currency['daemon']))
|
|
return True
|
|
else:
|
|
self.bitcoin_is_running = True
|
|
return True
|
|
except IndexError as err:
|
|
logger.warning("Error: {}".format(err))
|
|
|
|
return False
|
|
|
|
def update_bitcoind_log(self):
|
|
# check bitcoind log
|
|
if self.chain.val == "test":
|
|
cmds = "sudo -u bitcoin tail -n 20 {}/{}/debug.log".format(self.bitcoin_dir, self.currency["testnet_dir"])
|
|
else:
|
|
cmds = "sudo -u bitcoin tail -n 20 {}/debug.log".format(self.bitcoin_dir)
|
|
_bitcoind_log, success, timed_out = run_user(cmds, timeout=self.timeout)
|
|
if success:
|
|
try:
|
|
self.bitcoin_log_msgs = [_bitcoind_log.split("\n")[-3], _bitcoind_log.split("\n")[-2]]
|
|
except IndexError as err:
|
|
logger.warning("Error: {}".format(err))
|
|
|
|
def update_bitcoin_daemon_version(self):
|
|
# get bitcoin version from daemon (bitcoind -version)
|
|
cmds = "{} -datadir={} -version".format(self.bitcoin_cli, self.bitcoin_dir)
|
|
_version_info, success, timed_out = run_user(cmds, timeout=self.timeout)
|
|
if success:
|
|
self.bitcoin_version.val = re.match("^.* v(.*$)", _version_info).groups()[0]
|
|
self.bitcoin_version.prefix = "v"
|
|
|
|
def update_bitcoin_data(self):
|
|
self.sync_status.val = None
|
|
self.sync_status.txt = None
|
|
self.sync_status.style = "default"
|
|
self.sync_percentage.val = None
|
|
self.sync_percentage.txt = None
|
|
self.sync_percentage.style = "green"
|
|
|
|
# block count/height
|
|
cmds = "{} -datadir={} getblockcount".format(self.bitcoin_cli, self.bitcoin_dir)
|
|
_block_count, success, timed_out = run_user(cmds, timeout=self.timeout)
|
|
if success:
|
|
# reset self.bitcoin_log_msgs - which might have been set by update_bitcoind_log()
|
|
self.bitcoin_log_msgs = None
|
|
try:
|
|
self.block_height.val = int(_block_count.split("\n")[0])
|
|
self.block_height.txt = "{}".format(self.block_height.val)
|
|
except IndexError as err:
|
|
logger.warning("Error: {}".format(err))
|
|
|
|
else: # unable to run getblockcount.. maybe bitcoind is processing a long running job (e.g. txindex) TODO
|
|
# try:
|
|
# last_line = _block_count.split("\n")[-2]
|
|
# except AttributeError:
|
|
# pass
|
|
|
|
self.update_bitcoind_log()
|
|
|
|
# get blockchain (sync) status/percentage
|
|
cmds = "{} -datadir={} getblockchaininfo".format(self.bitcoin_cli, self.bitcoin_dir)
|
|
_chain_info, success, timed_out = run_user(cmds, timeout=self.timeout)
|
|
if success:
|
|
try:
|
|
_block_verified = json.loads(_chain_info)["blocks"]
|
|
_block_diff = int(self.block_height.val) - int(_block_verified)
|
|
|
|
_progress = json.loads(_chain_info)["verificationprogress"]
|
|
|
|
self.sync_percentage.val = _progress
|
|
self.sync_percentage.txt = "{:.2f}".format(self.sync_percentage.val * 100)
|
|
if _block_diff == 0: # fully synced
|
|
self.sync_status.val = _block_diff
|
|
self.sync_status.txt = "OK"
|
|
self.sync_status.style = "green"
|
|
self.sync_behind = " "
|
|
elif _block_diff == 1: # fully synced
|
|
self.sync_status.val = _block_diff
|
|
self.sync_status.txt = "OK"
|
|
self.sync_status.style = "green"
|
|
self.sync_behind = "-1 block"
|
|
elif _block_diff <= 10:
|
|
self.sync_status.val = _block_diff
|
|
self.sync_status.txt = "catchup"
|
|
self.sync_status.style = "red"
|
|
self.sync_percentage.style = "red"
|
|
self.sync_behind = "-{} blocks".format(_block_diff)
|
|
else:
|
|
self.sync_status.val = _block_diff
|
|
self.sync_status.txt = "progress"
|
|
self.sync_status.style = "red"
|
|
self.sync_percentage.style = "red"
|
|
self.sync_behind = "-{} blocks".format(_block_diff)
|
|
|
|
except (KeyError, TypeError) as err: # catch if result is None or expected key not present
|
|
logger.warning("Error: {}".format(err))
|
|
else:
|
|
logger.debug("Error: getblockchaininfo")
|
|
|
|
# mempool info
|
|
cmds = "{} -datadir={} getmempoolinfo".format(self.bitcoin_cli, self.bitcoin_dir)
|
|
_mempool_info, success, timed_out = run_user(cmds, timeout=self.timeout)
|
|
if success:
|
|
try:
|
|
self.mempool.val = json.loads(_mempool_info)["size"]
|
|
except (KeyError, TypeError) as err: # catch if None, expected index/key not present
|
|
logger.warning("Error: {}".format(err))
|
|
|
|
# bitcoin network connectivity info
|
|
cmds = "{} -datadir={} getnetworkinfo".format(self.bitcoin_cli, self.bitcoin_dir)
|
|
_network_info, success, timed_out = run_user(cmds, timeout=self.timeout)
|
|
if success:
|
|
try:
|
|
for nw in json.loads(_network_info)["networks"]:
|
|
if nw["name"] == "ipv4":
|
|
if nw["reachable"]:
|
|
self.bitcoin_ipv4_reachable.val = True
|
|
self.bitcoin_ipv4_reachable.txt = "True"
|
|
self.bitcoin_ipv4_reachable.style = "green"
|
|
else:
|
|
self.bitcoin_ipv4_reachable.val = False
|
|
self.bitcoin_ipv4_reachable.txt = "False"
|
|
self.bitcoin_ipv4_reachable.style = "red"
|
|
if nw["limited"]:
|
|
self.bitcoin_ipv4_limited.val = True
|
|
self.bitcoin_ipv4_limited.txt = "True"
|
|
self.bitcoin_ipv4_limited.style = "green"
|
|
else:
|
|
self.bitcoin_ipv4_limited.val = False
|
|
self.bitcoin_ipv4_limited.txt = "False"
|
|
self.bitcoin_ipv4_limited.style = "red"
|
|
|
|
if nw["name"] == "ipv6":
|
|
if nw["reachable"]:
|
|
self.bitcoin_ipv6_reachable.val = True
|
|
self.bitcoin_ipv6_reachable.txt = "True"
|
|
self.bitcoin_ipv6_reachable.style = "green"
|
|
else:
|
|
self.bitcoin_ipv6_reachable.val = False
|
|
self.bitcoin_ipv6_reachable.txt = "False"
|
|
self.bitcoin_ipv6_reachable.style = "red"
|
|
|
|
if nw["limited"]:
|
|
self.bitcoin_ipv6_limited.val = True
|
|
self.bitcoin_ipv6_limited.txt = "True"
|
|
self.bitcoin_ipv6_limited.style = "green"
|
|
else:
|
|
self.bitcoin_ipv6_limited.val = False
|
|
self.bitcoin_ipv6_limited.txt = "False"
|
|
self.bitcoin_ipv6_limited.style = "red"
|
|
|
|
if nw["name"] == "onion":
|
|
if nw["reachable"]:
|
|
self.bitcoin_onion_reachable.val = True
|
|
self.bitcoin_onion_reachable.txt = "True"
|
|
self.bitcoin_onion_reachable.style = "green"
|
|
else:
|
|
self.bitcoin_onion_reachable.val = False
|
|
self.bitcoin_onion_reachable.txt = "False"
|
|
self.bitcoin_onion_reachable.style = "red"
|
|
|
|
if nw["limited"]:
|
|
self.bitcoin_onion_limited.val = True
|
|
self.bitcoin_onion_limited.txt = "True"
|
|
self.bitcoin_onion_limited.style = "green"
|
|
else:
|
|
self.bitcoin_onion_limited.val = False
|
|
self.bitcoin_onion_limited.txt = "False"
|
|
self.bitcoin_onion_limited.style = "red"
|
|
|
|
except (KeyError, TypeError) as err: # catch if None, expected index/key not present
|
|
logger.warning("Error: {}".format(err))
|
|
|
|
self.bitcoin_local_adresses = list()
|
|
try:
|
|
for la in json.loads(_network_info)["localaddresses"]:
|
|
if ":" in la["address"]:
|
|
if la["address"] in self.ipv6_addresses:
|
|
self.bitcoin_local_adresses.append("[{}]:{}".format(la["address"], la["port"]))
|
|
elif ".onion" in la["address"]:
|
|
self.bitcoin_local_adresses.append("{}:{}".format(la["address"], la["port"]))
|
|
|
|
if self.bitcoin_onion_reachable:
|
|
self.tor_active = Metric("+ Tor")
|
|
else:
|
|
self.tor_active = Metric("+ Tor?")
|
|
else:
|
|
self.bitcoin_local_adresses.append("{}:{}".format(la["address"], la["port"]))
|
|
|
|
except (KeyError, TypeError) as err: # catch if None, expected index/key not present
|
|
logger.warning("Error: {}".format(err))
|
|
|
|
def update_lnd_dirs(self):
|
|
# set datadir - requires network and chain to be set/checked
|
|
self.lnd_dir = "/home/bitcoin/.lnd"
|
|
self.lnd_macaroon_dir = "/home/bitcoin/.lnd/data/chain/{0}/{1}net".format(self.network.val, self.chain.val)
|
|
|
|
def read_lnd_config(self):
|
|
_lnd_conf = "{}/lnd.conf".format(self.lnd_dir)
|
|
if not os.path.exists(_lnd_conf):
|
|
return
|
|
|
|
config = configparser.ConfigParser(strict=False)
|
|
config.read(_lnd_conf)
|
|
self.lnd_config = config
|
|
|
|
def check_lnd_is_running(self):
|
|
# check if lnd is running
|
|
cmds = "ps aux | grep -e \"bin\/lnd\" | grep -v grep | wc -l"
|
|
_lnd_running, success, timed_out = run_user(cmds, timeout=self.timeout)
|
|
if success:
|
|
try:
|
|
if _lnd_running.split("\n")[0] == "0":
|
|
self.lnd_is_running = False
|
|
# print("WARN: LND not running!")
|
|
else:
|
|
self.lnd_is_running = True
|
|
return True
|
|
except IndexError as err:
|
|
logger.warning("Error: {}".format(err))
|
|
|
|
return False
|
|
|
|
def update_lnd_wallet_is_locked(self):
|
|
# LN Wallet Lock Status
|
|
cmds = "sudo tail -n 1 /mnt/hdd/lnd/logs/{0}/{1}net/lnd.log".format(self.network.val, self.chain.val)
|
|
_ln_lock_status_log, success, timed_out = run_user(cmds)
|
|
if success:
|
|
if re.match(".*unlock.*", _ln_lock_status_log):
|
|
self.lnd_wallet_lock_status = Metric("\U0001F512", style="red")
|
|
self.lnd_wallet_lock_status.val = True
|
|
self.lnd_wallet_is_locked = True
|
|
else:
|
|
self.lnd_wallet_lock_status = Metric("\U0001F513", style="green")
|
|
self.lnd_wallet_lock_status.val = False
|
|
self.lnd_wallet_is_locked = False
|
|
return False
|
|
|
|
return True
|
|
|
|
# def _update_lncli_version(self):
|
|
# # get lnd client version client
|
|
# cmds = "/usr/local/bin/lncli --version"
|
|
# _ln_client_version, success, timed_out = run_user(cmds, timeout=self.timeout)
|
|
# if success:
|
|
# try:
|
|
# line = _ln_client_version.split("\n")[0]
|
|
# self.lnd_lncli_version.raw = line.split(" ")[2]
|
|
# self.lnd_lncli_version = self.lnd_lncli_version.raw
|
|
# except IndexError as err:
|
|
# logger.warning("Error: {}".format(err))
|
|
|
|
def update_lnd_alias(self):
|
|
try:
|
|
self.lnd_alias.val = self.lnd_config["Application Options"]["alias"]
|
|
except (KeyError, TypeError) as err: # catch if None, expected index/key not present
|
|
logger.warning("Error: {}".format(err))
|
|
|
|
def update_lnd_data(self):
|
|
# reset any data that might be changed in this method
|
|
self.lnd_base_msg.val = None
|
|
self.lnd_base_msg.txt = None
|
|
self.lnd_base_msg.style = "default"
|
|
self.lnd_version.val = None
|
|
self.lnd_version.txt = None
|
|
self.lnd_version.style = "green"
|
|
self.lnd_external.val = None
|
|
self.lnd_external.txt = None
|
|
self.lnd_external.style = "yellow"
|
|
self.lnd_channel_msg.val = None
|
|
self.lnd_channel_msg.txt = None
|
|
self.lnd_channel_msg.style = "default"
|
|
self.lnd_wallet_balance.val = None
|
|
self.lnd_wallet_balance.txt = None
|
|
self.lnd_wallet_balance.style = "default"
|
|
self.lnd_channel_balance.val = None
|
|
self.lnd_channel_balance.txt = None
|
|
self.lnd_channel_balance.style = "default"
|
|
self.lnd_channels_online.val = None
|
|
self.lnd_channels_online.txt = None
|
|
self.lnd_channels_online.style = "default"
|
|
self.lnd_channels_total.val = None
|
|
self.lnd_channels_total.txt = None
|
|
self.lnd_channels_total.style = "default"
|
|
self.lnd_is_syned = False
|
|
|
|
# If LND is not running exit
|
|
if not self.lnd_is_running:
|
|
return
|
|
|
|
# If LN wallet is locked exit
|
|
if self.lnd_wallet_is_locked:
|
|
self.lnd_base_msg.val = "\U0001F512Locked"
|
|
self.lnd_base_msg.style = "red"
|
|
return
|
|
|
|
cmds = ("sudo -u bitcoin /usr/local/bin/lncli --macaroonpath={}/readonly.macaroon "
|
|
"--tlscertpath={}/tls.cert getinfo 2>/dev/null".format(self.lnd_macaroon_dir, self.lnd_dir))
|
|
_ln_get_info, success, timed_out = run_user(cmds)
|
|
if success:
|
|
if not _ln_get_info:
|
|
self.lnd_base_msg.val = "Not Started/Ready Yet"
|
|
self.lnd_base_msg.style = "red"
|
|
|
|
else:
|
|
try:
|
|
self.lnd_version.val = json.loads(_ln_get_info)["version"].split(" ")[0]
|
|
except (IndexError, KeyError, TypeError) as err: # catch if None, expected index/key not present
|
|
logger.warning("Error: {}".format(err))
|
|
|
|
try:
|
|
self.lnd_external.val = json.loads(_ln_get_info)["uris"][0]
|
|
except (IndexError, KeyError, TypeError) as err: # catch if None, expected index/key not present
|
|
logger.warning("Error: {}".format(err))
|
|
|
|
try:
|
|
if not json.loads(_ln_get_info)["synced_to_chain"]:
|
|
self.lnd_is_syned = False
|
|
else:
|
|
self.lnd_is_syned = True
|
|
except (KeyError, TypeError) as err: # catch if None, expected index/key not present
|
|
logger.warning("Error: {}".format(err))
|
|
|
|
if self.lnd_is_syned:
|
|
# synched_to_chain is True
|
|
cmds = ("sudo -u bitcoin /usr/local/bin/lncli "
|
|
"--macaroonpath={}/readonly.macaroon --tlscertpath={}/tls.cert "
|
|
"walletbalance 2>/dev/null".format(self.lnd_macaroon_dir, self.lnd_dir))
|
|
_ln_wallet_balance, success, timed_out = run_user(cmds)
|
|
if success:
|
|
try:
|
|
self.lnd_wallet_balance.val = int(json.loads(_ln_wallet_balance)["confirmed_balance"])
|
|
self.lnd_wallet_balance.txt = "{}".format(self.lnd_wallet_balance.val)
|
|
self.lnd_wallet_balance.style = "yellow"
|
|
except (KeyError, TypeError) as err: # catch if None, expected index/key not present
|
|
logger.warning("Error: {}".format(err))
|
|
self.lnd_wallet_balance.val = None
|
|
self.lnd_wallet_balance.txt = None
|
|
|
|
cmds = ("sudo -u bitcoin /usr/local/bin/lncli "
|
|
"--macaroonpath={}/readonly.macaroon --tlscertpath={}/tls.cert "
|
|
"channelbalance 2>/dev/null".format(self.lnd_macaroon_dir, self.lnd_dir))
|
|
_ln_channel_balance, success, timed_out = run_user(cmds)
|
|
if success:
|
|
try:
|
|
self.lnd_channel_balance.val = int(json.loads(_ln_channel_balance)["balance"])
|
|
self.lnd_channel_balance.txt = "{}".format(self.lnd_channel_balance.val)
|
|
self.lnd_channel_balance.style = "yellow"
|
|
except (KeyError, TypeError) as err: # catch if None, expected index/key not present
|
|
logger.warning("Error: {}".format(err))
|
|
self.lnd_channel_balance.val = None
|
|
self.lnd_channel_balance.txt = None
|
|
|
|
try:
|
|
self.lnd_channels_online.val = int(json.loads(_ln_get_info)["num_active_channels"])
|
|
self.lnd_channels_online.txt = "{}".format(self.lnd_channels_online.val)
|
|
except (KeyError, TypeError) as err: # catch if None, expected index/key not present
|
|
logger.warning("Error: {}".format(err))
|
|
self.lnd_channels_online.val = None
|
|
self.lnd_channels_online.txt = None
|
|
except json.decoder.JSONDecodeError as err: # catch if LND is unable to respond
|
|
logger.warning("Error: {}".format(err))
|
|
self.lnd_channels_online.val = None
|
|
self.lnd_channels_online.txt = None
|
|
|
|
cmds = ("sudo -u bitcoin /usr/local/bin/lncli "
|
|
"--macaroonpath={}/readonly.macaroon --tlscertpath={}/tls.cert "
|
|
"listchannels 2>/dev/null".format(self.lnd_macaroon_dir, self.lnd_dir))
|
|
_ln_list_channels, success, timed_out = run_user(cmds)
|
|
if success:
|
|
try:
|
|
self.lnd_channels_total.val = len(json.loads(_ln_list_channels)["channels"])
|
|
except (KeyError, TypeError) as err: # catch if None, expected index/key not present
|
|
logger.warning("Error: {}".format(err))
|
|
|
|
else: # LND is not synched
|
|
# is Bitcoind running?!
|
|
if not self.bitcoin_is_running:
|
|
self.lnd_base_msg.val = "{} not running or not ready".format(self.currency['daemon'])
|
|
self.lnd_base_msg.vale = self.lnd_base_msg.val
|
|
self.lnd_base_msg.style = "red"
|
|
return
|
|
|
|
self.lnd_base_msg.val = "Waiting for chain sync"
|
|
self.lnd_base_msg.txt = self.lnd_base_msg.val
|
|
self.lnd_base_msg.style = "red"
|
|
|
|
cmds = ("sudo -u bitcoin tail -n 10000 "
|
|
"/mnt/hdd/lnd/logs/{}/{}net/lnd.log".format(self.network.val, self.chain.val))
|
|
_ln_item, success, timed_out = run_user(cmds)
|
|
if not success:
|
|
self.lnd_channel_msg.val = "?!"
|
|
self.lnd_channel_msg.style = "red"
|
|
|
|
else:
|
|
_last_match = ""
|
|
for line in _ln_item.split("\n"):
|
|
obj = re.match(".*\(height=(\d+).*", line)
|
|
if obj:
|
|
_last_match = obj.groups()[0]
|
|
else:
|
|
obj = re.match(".*Caught up to height (\d+)$", line)
|
|
if obj:
|
|
_last_match = obj.groups()[0]
|
|
|
|
try:
|
|
_last_match = int(_last_match)
|
|
except ValueError:
|
|
_last_match = 0
|
|
|
|
if self.block_height.val:
|
|
if int(_last_match) > 0:
|
|
self.lnd_channel_msg.val = int(_last_match)
|
|
self.lnd_channel_msg.txt = "-> scanning {}/{}".format(_last_match, self.block_height)
|
|
self.lnd_channel_msg.style = "red"
|
|
else:
|
|
self.lnd_channel_msg.val = int(_last_match)
|
|
self.lnd_channel_msg.txt = "-> scanning ??/{}".format(self.block_height)
|
|
self.lnd_channel_msg.style = "red"
|
|
|
|
def update_public_ip(self):
|
|
try:
|
|
f = urlopen('http://v4.ipv6-test.com/api/myip.php')
|
|
self.public_ip.val = f.read(100).decode('utf-8')
|
|
except Exception as err:
|
|
logger.warning("_update_public_ip failed: {}".format(err))
|
|
|
|
def update_bitcoin_public_port(self):
|
|
try:
|
|
_public_bitcoin_port = self.bitcoin_config["DEFAULT"]["port"]
|
|
except KeyError:
|
|
if self.chain.val == "test":
|
|
_public_bitcoin_port = self.currency["testnet_port"]
|
|
|
|
else:
|
|
_public_bitcoin_port = self.currency["mainnet_port"]
|
|
|
|
self.public_bitcoin_port.val = _public_bitcoin_port
|
|
|
|
def check_public_ip_bitcoin_port(self):
|
|
if port_check(self.public_ip.val, self.public_bitcoin_port.val, timeout=2.0):
|
|
self.public_bitcoin_port_status.val = True
|
|
self.public_bitcoin_port_status.txt = ""
|
|
self.public_ip.style = "green"
|
|
self.public_bitcoin_port.style = "green"
|
|
else:
|
|
self.public_bitcoin_port_status.val = False
|
|
self.public_bitcoin_port_status.txt = "not reachable"
|
|
self.public_bitcoin_port_status.style = "red"
|
|
self.public_ip.style = "red"
|
|
self.public_bitcoin_port.style = "red"
|
|
|
|
def check_public_ip_lnd_port(self):
|
|
if not self.lnd_external.val:
|
|
return
|
|
|
|
try:
|
|
_public_lnd_port = int(self.lnd_external.val.split(":")[1])
|
|
|
|
if _public_lnd_port:
|
|
if port_check(self.public_ip.val, _public_lnd_port, timeout=2.0):
|
|
self.public_ip_lnd_port_status.val = True
|
|
self.public_ip_lnd_port_status.txt = ""
|
|
else:
|
|
self.public_ip_lnd_port_status.val = False
|
|
self.public_ip_lnd_port_status.txt = "not reachable"
|
|
self.public_ip_lnd_port_status.style = "red"
|
|
except IndexError as err:
|
|
logger.warning("Error: {}".format(err))
|
|
|
|
def update(self):
|
|
"""update Metrics directly or call helper methods"""
|
|
pass
|
|
|
|
# self.update_load()
|
|
# self.update_uptime()
|
|
# self.update_cpu_temp()
|
|
# self.update_memory()
|
|
# self.update_storage()
|
|
# self.update_ip_network_data()
|
|
|
|
# self.update_network()
|
|
#
|
|
# self.update_bitcoin_dir()
|
|
# self.read_bitcoin_config()
|
|
#
|
|
# self.update_chain()
|
|
#
|
|
# self.update_bitcoin_binaries()
|
|
# self.check_bitcoind_is_running()
|
|
# self.update_bitcoin_daemon_version()
|
|
# self.update_bitcoin_data()
|
|
|
|
# self.update_lnd_dirs()
|
|
# self.read_lnd_config()
|
|
# self.check_lnd_is_running()
|
|
# self.update_lnd_wallet_is_locked()
|
|
# self.update_lnd_alias()
|
|
# self.update_lnd_data()
|
|
#
|
|
# self.update_public_ip()
|
|
# self.update_bitcoin_public_port()
|
|
# self.check_public_ip_lnd_port()
|
|
# self.check_public_ip_bitcoin_port()
|
|
|
|
def display(self):
|
|
logo0 = _yellow(" ")
|
|
logo1 = _yellow(" ,/ ")
|
|
logo2 = _yellow(" ,'/ ")
|
|
logo3 = _yellow(" ,' / ")
|
|
logo4 = _yellow(" ,' /_____, ")
|
|
logo5 = _yellow(" .'____ ,' ")
|
|
logo6 = _yellow(" / ,' ")
|
|
logo7 = _yellow(" / ,' ")
|
|
logo8 = _yellow(" /,' ")
|
|
logo9 = _yellow(" /' ")
|
|
|
|
if self.lnd_is_running:
|
|
if self.lnd_wallet_is_locked:
|
|
lnd_info = Metric("Running", style="yellow")
|
|
else:
|
|
lnd_info = self.lnd_version
|
|
else:
|
|
lnd_info = Metric("Not Running", style="red")
|
|
|
|
line9 = "LND {}".format(lnd_info)
|
|
if self.lnd_base_msg.val and self.lnd_channel_msg.val:
|
|
line9 = "{} {}\n {}".format(line9, self.lnd_base_msg, self.lnd_channel_msg)
|
|
elif self.lnd_base_msg.val:
|
|
line9 = "{} {}".format(line9, self.lnd_base_msg)
|
|
elif self.lnd_channel_msg.val:
|
|
line9 = "{} {}".format(line9, self.lnd_channel_msg)
|
|
|
|
if not (self.lnd_channels_online.val and self.lnd_channels_total.val):
|
|
pass
|
|
else:
|
|
if self.lnd_channels_online.val <= self.lnd_channels_total.val:
|
|
self.lnd_channels_online.style = "yellow"
|
|
self.lnd_channels_total.style = "yellow"
|
|
elif self.lnd_channels_online.val == self.lnd_channels_total.val:
|
|
self.lnd_channels_online.style = "green"
|
|
self.lnd_channels_total.style = "green"
|
|
else:
|
|
self.lnd_channels_online.style = "red"
|
|
self.lnd_channels_total.style = "red"
|
|
|
|
lines = [
|
|
logo0,
|
|
logo0 + "{} {} {}".format(self.name, self.version, self.lnd_alias),
|
|
logo0 + "{} {} {}".format(self.network, "Fullnode + Lightning Network", self.tor_active),
|
|
logo1 + _yellow("-------------------------------------------"),
|
|
logo2 + "{} {}, {}, {} {} {}".format("load average:", self.load_one, self.load_five, self.load_fifteen,
|
|
"CPU:", self.cpu_temp),
|
|
logo3 + "{} {} / {} {} {} ({})".format("Free Mem:", self.memory_avail, self.memory_total,
|
|
"Free HDD:", self.hdd_free_abs, self.hdd_free),
|
|
logo4 + "{}{} ▼{} ▲{}".format("ssh admin@", self.local_ip, self.network_rx, self.network_tx),
|
|
logo5,
|
|
logo6 + "{} {} {} {} {} ({})".format(self.network, self.bitcoin_version, self.chain,
|
|
"Sync", self.sync_status, self.sync_percentage),
|
|
logo7 + "{} {}:{} {}".format("Public", self.public_ip, self.public_bitcoin_port,
|
|
self.public_bitcoin_port_status),
|
|
logo8 + "{} {} {}".format("", "", ""),
|
|
logo9 + line9,
|
|
logo0 + "Wallet {} sat {}/{} Chan {} sat".format(self.lnd_wallet_balance,
|
|
self.lnd_channels_online, self.lnd_channels_total,
|
|
self.lnd_channel_balance),
|
|
logo0,
|
|
"{} {}".format(self.lnd_external, self.public_ip_lnd_port_status)
|
|
]
|
|
|
|
if self.bitcoin_log_msgs:
|
|
lines.append(_yellow("Last lines of: ") + _red("bitcoin/debug.log"))
|
|
for msg in self.bitcoin_log_msgs:
|
|
if len(msg) <= 60:
|
|
lines.append(msg)
|
|
else:
|
|
lines.append(msg[0:57] + "...")
|
|
|
|
if len(self.bitcoin_local_adresses) == 1:
|
|
lines.append("\nAdditional Public Address (e.g. IPv6)")
|
|
lines.append("* {}".format(self.bitcoin_local_adresses[0]))
|
|
elif len(self.bitcoin_local_adresses) >= 1:
|
|
lines.append("\nAdditional Public Addresses (e.g. IPv6) only showing first")
|
|
lines.append("* {}".format(self.bitcoin_local_adresses[0]))
|
|
|
|
for line in lines:
|
|
print(line)
|
|
|
|
# def update_and_display(self):
|
|
# self.update()
|
|
# clear()
|
|
# self.display()
|
|
|
|
|
|
def main():
|
|
setup_logging()
|
|
|
|
usage = "usage: %prog [Options]"
|
|
parser = OptionParser(usage=usage, version="%prog {}".format(BOARD_VERSION))
|
|
|
|
parser.add_option("-H", "--host", dest="host", type="string", default="localhost",
|
|
help="Host to listen on (default localhost)")
|
|
parser.add_option("-P", "--port", dest="port", type="int", default="8000",
|
|
help="Port to listen on (default 8000)")
|
|
|
|
parser.add_option("-c", "--crypto-currency", dest="crypto_currency", type="string", default="bitcoin",
|
|
help="Currency/Network to report on (default bitcoin)")
|
|
parser.add_option("-t", "--timeout", dest="timeout", type="int", default=TIMEOUT,
|
|
help="how long to wait for data to be collected (default {} sec)".format(TIMEOUT))
|
|
parser.add_option("-r", "--refresh", dest="refresh", type="int", default=5,
|
|
help="interval to refresh data when looping (default 5 sec)")
|
|
parser.add_option("--interface", dest="interface", type="string", default=IF_NAME,
|
|
help="network interface to report on (default {})".format(IF_NAME))
|
|
|
|
options, args = parser.parse_args()
|
|
|
|
crypto_currency = options.crypto_currency.lower()
|
|
if crypto_currency not in list(CRYPTO_CURRENCIES.keys()):
|
|
raise ValueError("Unexpected Crypto Currency given: {}".format(options.crypto_currency))
|
|
|
|
logger.info("Starting infoBlitz...")
|
|
|
|
board = Dashboard(crypto_currency)
|
|
board.timeout = 120
|
|
board.interface = options.interface
|
|
board.name = Metric(BOARD_NAME, style="yellow")
|
|
board.version = Metric(BOARD_VERSION, style="yellow")
|
|
|
|
# use a threading.Lock() to ensure access to the same data from different threads
|
|
board_lock = threading.Lock()
|
|
|
|
dashboard_updater_thread = DashboardUpdater(board=board, board_lock=board_lock, interval=options.refresh)
|
|
dashboard_printer_thread = DashboardPrinter(board=board, board_lock=board_lock, interval=options.refresh + 10)
|
|
web_server_thread = ThreadedHTTPServer(options.host, options.port, board, board_lock, name="Web_Server")
|
|
|
|
logger.info("Starting Dashboard Updater")
|
|
dashboard_updater_thread.start()
|
|
logger.info("Starting Dashboard Printer")
|
|
dashboard_printer_thread.start()
|
|
logger.info("Starting Web Server: http://{}:{}".format(options.host, options.port))
|
|
web_server_thread.start()
|
|
|
|
# for info/debug only
|
|
logger.debug("Threads: [{}]".format("; ".join([t.getName() for t in threading.enumerate()])))
|
|
|
|
try:
|
|
while True: # run in loop that can be interrupted with CTRL+c
|
|
time.sleep(0.2) # ToDO check.. not quite sure..
|
|
except KeyboardInterrupt:
|
|
logger.debug("Stopping server loop")
|
|
web_server_thread.stop()
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|