From b2e621da4b058a1fc727ff6741a27c03a0b220e4 Mon Sep 17 00:00:00 2001 From: Hemna Date: Sun, 9 Jul 2023 21:06:57 -0400 Subject: [PATCH] Added the fetch-stats command You can now fetch and view the stats of a live running aprsd server if it has enabled the rpc server in the config file's rpc_settings block. You just have to match the magic word as specified in the config file to authorize against the rpc server. aprsd fetch-stats --ip-address --port --magic-word --- aprsd/cmds/fetch_stats.py | 131 ++++++++++++++++++++++++++++++++++++++ aprsd/main.py | 4 +- aprsd/rpc/client.py | 23 +++++-- requirements.in | 1 + requirements.txt | 7 +- 5 files changed, 155 insertions(+), 11 deletions(-) create mode 100644 aprsd/cmds/fetch_stats.py diff --git a/aprsd/cmds/fetch_stats.py b/aprsd/cmds/fetch_stats.py new file mode 100644 index 0000000..8c5983b --- /dev/null +++ b/aprsd/cmds/fetch_stats.py @@ -0,0 +1,131 @@ +# Fetch active stats from a remote running instance of aprsd server +# This uses the RPC server to fetch the stats from the remote server. + +import logging + +import click +from click_params import IP_ADDRESS +from oslo_config import cfg +from rich.console import Console +from rich.table import Table + +# local imports here +import aprsd +from aprsd import cli_helper +from aprsd.main import cli +from aprsd.rpc import client as rpc_client + + +# setup the global logger +# logging.basicConfig(level=logging.DEBUG) # level=10 +LOG = logging.getLogger("APRSD") +CONF = cfg.CONF + + +@cli.command() +@cli_helper.add_options(cli_helper.common_options) +@click.option( + "--ip-address", type=IP_ADDRESS, + default=CONF.rpc_settings.ip, + help="IP address of the remote aprsd server to fetch stats from.", +) +@click.option( + "--port", type=int, + default=CONF.rpc_settings.port, + help="Port of the remote aprsd server rpc port to fetch stats from.", +) +@click.option( + "--magic-word", type=str, + default=CONF.rpc_settings.magic_word, + help="Magic word of the remote aprsd server rpc port to fetch stats from.", +) +@click.pass_context +@cli_helper.process_standard_options +def fetch_stats(ctx, ip_address, port, magic_word): + """Fetch stats from a remote running instance of aprsd server.""" + LOG.info(f"APRSD Fetch-Stats started version: {aprsd.__version__}") + + CONF.log_opt_values(LOG, logging.DEBUG) + + msg = f"Fetching stats from {ip_address}:{port} with magic word '{magic_word}'" + + console = Console() + with console.status(msg) as status: + client = rpc_client.RPCClient(ip_address, port, magic_word) + stats = client.get_stats_dict() + console.print_json(data=stats) + aprsd_title = ( + "APRSD " + f"[bold cyan]v{stats['aprsd']['version']}[/] " + f"Callsign [bold green]{stats['aprsd']['callsign']}[/] " + f"Uptime [bold yellow]{stats['aprsd']['uptime']}[/]" + ) + + console.rule(f"Stats from {ip_address}:{port} with magic word '{magic_word}'") + console.print("\n\n") + console.rule(aprsd_title) + + # Show the connection to APRS + # It can be a connection to an APRS-IS server or a local TNC via KISS or KISSTCP + if "aprs-is" in stats: + title = f"APRS-IS Connection {stats['aprs-is']['server']}" + table = Table(title=title) + table.add_column("Key") + table.add_column("Value") + for key, value in stats["aprs-is"].items(): + table.add_row(key, value) + console.print(table) + + + msgs_table = Table(title="Messages") + msgs_table.add_column("Key") + msgs_table.add_column("Value") + for key, value in stats["messages"].items(): + msgs_table.add_row(key, str(value)) + + console.print(msgs_table) + + packets_table = Table(title="Packets") + packets_table.add_column("Key") + packets_table.add_column("Value") + for key, value in stats["packets"].items(): + packets_table.add_row(key, str(value)) + + console.print(packets_table) + + if "plugins" in stats: + plugins_table = Table(title="Plugins") + plugins_table.add_column("Plugin") + plugins_table.add_column("Enabled") + plugins_table.add_column("Version") + plugins_table.add_column("TX") + plugins_table.add_column("RX") + for key, value in stats["plugins"].items(): + plugins_table.add_row( + key, + str(stats["plugins"][key]["enabled"]), + stats["plugins"][key]["version"], + str(stats["plugins"][key]["tx"]), + str(stats["plugins"][key]["rx"]), + ) + + console.print(plugins_table) + + if "seen_list" in stats["aprsd"]: + seen_table = Table(title="Seen List") + seen_table.add_column("Callsign") + seen_table.add_column("Message Count") + seen_table.add_column("Last Heard") + for key, value in stats["aprsd"]["seen_list"].items(): + seen_table.add_row(key, str(value["count"]), value["last"]) + + console.print(seen_table) + + if "watch_list" in stats["aprsd"]: + watch_table = Table(title="Watch List") + watch_table.add_column("Callsign") + watch_table.add_column("Last Heard") + for key, value in stats["aprsd"]["watch_list"].items(): + watch_table.add_row(key, value["last"]) + + console.print(watch_table) diff --git a/aprsd/main.py b/aprsd/main.py index af76927..34bc5e8 100644 --- a/aprsd/main.py +++ b/aprsd/main.py @@ -70,8 +70,8 @@ def main(): # First import all the possible commands for the CLI # The commands themselves live in the cmds directory from .cmds import ( # noqa - completion, dev, healthcheck, list_plugins, listen, send_message, - server, webchat, + completion, dev, fetch_stats, healthcheck, list_plugins, listen, + send_message, server, webchat, ) cli(auto_envvar_prefix="APRSD") diff --git a/aprsd/rpc/client.py b/aprsd/rpc/client.py index 8b192c7..c42f494 100644 --- a/aprsd/rpc/client.py +++ b/aprsd/rpc/client.py @@ -16,23 +16,32 @@ class RPCClient: _instance = None _rpc_client = None + ip = None + port = None + magic_word = None + def __new__(cls, *args, **kwargs): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance - def __init__(self): + def __init__(self, ip=None, port=None, magic_word=None): + self.ip = str(ip) or CONF.rpc_settings.ip + self.port = int(port) or CONF.rpc_settings.port + self.magic_word = magic_word or CONF.rpc_settings.magic_word self._check_settings() self.get_rpc_client() def _check_settings(self): if not CONF.rpc_settings.enabled: - LOG.error("RPC is not enabled, no way to get stats!!") + LOG.warning("RPC is not enabled, no way to get stats!!") - if CONF.rpc_settings.magic_word == conf.common.APRSD_DEFAULT_MAGIC_WORD: + if self.magic_word == conf.common.APRSD_DEFAULT_MAGIC_WORD: LOG.warning("You are using the default RPC magic word!!!") LOG.warning("edit aprsd.conf and change rpc_settings.magic_word") + LOG.debug(f"RPC Client: {self.ip}:{self.port} {self.magic_word}") + def _rpyc_connect( self, host, port, service=rpyc.VoidService, @@ -53,11 +62,11 @@ class RPCClient: def get_rpc_client(self): if not self._rpc_client: - magic = CONF.rpc_settings.magic_word + CONF.rpc_settings.magic_word self._rpc_client = self._rpyc_connect( - CONF.rpc_settings.ip, - CONF.rpc_settings.port, - authorizer=lambda sock: sock.send(magic.encode()), + self.ip, + self.port, + authorizer=lambda sock: sock.send(self.magic_word.encode()), ) return self._rpc_client diff --git a/requirements.in b/requirements.in index bec86f7..e409f71 100644 --- a/requirements.in +++ b/requirements.in @@ -1,5 +1,6 @@ aprslib>=0.7.0 click +click-params click-completion flask==2.1.2 werkzeug==2.1.2 diff --git a/requirements.txt b/requirements.txt index e6b174d..498cafe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,13 +13,15 @@ bitarray==2.7.6 # via ax253, kiss3 certifi==2023.5.7 # via requests cffi==1.15.1 # via cryptography charset-normalizer==3.2.0 # via requests -click==8.1.4 # via -r requirements.in, click-completion, flask +click==8.1.4 # via -r requirements.in, click-completion, click-params, flask click-completion==0.5.2 # via -r requirements.in +click-params==0.4.1 # via -r requirements.in commonmark==0.9.1 # via rich cryptography==38.0.1 # via -r requirements.in, pyopenssl dacite2==2.0.0 # via -r requirements.in dataclasses==0.6 # via -r requirements.in debtcollector==2.5.0 # via oslo-config +decorator==5.1.1 # via validators dnspython==2.3.0 # via eventlet eventlet==0.33.3 # via -r requirements.in flask==2.1.2 # via -r requirements.in, flask-classful, flask-httpauth, flask-socketio @@ -66,6 +68,7 @@ ua-parser==0.18.0 # via user-agents update-checker==0.18.0 # via -r requirements.in urllib3==2.0.3 # via requests user-agents==2.2.0 # via -r requirements.in +validators==0.20.0 # via click-params werkzeug==2.1.2 # via -r requirements.in, flask wrapt==1.15.0 # via -r requirements.in, debtcollector -zipp==3.15.0 # via importlib-metadata +zipp==3.16.0 # via importlib-metadata