diff --git a/.gitignore b/.gitignore index e4ecc87..b539ed6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .DS_Store home.admin/.DS_Store +*.log +__pycache__ diff --git a/home.admin/00infoBlitz.sh b/home.admin/00infoBlitz.sh index cd64285..bcb5b2e 100755 --- a/home.admin/00infoBlitz.sh +++ b/home.admin/00infoBlitz.sh @@ -1,224 +1,2 @@ #!/bin/sh - -# set colors -color_red='\033[0;31m' -color_green='\033[0;32m' -color_yellow='\033[0;33m' -color_gray='\033[0;37m' - -# load network -network=`sudo cat /home/admin/.network` - -# get chain -chain="test" -isMainChain=$(sudo cat /mnt/hdd/${network}/${network}.conf 2>/dev/null | grep "#testnet=1" -c) -if [ ${isMainChain} -gt 0 ];then - chain="main" -fi - -# set datadir -bitcoin_dir="/home/bitcoin/.${network}" -lnd_dir="/home/bitcoin/.lnd" -lnd_macaroon_dir="/home/bitcoin/.lnd/data/chain/${network}/${chain}net" - -# get uptime & load -load=$(w | head -n 1 | cut -d 'v' -f2 | cut -d ':' -f2) - -# get CPU temp -cpu=$(cat /sys/class/thermal/thermal_zone0/temp) -temp=$((cpu/1000)) - -# get memory -ram_avail=$(free -m | grep Mem | awk '{ print $7 }') -ram=$(printf "%sM / %sM" "${ram_avail}" "$(free -m | grep Mem | awk '{ print $2 }')") - -if [ ${ram_avail} -lt 100 ]; then - color_ram="${color_red}\e[7m" -else - color_ram=${color_green} -fi - -# get storage -sd_free_ratio=$(printf "%d" "$(df -h | grep "/$" | awk '{ print $4/$2*100 }')") 2>/dev/null -sd=$(printf "%s (%s%%)" "$(df -h | grep '/$' | awk '{ print $4 }')" "${sd_free_ratio}") -if [ ${sd_free_ratio} -lt 10 ]; then - color_sd="${color_red}" -else - color_sd=${color_green} -fi - -hdd_free_ratio=$(printf "%d" "$(df -h | grep '/mnt/hdd$' | awk '{ print $4/$2*100 }')") 2>/dev/null -hdd=$(printf "%s (%s%%)" "$(df -h | grep '/mnt/hdd$' | awk '{ print $4 }')" "${hdd_free_ratio}") - -if [ ${hdd_free_ratio} -lt 10 ]; then - color_hdd="${color_red}\e[7m" -else - color_hdd=${color_green} -fi - -# get network traffic -network_rx=$(ifconfig eth0 | grep 'RX packets' | awk '{ print $6$7 }' | sed 's/[()]//g') -network_tx=$(ifconfig eth0 | grep 'TX packets' | awk '{ print $6$7 }' | sed 's/[()]//g') - -# Bitcoin blockchain -btc_path=$(command -v ${network}-cli) -if [ -n ${btc_path} ]; then - btc_title=$network - blockchaininfo="$(${network}-cli -datadir=${bitcoin_dir} getblockchaininfo)" - if [ ${#blockchaininfo} -gt 0 ]; then - btc_title="${btc_title} (${chain}net)" - - # get sync status - block_chain="$(${network}-cli -datadir=${bitcoin_dir} getblockcount)" - block_verified="$(echo "${blockchaininfo}" | jq -r '.blocks')" - block_diff=$(expr ${block_chain} - ${block_verified}) - - progress="$(echo "${blockchaininfo}" | jq -r '.verificationprogress')" - sync_percentage=$(printf "%.2f%%" "$(echo $progress | awk '{print 100 * $1}')") - - if [ ${block_diff} -eq 0 ]; then # fully synced - sync="OK" - sync_color="${color_green}" - sync_behind=" " - elif [ ${block_diff} -eq 1 ]; then # fully synced - sync="OK" - sync_color="${color_green}" - sync_behind="-1 block" - elif [ ${block_diff} -le 10 ]; then # <= 2 blocks behind - sync="catchup" - sync_color="${color_red}" - sync_behind="-${block_diff} blocks" - else - sync="progress" - sync_color="${color_red}" - sync_behind="${sync_percentage}" - fi - - # get last known block - last_block="$(${network}-cli -datadir=${bitcoin_dir} getblockcount)" - if [ ! -z "${last_block}" ]; then - btc_line2="${btc_line2} ${color_gray}(block ${last_block})" - fi - - # get mem pool transactions - mempool="$(${network}-cli -datadir=${bitcoin_dir} getmempoolinfo | jq -r '.size')" - - else - btc_line2="${color_red}NOT RUNNING\t\t" - fi -fi - -# get IP address & port -local_ip=$(ip addr | grep 'state UP' -A2 | tail -n1 | awk '{print $2}' | cut -f1 -d'/') -public_ip=$(curl -s http://v4.ipv6-test.com/api/myip.php) -public_port=$(cat ${bitcoin_dir}/${network}.conf 2>/dev/null | grep port= | awk -F"=" '{print $2}') -if [ "${public_port}" = "" ]; then - if [ "${network}" = "litecoin" ]; then - if [ "${chain}" = "test" ]; then - public_port=19333 - else - public_port=9333 - fi - else - if [ "${chain}" = "test" ]; then - public_port=18333 - else - public_port=8333 - fi - fi -fi - -# CHAIN NETWORK -public_addr="??" -torInfo="" -# Version -networkVersion=$(${network}-cli -datadir=${bitcoin_dir} -version | cut -d ' ' -f6) -# TOR or IP -onionAddress=$(${network}-cli -datadir=${bitcoin_dir} getnetworkinfo | grep '"address"' | cut -d '"' -f4) -if [ ${#onionAddress} -gt 0 ]; then - # TOR address - public_addr="${onionAddress}:${public_port}" - public="" - public_color="${color_green}" - torInfo="+ TOR" -else - # IP address - public_addr="${public_ip}:${public_port}" - public_check=$(timeout 2s nc -z ${public_ip} ${public_port}; echo $?) - if [ $public_check = "0" ]; then - public="Yes" - public_color="${color_green}" - else - public="Not reachable" - public_color="${color_red}" - fi -fi - -#IP - - -# LIGHTNING NETWORK - -ln_baseInfo="-" -ln_channelInfo="\n" -ln_external="" - -wallet_unlocked=$(sudo tail -n 1 /mnt/hdd/lnd/logs/${network}/${chain}net/lnd.log | grep -c unlock) -if [ "$wallet_unlocked" -gt 0 ] ; then - alias_color="${color_red}" - ln_alias="Wallet Locked" -else - ln_getInfo=$(sudo -u bitcoin /usr/local/bin/lncli --macaroonpath=${lnd_macaroon_dir}/readonly.macaroon --tlscertpath=${lnd_dir}/tls.cert getinfo 2>/dev/null) - ln_external=$(echo "${ln_getInfo}" | grep "uris" -A 1 | tr -d '\n' | cut -d '"' -f4) - alias_color="${color_grey}" - ln_alias=$(echo "${ln_getInfo}" | grep "alias" | cut -d '"' -f4) - ln_sync=$(echo "${ln_getInfo}" | grep "synced_to_chain" | grep "true" -c) - if [ ${ln_sync} -eq 0 ]; then - if [ ${#ln_getInfo} -eq 0 ]; then - ln_baseInfo="${color_red} Not Started | Not Ready Yet" - else - item=$(sudo -u bitcoin tail -n 100 /mnt/hdd/lnd/logs/${network}/${chain}net/lnd.log | grep "(height" | tail -n1 | awk '{print $10} {print $11} {print $12}' | tr -dc '0-9') - total=$(sudo -u bitcoin ${network}-cli -datadir=/home/bitcoin/.${network} getblockchaininfo | jq -r '.blocks') - ln_baseInfo="${color_red} waiting for chain sync" - if [ ${#item} -gt 0 ]; then - ln_channelInfo="scanning ${item}/${total}" - fi - fi - else - ln_walletbalance="$(sudo -u bitcoin /usr/local/bin/lncli --macaroonpath=${lnd_macaroon_dir}/readonly.macaroon --tlscertpath=${lnd_dir}/tls.cert walletbalance | jq -r '.confirmed_balance')" 2>/dev/null - ln_channelbalance="$(sudo -u bitcoin /usr/local/bin/lncli --macaroonpath=${lnd_macaroon_dir}/readonly.macaroon --tlscertpath=${lnd_dir}/tls.cert channelbalance | jq -r '.balance')" 2>/dev/null - ln_channels_online="$(echo "${ln_getInfo}" | jq -r '.num_active_channels')" 2>/dev/null - ln_channels_total="$(sudo -u bitcoin /usr/local/bin/lncli --macaroonpath=${lnd_macaroon_dir}/readonly.macaroon --tlscertpath=${lnd_dir}/tls.cert listchannels | jq '.[] | length')" 2>/dev/null - ln_baseInfo="${color_gray}wallet ${ln_walletbalance} sat" - ln_channelInfo="${ln_channels_online}/${ln_channels_total} Channels ${ln_channelbalance} sat" - fi -fi - -sleep 5 -printf " -${color_yellow} -${color_yellow} -${color_yellow} -${color_yellow} ${color_yellow}%s ${color_green} ${ln_alias} -${color_yellow} ${color_gray}${network} Fullnode + Lightning Network ${torInfo} -${color_yellow} ,/ ${color_yellow}%s -${color_yellow} ,'/ ${color_gray}%s, CPU %s°C -${color_yellow} ,' / ${color_gray}Free Mem ${color_ram}${ram} ${color_gray} Free HDD ${color_hdd}%s -${color_yellow} ,' /_____, ${color_gray}ssh admin@${color_green}${local_ip}${color_gray} ▼${network_rx} ▲${network_tx} -${color_yellow} .'____ ,' ${color_gray} -${color_yellow} / ,' ${color_gray}${network} ${color_green}${networkVersion} ${chain}net ${color_gray}Sync ${sync_color}${sync} (%s) -${color_yellow} / ,' ${color_gray}Public ${public_color}${public_addr} ${public} -${color_yellow} /,' ${color_gray} -${color_yellow} /' ${color_gray}LND ${color_green}v0.5-beta ${ln_baseInfo} -${color_yellow} ${color_gray}${ln_channelInfo} -${color_yellow} -${color_yellow}${ln_external} -" \ -"RaspiBlitz v0.93" \ -"-------------------------------------------" \ -"load average:${load##up*, }" "${temp}" \ -"${hdd}" "${sync_percentage}" -if [ ${#onionAddress} -eq 0 ]; then - # one extra space line at the end if nodeaddress is not TOR - echo "" -fi \ No newline at end of file +/usr/bin/env python3 infoblitzd.py $@ diff --git a/home.admin/00infoLCD.sh b/home.admin/00infoLCD.sh index 3c8df9c..cbd236d 100755 --- a/home.admin/00infoLCD.sh +++ b/home.admin/00infoLCD.sh @@ -121,7 +121,8 @@ if [ "$USER" = "pi" ]; then else # no special case - show status display - /home/admin/00infoBlitz.sh + # /home/admin/00infoBlitz.sh + /home/admin/00infoBlitz.py sleep 5 fi diff --git a/home.admin/infoblitzd.py b/home.admin/infoblitzd.py new file mode 100644 index 0000000..19e05f3 --- /dev/null +++ b/home.admin/infoblitzd.py @@ -0,0 +1,1586 @@ +#!/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("
The Dashboard Version is: v{}
".format(self.board.version.val), "UTF-8") + content += bytes("The API Endpoint (JSON) is located here: /json/
", "UTF-8") + content += bytes("", "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("-l", "--loop", dest="loop", action="store_true", + help="run in a loop (CTRL+c to exit)") + + 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()