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 <ip of aprsd> --port <port> --magic-word
<magic word>
This commit is contained in:
Hemna 2023-07-09 21:06:57 -04:00
parent fe0d71de4d
commit b2e621da4b
5 changed files with 155 additions and 11 deletions

131
aprsd/cmds/fetch_stats.py Normal file
View File

@ -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)

View File

@ -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")

View File

@ -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

View File

@ -1,5 +1,6 @@
aprslib>=0.7.0
click
click-params
click-completion
flask==2.1.2
werkzeug==2.1.2

View File

@ -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