#!/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("RaspiBlitz Info Dashboard", "UTF-8") content += bytes("

RaspiBlitz Info Dashboard

", "UTF-8") 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("-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()