From e13ca0061a2a04514de3bf70646fa6b6257cf36b Mon Sep 17 00:00:00 2001 From: Hemna Date: Sat, 24 Dec 2022 13:53:06 -0500 Subject: [PATCH 1/7] Convert config to oslo_config This patch is the initial conversion of the custom config and config file yaml format to oslo_config's configuration mechanism. The resulting config format is now an ini type file. The default location is ~/.config/aprsd/aprsd.conf This is a backwards incompatible change. You will have to rebuild the config file and edit it. Also any aprsd plugins can now define config options in code and add an setup.cfg entry_point definition oslo_config.opts = foo.conf = foo.conf:list_opts --- ChangeLog | 28 ++++++++ aprsd/aprsd.py | 35 ++++++++-- aprsd/cli_helper.py | 25 ++++++-- aprsd/client.py | 114 +++++++++++++++++---------------- aprsd/cmds/listen.py | 28 +++----- aprsd/cmds/server.py | 48 ++++++-------- aprsd/logging/log.py | 25 +++++--- aprsd/packets/packet_list.py | 12 +--- aprsd/packets/seen_list.py | 11 +--- aprsd/packets/tracker.py | 10 ++- aprsd/packets/watch_list.py | 27 +++----- aprsd/plugin.py | 54 ++++++++++------ aprsd/plugin_utils.py | 11 +++- aprsd/plugins/email.py | 120 ++++++++++++++++++++--------------- aprsd/plugins/location.py | 5 +- aprsd/plugins/notify.py | 7 +- aprsd/plugins/query.py | 12 +++- aprsd/plugins/time.py | 4 +- aprsd/plugins/weather.py | 108 +++++++++++++------------------ aprsd/stats.py | 13 ++-- aprsd/threads/keep_alive.py | 18 ++---- aprsd/threads/rx.py | 28 ++++---- aprsd/utils/objectstore.py | 117 ++++++++++++++++------------------ dev-requirements.txt | 24 ++++--- requirements.in | 1 + requirements.txt | 24 ++++--- setup.cfg | 4 ++ tests/test_email.py | 9 ++- tests/test_main.py | 4 +- 29 files changed, 496 insertions(+), 430 deletions(-) diff --git a/ChangeLog b/ChangeLog index b81c936..b6c6378 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,9 +1,37 @@ CHANGES ======= +* Added rain formatting unit tests to WeatherPacket +* Fix Rain reporting in WeatherPacket send +* Removed Packet.send() +* Removed watchlist plugins +* Fix PluginManager.get\_plugins +* Cleaned up PluginManager +* Cleaned up PluginManager +* Update routing for weatherpacket +* Fix some WeatherPacket formatting +* Fix pep8 violation +* Add packet filtering for aprsd listen +* Added WeatherPacket encoding +* Updated webchat and listen for queue based RX +* reworked collecting and reporting stats +* Removed unused threading code +* Change RX packet processing to enqueu +* Make tracking objectstores work w/o initializing +* Cleaned up packet transmit class attributes +* Fix packets timestamp to int +* More messaging -> packets cleanup +* Cleaned out all references to messaging +* Added contructing a GPSPacket for sending +* cleanup webchat +* Reworked all packet processing +* Updated plugins and plugin interfaces for Packet +* Started using dataclasses to describe packets + v2.6.1 ------ +* v2.6.1 * Fixed position report for webchat beacon * Try and fix broken 32bit qemu builds on 64bit system * Add unit tests for webchat diff --git a/aprsd/aprsd.py b/aprsd/aprsd.py index 7c91d2e..7021e1b 100644 --- a/aprsd/aprsd.py +++ b/aprsd/aprsd.py @@ -21,6 +21,8 @@ # python included libs import datetime +from importlib.metadata import entry_points +from importlib.metadata import version as metadata_version import logging import os import signal @@ -29,12 +31,11 @@ import time import click import click_completion +from oslo_config import cfg, generator # local imports here import aprsd -from aprsd import cli_helper -from aprsd import config as aprsd_config -from aprsd import packets, stats, threads, utils +from aprsd import cli_helper, packets, stats, threads, utils # setup the global logger @@ -111,8 +112,32 @@ def check_version(ctx): @cli.command() @click.pass_context def sample_config(ctx): - """This dumps the config to stdout.""" - click.echo(aprsd_config.dump_default_cfg()) + """Generate a sample Config file from aprsd and all installed plugins.""" + + def get_namespaces(): + args = [] + + selected = entry_points(group="oslo.config.opts") + for entry in selected: + if "aprsd" in entry.name: + args.append("--namespace") + args.append(entry.name) + + return args + + args = get_namespaces() + config_version = metadata_version("oslo.config") + logging.basicConfig(level=logging.WARN) + conf = cfg.ConfigOpts() + generator.register_cli_opts(conf) + try: + conf(args, version=config_version) + except cfg.RequiredOptError: + conf.print_help() + if not sys.argv[1:]: + raise SystemExit + raise + generator.generate(conf) @cli.command() diff --git a/aprsd/cli_helper.py b/aprsd/cli_helper.py index c70f22c..f86963a 100644 --- a/aprsd/cli_helper.py +++ b/aprsd/cli_helper.py @@ -1,13 +1,22 @@ from functools import update_wrapper +from pathlib import Path import typing as t import click +from oslo_config import cfg -from aprsd import config as aprsd_config +import aprsd from aprsd.logging import log from aprsd.utils import trace +CONF = cfg.CONF +home = str(Path.home()) +DEFAULT_CONFIG_DIR = f"{home}/.config/aprsd/" +DEFAULT_SAVE_FILE = f"{home}/.config/aprsd/aprsd.p" +DEFAULT_CONFIG_FILE = f"{home}/.config/aprsd/aprsd.conf" + + F = t.TypeVar("F", bound=t.Callable[..., t.Any]) common_options = [ @@ -27,7 +36,7 @@ common_options = [ "--config", "config_file", show_default=True, - default=aprsd_config.DEFAULT_CONFIG_FILE, + default=DEFAULT_CONFIG_FILE, help="The aprsd config file to use for options.", ), click.option( @@ -51,16 +60,22 @@ def process_standard_options(f: F) -> F: def new_func(*args, **kwargs): ctx = args[0] ctx.ensure_object(dict) + if kwargs["config_file"]: + default_config_files = [kwargs["config_file"]] + else: + default_config_files = None + CONF( + [], project="aprsd", version=aprsd.__version__, + default_config_files=default_config_files, + ) ctx.obj["loglevel"] = kwargs["loglevel"] ctx.obj["config_file"] = kwargs["config_file"] ctx.obj["quiet"] = kwargs["quiet"] - ctx.obj["config"] = aprsd_config.parse_config(kwargs["config_file"]) log.setup_logging( - ctx.obj["config"], ctx.obj["loglevel"], ctx.obj["quiet"], ) - if ctx.obj["config"]["aprsd"].get("trace", False): + if CONF.trace_enabled: trace.setup_tracing(["method", "api"]) del kwargs["loglevel"] diff --git a/aprsd/client.py b/aprsd/client.py index 3d06e08..bd5ce54 100644 --- a/aprsd/client.py +++ b/aprsd/client.py @@ -4,14 +4,15 @@ import time import aprslib from aprslib.exceptions import LoginError +from oslo_config import cfg -from aprsd import config as aprsd_config from aprsd import exception from aprsd.clients import aprsis, kiss from aprsd.packets import core, packet_list from aprsd.utils import trace +CONF = cfg.CONF LOG = logging.getLogger("APRSD") TRANSPORT_APRSIS = "aprsis" TRANSPORT_TCPKISS = "tcpkiss" @@ -28,7 +29,6 @@ class Client: _instance = None _client = None - config = None connected = False server_string = None @@ -41,11 +41,6 @@ class Client: # Put any initialization here. return cls._instance - def __init__(self, config=None): - """Initialize the object instance.""" - if config: - self.config = config - def set_filter(self, filter): self.filter = filter if self._client: @@ -74,12 +69,12 @@ class Client: @staticmethod @abc.abstractmethod - def is_enabled(config): + def is_enabled(): pass @staticmethod @abc.abstractmethod - def transport(config): + def transport(): pass @abc.abstractmethod @@ -90,26 +85,38 @@ class Client: class APRSISClient(Client): @staticmethod - def is_enabled(config): + def is_enabled(): # Defaults to True if the enabled flag is non existent try: - return config["aprs"].get("enabled", True) + return CONF.aprs_network.enabled except KeyError: return False @staticmethod - def is_configured(config): - if APRSISClient.is_enabled(config): + def is_configured(): + if APRSISClient.is_enabled(): # Ensure that the config vars are correctly set - config.check_option("aprs.login") - config.check_option("aprs.password") - config.check_option("aprs.host") - return True + if not CONF.aprs_network.login: + LOG.error("Config aprs_network.login not set.") + raise exception.MissingConfigOptionException( + "aprs_network.login is not set.", + ) + if not CONF.aprs_network.password: + LOG.error("Config aprs_network.password not set.") + raise exception.MissingConfigOptionException( + "aprs_network.password is not set.", + ) + if not CONF.aprs_network.host: + LOG.error("Config aprs_network.host not set.") + raise exception.MissingConfigOptionException( + "aprs_network.host is not set.", + ) + return True return True @staticmethod - def transport(config): + def transport(): return TRANSPORT_APRSIS def decode_packet(self, *args, **kwargs): @@ -118,10 +125,10 @@ class APRSISClient(Client): @trace.trace def setup_connection(self): - user = self.config["aprs"]["login"] - password = self.config["aprs"]["password"] - host = self.config["aprs"].get("host", "rotate.aprs.net") - port = self.config["aprs"].get("port", 14580) + user = CONF.aprs_network.login + password = CONF.aprs_network.password + host = CONF.aprs_network.host + port = CONF.aprs_network.port connected = False backoff = 1 aprs_client = None @@ -151,45 +158,43 @@ class APRSISClient(Client): class KISSClient(Client): @staticmethod - def is_enabled(config): + def is_enabled(): """Return if tcp or serial KISS is enabled.""" - if "kiss" not in config: - return False - - if config.get("kiss.serial.enabled", default=False): + if CONF.kiss_serial.enabled: return True - if config.get("kiss.tcp.enabled", default=False): + if CONF.kiss_tcp.enabled: return True return False @staticmethod - def is_configured(config): + def is_configured(): # Ensure that the config vars are correctly set - if KISSClient.is_enabled(config): - config.check_option( - "aprsd.callsign", - default_fail=aprsd_config.DEFAULT_CONFIG_DICT["aprsd"]["callsign"], - ) - transport = KISSClient.transport(config) + if KISSClient.is_enabled(): + transport = KISSClient.transport() if transport == TRANSPORT_SERIALKISS: - config.check_option("kiss.serial") - config.check_option("kiss.serial.device") + if not CONF.kiss_serial.device: + LOG.error("KISS serial enabled, but no device is set.") + raise exception.MissingConfigOptionException( + "kiss_serial.device is not set.", + ) elif transport == TRANSPORT_TCPKISS: - config.check_option("kiss.tcp") - config.check_option("kiss.tcp.host") - config.check_option("kiss.tcp.port") + if not CONF.kiss_tcp.host: + LOG.error("KISS TCP enabled, but no host is set.") + raise exception.MissingConfigOptionException( + "kiss_tcp.host is not set.", + ) return True - return True + return False @staticmethod - def transport(config): - if config.get("kiss.serial.enabled", default=False): + def transport(): + if CONF.kiss_serial.enabled: return TRANSPORT_SERIALKISS - if config.get("kiss.tcp.enabled", default=False): + if CONF.kiss_tcp.enabled: return TRANSPORT_TCPKISS def decode_packet(self, *args, **kwargs): @@ -208,7 +213,7 @@ class KISSClient(Client): @trace.trace def setup_connection(self): - client = kiss.KISS3Client(self.config) + client = kiss.KISS3Client() return client @@ -222,8 +227,7 @@ class ClientFactory: # Put any initialization here. return cls._instance - def __init__(self, config): - self.config = config + def __init__(self): self._builders = {} def register(self, key, builder): @@ -231,23 +235,23 @@ class ClientFactory: def create(self, key=None): if not key: - if APRSISClient.is_enabled(self.config): + if APRSISClient.is_enabled(): key = TRANSPORT_APRSIS - elif KISSClient.is_enabled(self.config): - key = KISSClient.transport(self.config) + elif KISSClient.is_enabled(): + key = KISSClient.transport() LOG.debug(f"GET client '{key}'") builder = self._builders.get(key) if not builder: raise ValueError(key) - return builder(self.config) + return builder() def is_client_enabled(self): """Make sure at least one client is enabled.""" enabled = False for key in self._builders.keys(): try: - enabled |= self._builders[key].is_enabled(self.config) + enabled |= self._builders[key].is_enabled() except KeyError: pass @@ -257,7 +261,7 @@ class ClientFactory: enabled = False for key in self._builders.keys(): try: - enabled |= self._builders[key].is_configured(self.config) + enabled |= self._builders[key].is_configured() except KeyError: pass except exception.MissingConfigOptionException as ex: @@ -270,11 +274,11 @@ class ClientFactory: return enabled @staticmethod - def setup(config): + def setup(): """Create and register all possible client objects.""" global factory - factory = ClientFactory(config) + factory = ClientFactory() factory.register(TRANSPORT_APRSIS, APRSISClient) factory.register(TRANSPORT_TCPKISS, KISSClient) factory.register(TRANSPORT_SERIALKISS, KISSClient) diff --git a/aprsd/cmds/listen.py b/aprsd/cmds/listen.py index 8da7da3..7d3c14f 100644 --- a/aprsd/cmds/listen.py +++ b/aprsd/cmds/listen.py @@ -10,11 +10,12 @@ import sys import time import click +from oslo_config import cfg from rich.console import Console # local imports here import aprsd -from aprsd import cli_helper, client, packets, stats, threads, utils +from aprsd import cli_helper, client, packets, stats, threads from aprsd.aprsd import cli from aprsd.threads import rx @@ -22,6 +23,7 @@ from aprsd.threads import rx # setup the global logger # logging.basicConfig(level=logging.DEBUG) # level=10 LOG = logging.getLogger("APRSD") +CONF = cfg.CONF console = Console() @@ -38,8 +40,8 @@ def signal_handler(sig, frame): class APRSDListenThread(rx.APRSDRXThread): - def __init__(self, config, packet_queue, packet_filter=None): - super().__init__(config, packet_queue) + def __init__(self, packet_queue, packet_filter=None): + super().__init__(packet_queue) self.packet_filter = packet_filter def process_packet(self, *args, **kwargs): @@ -118,7 +120,6 @@ def listen( """ signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) - config = ctx.obj["config"] if not aprs_login: click.echo(ctx.get_help()) @@ -132,27 +133,19 @@ def listen( ctx.fail("Must set --aprs-password or APRS_PASSWORD") ctx.exit() - config["aprs"]["login"] = aprs_login - config["aprs"]["password"] = aprs_password + # CONF.aprs_network.login = aprs_login + # config["aprs"]["password"] = aprs_password LOG.info(f"APRSD Listen Started version: {aprsd.__version__}") - flat_config = utils.flatten_dict(config) - LOG.info("Using CONFIG values:") - for x in flat_config: - if "password" in x or "aprsd.web.users.admin" in x: - LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX") - else: - LOG.info(f"{x} = {flat_config[x]}") - - stats.APRSDStats(config) + CONF.log_opt_values(LOG, logging.DEBUG) # Try and load saved MsgTrack list LOG.debug("Loading saved MsgTrack object.") # Initialize the client factory and create # The correct client object ready for use - client.ClientFactory.setup(config) + client.ClientFactory.setup() # Make sure we have 1 client transport enabled if not client.factory.is_client_enabled(): LOG.error("No Clients are enabled in config.") @@ -166,12 +159,11 @@ def listen( LOG.debug(f"Filter by '{filter}'") aprs_client.set_filter(filter) - keepalive = threads.KeepAliveThread(config=config) + keepalive = threads.KeepAliveThread() keepalive.start() LOG.debug("Create APRSDListenThread") listen_thread = APRSDListenThread( - config=config, packet_queue=threads.packet_queue, packet_filter=packet_filter, ) diff --git a/aprsd/cmds/server.py b/aprsd/cmds/server.py index b7a16a2..d936352 100644 --- a/aprsd/cmds/server.py +++ b/aprsd/cmds/server.py @@ -3,16 +3,16 @@ import signal import sys import click +from oslo_config import cfg import aprsd -from aprsd import ( - cli_helper, client, flask, packets, plugin, stats, threads, utils, -) from aprsd import aprsd as aprsd_main +from aprsd import cli_helper, client, flask, packets, plugin, threads, utils from aprsd.aprsd import cli from aprsd.threads import rx +CONF = cfg.CONF LOG = logging.getLogger("APRSD") @@ -32,10 +32,8 @@ LOG = logging.getLogger("APRSD") @cli_helper.process_standard_options def server(ctx, flush): """Start the aprsd server gateway process.""" - ctx.obj["config_file"] loglevel = ctx.obj["loglevel"] quiet = ctx.obj["quiet"] - config = ctx.obj["config"] signal.signal(signal.SIGINT, aprsd_main.signal_handler) signal.signal(signal.SIGTERM, aprsd_main.signal_handler) @@ -50,19 +48,11 @@ def server(ctx, flush): LOG.info(msg) LOG.info(f"APRSD Started version: {aprsd.__version__}") - flat_config = utils.flatten_dict(config) - LOG.info("Using CONFIG values:") - for x in flat_config: - if "password" in x or "aprsd.web.users.admin" in x: - LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX") - else: - LOG.info(f"{x} = {flat_config[x]}") - - stats.APRSDStats(config) + CONF.log_opt_values(LOG, logging.DEBUG) # Initialize the client factory and create # The correct client object ready for use - client.ClientFactory.setup(config) + client.ClientFactory.setup() # Make sure we have 1 client transport enabled if not client.factory.is_client_enabled(): LOG.error("No Clients are enabled in config.") @@ -77,30 +67,28 @@ def server(ctx, flush): client.factory.create().client # Now load the msgTrack from disk if any - packets.PacketList(config=config) + packets.PacketList() if flush: LOG.debug("Deleting saved MsgTrack.") - packets.PacketTrack(config=config).flush() - packets.WatchList(config=config) - packets.SeenList(config=config) + packets.PacketTrack().flush() + packets.WatchList() + packets.SeenList() else: # Try and load saved MsgTrack list LOG.debug("Loading saved MsgTrack object.") - packets.PacketTrack(config=config).load() - packets.WatchList(config=config).load() - packets.SeenList(config=config).load() + packets.PacketTrack().load() + packets.WatchList().load() + packets.SeenList().load() # Create the initial PM singleton and Register plugins LOG.info("Loading Plugin Manager and registering plugins") - plugin_manager = plugin.PluginManager(config) + plugin_manager = plugin.PluginManager() plugin_manager.setup_plugins() rx_thread = rx.APRSDPluginRXThread( packet_queue=threads.packet_queue, - config=config, ) process_thread = rx.APRSDPluginProcessPacketThread( - config=config, packet_queue=threads.packet_queue, ) rx_thread.start() @@ -108,19 +96,19 @@ def server(ctx, flush): packets.PacketTrack().restart() - keepalive = threads.KeepAliveThread(config=config) + keepalive = threads.KeepAliveThread() keepalive.start() - web_enabled = config.get("aprsd.web.enabled", default=False) + web_enabled = CONF.admin.web_enabled if web_enabled: aprsd_main.flask_enabled = True - (socketio, app) = flask.init_flask(config, loglevel, quiet) + (socketio, app) = flask.init_flask(loglevel, quiet) socketio.run( app, allow_unsafe_werkzeug=True, - host=config["aprsd"]["web"]["host"], - port=config["aprsd"]["web"]["port"], + host=CONF.admin.web_ip, + port=CONF.admin.web_port, ) # If there are items in the msgTracker, then save them diff --git a/aprsd/logging/log.py b/aprsd/logging/log.py index 855547d..a148756 100644 --- a/aprsd/logging/log.py +++ b/aprsd/logging/log.py @@ -4,10 +4,13 @@ from logging.handlers import RotatingFileHandler import queue import sys +from oslo_config import cfg + from aprsd import config as aprsd_config from aprsd.logging import rich as aprsd_logging +CONF = cfg.CONF LOG = logging.getLogger("APRSD") logging_queue = queue.Queue() @@ -15,13 +18,15 @@ logging_queue = queue.Queue() # Setup the logging faciility # to disable logging to stdout, but still log to file # use the --quiet option on the cmdln -def setup_logging(config, loglevel, quiet): +def setup_logging(loglevel, quiet): log_level = aprsd_config.LOG_LEVELS[loglevel] LOG.setLevel(log_level) - date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT) + date_format = CONF.logging.get("date_format", aprsd_config.DEFAULT_DATE_FORMAT) + rh = None + fh = None rich_logging = False - if config["aprsd"].get("rich_logging", False) and not quiet: + if CONF.logging.get("rich_logging", False) and not quiet: log_format = "%(message)s" log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) rh = aprsd_logging.APRSDRichHandler( @@ -32,8 +37,8 @@ def setup_logging(config, loglevel, quiet): LOG.addHandler(rh) rich_logging = True - log_file = config["aprsd"].get("logfile", None) - log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT) + log_file = CONF.logging.logfile + log_format = CONF.logging.logformat log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) if log_file: @@ -42,12 +47,16 @@ def setup_logging(config, loglevel, quiet): LOG.addHandler(fh) imap_logger = None - if config.get("aprsd.email.enabled", default=False) and config.get("aprsd.email.imap.debug", default=False): + if CONF.email_plugin.enabled and CONF.email_plugin.debug: imap_logger = logging.getLogger("imapclient.imaplib") imap_logger.setLevel(log_level) - imap_logger.addHandler(fh) + if rh: + imap_logger.addHandler(rh) + if fh: + imap_logger.addHandler(fh) - if config.get("aprsd.web.enabled", default=False): + + if CONF.admin.get("web_enabled", default=False): qh = logging.handlers.QueueHandler(logging_queue) q_log_formatter = logging.Formatter( fmt=aprsd_config.QUEUE_LOG_FORMAT, diff --git a/aprsd/packets/packet_list.py b/aprsd/packets/packet_list.py index d9c2c94..d42e9de 100644 --- a/aprsd/packets/packet_list.py +++ b/aprsd/packets/packet_list.py @@ -1,12 +1,14 @@ import logging import threading +from oslo_config import cfg import wrapt from aprsd import stats, utils from aprsd.packets import seen_list +CONF = cfg.CONF LOG = logging.getLogger("APRSD") @@ -15,7 +17,6 @@ class PacketList: _instance = None lock = threading.Lock() - config = None packet_list: utils.RingBuffer = utils.RingBuffer(1000) @@ -25,17 +26,8 @@ class PacketList: def __new__(cls, *args, **kwargs): if cls._instance is None: cls._instance = super().__new__(cls) - if "config" in kwargs: - cls._instance.config = kwargs["config"] return cls._instance - def __init__(self, config=None): - if config: - self.config = config - - def _is_initialized(self): - return self.config is not None - @wrapt.synchronized(lock) def __iter__(self): return iter(self.packet_list) diff --git a/aprsd/packets/seen_list.py b/aprsd/packets/seen_list.py index d68a933..f917eb9 100644 --- a/aprsd/packets/seen_list.py +++ b/aprsd/packets/seen_list.py @@ -2,11 +2,13 @@ import datetime import logging import threading +from oslo_config import cfg import wrapt from aprsd.utils import objectstore +CONF = cfg.CONF LOG = logging.getLogger("APRSD") @@ -16,21 +18,14 @@ class SeenList(objectstore.ObjectStoreMixin): _instance = None lock = threading.Lock() data: dict = {} - config = None def __new__(cls, *args, **kwargs): if cls._instance is None: cls._instance = super().__new__(cls) - if "config" in kwargs: - if "config" in kwargs: - cls._instance.config = kwargs["config"] - cls._instance._init_store() + cls._instance._init_store() cls._instance.data = {} return cls._instance - def is_initialized(self): - return self.config is not None - @wrapt.synchronized(lock) def update_seen(self, packet): callsign = None diff --git a/aprsd/packets/tracker.py b/aprsd/packets/tracker.py index e58e323..e62706a 100644 --- a/aprsd/packets/tracker.py +++ b/aprsd/packets/tracker.py @@ -1,12 +1,16 @@ import datetime import threading +from oslo_config import cfg import wrapt from aprsd.threads import tx from aprsd.utils import objectstore +CONF = cfg.CONF + + class PacketTrack(objectstore.ObjectStoreMixin): """Class to keep track of outstanding text messages. @@ -23,7 +27,6 @@ class PacketTrack(objectstore.ObjectStoreMixin): _instance = None _start_time = None lock = threading.Lock() - config = None data: dict = {} total_tracked: int = 0 @@ -32,14 +35,9 @@ class PacketTrack(objectstore.ObjectStoreMixin): if cls._instance is None: cls._instance = super().__new__(cls) cls._instance._start_time = datetime.datetime.now() - if "config" in kwargs: - cls._instance.config = kwargs["config"] cls._instance._init_store() return cls._instance - def is_initialized(self): - return self.config is not None - @wrapt.synchronized(lock) def __getitem__(self, name): return self.data[name] diff --git a/aprsd/packets/watch_list.py b/aprsd/packets/watch_list.py index c1da79f..1a47d91 100644 --- a/aprsd/packets/watch_list.py +++ b/aprsd/packets/watch_list.py @@ -2,12 +2,14 @@ import datetime import logging import threading +from oslo_config import cfg import wrapt from aprsd import utils from aprsd.utils import objectstore +CONF = cfg.CONF LOG = logging.getLogger("APRSD") @@ -17,24 +19,22 @@ class WatchList(objectstore.ObjectStoreMixin): _instance = None lock = threading.Lock() data = {} - config = None def __new__(cls, *args, **kwargs): if cls._instance is None: cls._instance = super().__new__(cls) - if "config" in kwargs: - cls._instance.config = kwargs["config"] - cls._instance._init_store() + cls._instance._init_store() cls._instance.data = {} return cls._instance def __init__(self, config=None): - if config: - self.config = config + ring_size = CONF.watch_list.packet_keep_count - ring_size = config["aprsd"]["watch_list"].get("packet_keep_count", 10) + if not self.is_enabled(): + LOG.info("Watch List is disabled.") - for callsign in config["aprsd"]["watch_list"].get("callsigns", []): + if CONF.watch_list.callsigns: + for callsign in CONF.watch_list.callsigns: call = callsign.replace("*", "") # FIXME(waboring) - we should fetch the last time we saw # a beacon from a callsign or some other mechanism to find @@ -47,14 +47,8 @@ class WatchList(objectstore.ObjectStoreMixin): ), } - def is_initialized(self): - return self.config is not None - def is_enabled(self): - if self.config and "watch_list" in self.config["aprsd"]: - return self.config["aprsd"]["watch_list"].get("enabled", False) - else: - return False + return CONF.watch_list.enabled def callsign_in_watchlist(self, callsign): return callsign in self.data @@ -78,9 +72,8 @@ class WatchList(objectstore.ObjectStoreMixin): return str(now - self.last_seen(callsign)) def max_delta(self, seconds=None): - watch_list_conf = self.config["aprsd"]["watch_list"] if not seconds: - seconds = watch_list_conf["alert_time_seconds"] + seconds = CONF.watch_list.alert_time_seconds max_timeout = {"seconds": seconds} return datetime.timedelta(**max_timeout) diff --git a/aprsd/plugin.py b/aprsd/plugin.py index d25adad..aeb45ef 100644 --- a/aprsd/plugin.py +++ b/aprsd/plugin.py @@ -7,6 +7,7 @@ import re import textwrap import threading +from oslo_config import cfg import pluggy import aprsd @@ -15,6 +16,7 @@ from aprsd.packets import watch_list # setup the global logger +CONF = cfg.CONF LOG = logging.getLogger("APRSD") CORE_MESSAGE_PLUGINS = [ @@ -211,6 +213,10 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta): @hookimpl def filter(self, packet: packets.core.MessagePacket): + if not self.enabled: + LOG.info(f"{self.__class__.__name__} is not enabled.") + return None + result = None message = packet.get("message_text", None) @@ -220,7 +226,7 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta): # Only process messages destined for us # and is an APRS message format and has a message. if ( - tocall == self.config["aprs"]["login"] + tocall == CONF.callsign and msg_format == "message" and message ): @@ -249,12 +255,11 @@ class APRSFIKEYMixin: """Mixin class to enable checking the existence of the aprs.fi apiKey.""" def ensure_aprs_fi_key(self): - try: - self.config.check_option(["services", "aprs.fi", "apiKey"]) - self.enabled = True - except Exception as ex: - LOG.error(f"Failed to find config aprs.fi:apikey {ex}") + if not CONF.aprs_fi.apiKey: + LOG.error("Config aprs_fi.apiKey is not set") self.enabled = False + else: + self.enabled = True class HelpPlugin(APRSDRegexCommandPluginBase): @@ -410,21 +415,28 @@ class PluginManager: ) if plugin_obj: if isinstance(plugin_obj, APRSDWatchListPluginBase): - LOG.info( - "Registering WatchList plugin '{}'({})".format( - plugin_name, - plugin_obj.version, - ), - ) - self._watchlist_pm.register(plugin_obj) + if plugin_obj.enabled: + LOG.info( + "Registering WatchList plugin '{}'({})".format( + plugin_name, + plugin_obj.version, + ), + ) + self._watchlist_pm.register(plugin_obj) + else: + LOG.warning(f"Plugin {plugin_obj.__class__.__name__} is disabled") else: - LOG.info( - "Registering plugin '{}'({})".format( - plugin_name, - plugin_obj.version, - ), - ) - self._pluggy_pm.register(plugin_obj) + if plugin_obj.enabled: + LOG.info( + "Registering plugin '{}'({}) -- {}".format( + plugin_name, + plugin_obj.version, + plugin_obj.command_regex, + ), + ) + self._pluggy_pm.register(plugin_obj) + else: + LOG.warning(f"Plugin {plugin_obj.__class__.__name__} is disabled") except Exception as ex: LOG.error(f"Couldn't load plugin '{plugin_name}'") LOG.exception(ex) @@ -443,7 +455,7 @@ class PluginManager: _help = HelpPlugin(self.config) self._pluggy_pm.register(_help) - enabled_plugins = self.config["aprsd"].get("enabled_plugins", None) + enabled_plugins = CONF.enabled_plugins if enabled_plugins: for p_name in enabled_plugins: self._load_plugin(p_name) diff --git a/aprsd/plugin_utils.py b/aprsd/plugin_utils.py index bad0bb3..750e73a 100644 --- a/aprsd/plugin_utils.py +++ b/aprsd/plugin_utils.py @@ -26,13 +26,18 @@ def get_aprs_fi(api_key, callsign): def get_weather_gov_for_gps(lat, lon): LOG.debug(f"Fetch station at {lat}, {lon}") + headers = requests.utils.default_headers() + headers.update( + {"User-Agent": "(aprsd, waboring@hemna.com)"}, + ) try: url2 = ( - "https://forecast.weather.gov/MapClick.php?lat=%s" - "&lon=%s&FcstType=json" % (lat, lon) + #"https://forecast.weather.gov/MapClick.php?lat=%s" + #"&lon=%s&FcstType=json" % (lat, lon) + f"https://api.weather.gov/points/{lat},{lon}" ) LOG.debug(f"Fetching weather '{url2}'") - response = requests.get(url2) + response = requests.get(url2, headers=headers) except Exception as e: LOG.error(e) raise Exception("Failed to get weather") diff --git a/aprsd/plugins/email.py b/aprsd/plugins/email.py index e5ea994..b7748ab 100644 --- a/aprsd/plugins/email.py +++ b/aprsd/plugins/email.py @@ -9,13 +9,16 @@ import threading import time import imapclient +from oslo_config import cfg from aprsd import packets, plugin, stats, threads from aprsd.threads import tx from aprsd.utils import trace +CONF = cfg.CONF LOG = logging.getLogger("APRSD") +shortcuts_dict = None class EmailInfo: @@ -71,18 +74,18 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase): def setup(self): """Ensure that email is enabled and start the thread.""" - - email_enabled = self.config["aprsd"]["email"].get("enabled", False) - if email_enabled: + if CONF.email_plugin.enabled: self.enabled = True + shortcuts = _build_shortcuts_dict() + LOG.info(f"Email shortcuts {shortcuts}") + else: LOG.info("Email services not enabled.") + self.enabled = False def create_threads(self): if self.enabled: - return APRSDEmailThread( - config=self.config, - ) + return APRSDEmailThread() @trace.trace def process(self, packet: packets.MessagePacket): @@ -97,18 +100,18 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase): ack = packet.get("msgNo", "0") reply = None - if not self.config["aprsd"]["email"].get("enabled", False): + if not CONF.email_plugin.enabled: LOG.debug("Email is not enabled in config file ignoring.") return "Email not enabled." - searchstring = "^" + self.config["ham"]["callsign"] + ".*" + searchstring = "^" + CONF.email_plugin.callsign + ".*" # only I can do email if re.search(searchstring, fromcall): # digits only, first one is number of emails to resend r = re.search("^-([0-9])[0-9]*$", message) if r is not None: LOG.debug("RESEND EMAIL") - resend_email(self.config, r.group(1), fromcall) + resend_email(r.group(1), fromcall) reply = packets.NULL_MESSAGE # -user@address.com body of email elif re.search(r"^-([A-Za-z0-9_\-\.@]+) (.*)", message): @@ -118,7 +121,7 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase): to_addr = a.group(1) content = a.group(2) - email_address = get_email_from_shortcut(self.config, to_addr) + email_address = get_email_from_shortcut(to_addr) if not email_address: reply = "Bad email address" return reply @@ -128,7 +131,7 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase): content = ( "Click for my location: http://aprs.fi/{}" "" ).format( - self.config["ham"]["callsign"], + CONF.email_plugin.callsign, ) too_soon = 0 now = time.time() @@ -141,7 +144,7 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase): too_soon = 1 if not too_soon or ack == 0: LOG.info(f"Send email '{content}'") - send_result = send_email(self.config, to_addr, content) + send_result = send_email(to_addr, content) reply = packets.NULL_MESSAGE if send_result != 0: reply = f"-{to_addr} failed" @@ -169,9 +172,9 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase): return reply -def _imap_connect(config): - imap_port = config["aprsd"]["email"]["imap"].get("port", 143) - use_ssl = config["aprsd"]["email"]["imap"].get("use_ssl", False) +def _imap_connect(): + imap_port = CONF.email_plugin.imap_port + use_ssl = CONF.email_plugin.imap_use_ssl # host = CONFIG["aprsd"]["email"]["imap"]["host"] # msg = "{}{}:{}".format("TLS " if use_ssl else "", host, imap_port) # LOG.debug("Connect to IMAP host {} with user '{}'". @@ -179,7 +182,7 @@ def _imap_connect(config): try: server = imapclient.IMAPClient( - config["aprsd"]["email"]["imap"]["host"], + CONF.email_plugin.imap_host, port=imap_port, use_uid=True, ssl=use_ssl, @@ -191,8 +194,8 @@ def _imap_connect(config): try: server.login( - config["aprsd"]["email"]["imap"]["login"], - config["aprsd"]["email"]["imap"]["password"], + CONF.email_plugin.imap_login, + CONF.email_plugin.imap_password, ) except (imaplib.IMAP4.error, Exception) as e: msg = getattr(e, "message", repr(e)) @@ -208,15 +211,15 @@ def _imap_connect(config): return server -def _smtp_connect(config): - host = config["aprsd"]["email"]["smtp"]["host"] - smtp_port = config["aprsd"]["email"]["smtp"]["port"] - use_ssl = config["aprsd"]["email"]["smtp"].get("use_ssl", False) +def _smtp_connect(): + host = CONF.email_plugin.smtp_host + smtp_port = CONF.email_plugin.smtp_port + use_ssl = CONF.email_plugin.smtp_use_ssl msg = "{}{}:{}".format("SSL " if use_ssl else "", host, smtp_port) LOG.debug( "Connect to SMTP host {} with user '{}'".format( msg, - config["aprsd"]["email"]["imap"]["login"], + CONF.email_plugin.smtp_login, ), ) @@ -239,15 +242,15 @@ def _smtp_connect(config): LOG.debug(f"Connected to smtp host {msg}") - debug = config["aprsd"]["email"]["smtp"].get("debug", False) + debug = CONF.email_plugin.debug if debug: server.set_debuglevel(5) server.sendmail = trace.trace(server.sendmail) try: server.login( - config["aprsd"]["email"]["smtp"]["login"], - config["aprsd"]["email"]["smtp"]["password"], + CONF.email_plugin.smtp_login, + CONF.email_plugin.smtp_password, ) except Exception: LOG.error("Couldn't connect to SMTP Server") @@ -257,22 +260,40 @@ def _smtp_connect(config): return server -def get_email_from_shortcut(config, addr): - if config["aprsd"]["email"].get("shortcuts", False): - return config["aprsd"]["email"]["shortcuts"].get(addr, addr) +def _build_shortcuts_dict(): + global shortcuts_dict + if not shortcuts_dict: + if CONF.email_plugin.email_shortcuts: + shortcuts_dict = {} + tmp = CONF.email_plugin.email_shortcuts + for combo in tmp: + entry = combo.split("=") + shortcuts_dict[entry[0]] = entry[1] + else: + shortcuts_dict = {} + + LOG.info(f"Shortcuts Dict {shortcuts_dict}") + return shortcuts_dict + + +def get_email_from_shortcut(addr): + if CONF.email_plugin.email_shortcuts: + shortcuts = _build_shortcuts_dict() + LOG.info(f"Shortcut lookup {addr} returns {shortcuts.get(addr, addr)}") + return shortcuts.get(addr, addr) else: return addr -def validate_email_config(config, disable_validation=False): +def validate_email_config(disable_validation=False): """function to simply ensure we can connect to email services. This helps with failing early during startup. """ LOG.info("Checking IMAP configuration") - imap_server = _imap_connect(config) + imap_server = _imap_connect() LOG.info("Checking SMTP configuration") - smtp_server = _smtp_connect(config) + smtp_server = _smtp_connect() if imap_server and smtp_server: return True @@ -376,16 +397,16 @@ def parse_email(msgid, data, server): @trace.trace -def send_email(config, to_addr, content): - shortcuts = config["aprsd"]["email"]["shortcuts"] - email_address = get_email_from_shortcut(config, to_addr) +def send_email(to_addr, content): + shortcuts = _build_shortcuts_dict() + email_address = get_email_from_shortcut(to_addr) LOG.info("Sending Email_________________") if to_addr in shortcuts: LOG.info(f"To : {to_addr}") to_addr = email_address LOG.info(f" ({to_addr})") - subject = config["ham"]["callsign"] + subject = CONF.email_plugin.callsign # content = content + "\n\n(NOTE: reply with one line)" LOG.info(f"Subject : {subject}") LOG.info(f"Body : {content}") @@ -395,13 +416,13 @@ def send_email(config, to_addr, content): msg = MIMEText(content) msg["Subject"] = subject - msg["From"] = config["aprsd"]["email"]["smtp"]["login"] + msg["From"] = CONF.email_plugin.smtp_login msg["To"] = to_addr - server = _smtp_connect(config) + server = _smtp_connect() if server: try: server.sendmail( - config["aprsd"]["email"]["smtp"]["login"], + CONF.email_plugin.smtp_login, [to_addr], msg.as_string(), ) @@ -415,19 +436,19 @@ def send_email(config, to_addr, content): @trace.trace -def resend_email(config, count, fromcall): +def resend_email(count, fromcall): date = datetime.datetime.now() month = date.strftime("%B")[:3] # Nov, Mar, Apr day = date.day year = date.year today = f"{day}-{month}-{year}" - shortcuts = config["aprsd"]["email"]["shortcuts"] + shortcuts = _build_shortcuts_dict() # swap key/value shortcuts_inverted = {v: k for k, v in shortcuts.items()} try: - server = _imap_connect(config) + server = _imap_connect() except Exception: LOG.exception("Failed to Connect to IMAP. Cannot resend email ") return @@ -467,7 +488,7 @@ def resend_email(config, count, fromcall): reply = "-" + from_addr + " * " + body.decode(errors="ignore") tx.send( packets.MessagePacket( - from_call=config["aprsd"]["callsign"], + from_call=CONF.callsign, to_call=fromcall, message_text=reply, ), @@ -490,7 +511,7 @@ def resend_email(config, count, fromcall): ) tx.send( packets.MessagePacket( - from_call=config["aprsd"]["callsign"], + from_call=CONF.callsign, to_call=fromcall, message_text=reply, ), @@ -504,9 +525,8 @@ def resend_email(config, count, fromcall): class APRSDEmailThread(threads.APRSDThread): - def __init__(self, config): + def __init__(self): super().__init__("EmailThread") - self.config = config self.past = datetime.datetime.now() def loop(self): @@ -527,7 +547,7 @@ class APRSDEmailThread(threads.APRSDThread): f"check_email_delay is {EmailInfo().delay} seconds ", ) - shortcuts = self.config["aprsd"]["email"]["shortcuts"] + shortcuts = _build_shortcuts_dict() # swap key/value shortcuts_inverted = {v: k for k, v in shortcuts.items()} @@ -538,7 +558,7 @@ class APRSDEmailThread(threads.APRSDThread): today = f"{day}-{month}-{year}" try: - server = _imap_connect(self.config) + server = _imap_connect() except Exception: LOG.exception("IMAP Failed to connect") return True @@ -611,8 +631,8 @@ class APRSDEmailThread(threads.APRSDThread): # config ham.callsign tx.send( packets.MessagePacket( - from_call=self.config["aprsd"]["callsign"], - to_call=self.config["ham"]["callsign"], + from_call=CONF.callsign, + to_call=CONF.email_plugin.callsign, message_text=reply, ), ) diff --git a/aprsd/plugins/location.py b/aprsd/plugins/location.py index 4086c85..84c21c5 100644 --- a/aprsd/plugins/location.py +++ b/aprsd/plugins/location.py @@ -2,10 +2,13 @@ import logging import re import time +from oslo_config import cfg + from aprsd import packets, plugin, plugin_utils from aprsd.utils import trace +CONF = cfg.CONF LOG = logging.getLogger("APRSD") @@ -26,7 +29,7 @@ class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin): message = packet.get("message_text", None) # ack = packet.get("msgNo", "0") - api_key = self.config["services"]["aprs.fi"]["apiKey"] + api_key = CONF.aprs_fi.apiKey # optional second argument is a callsign to search a = re.search(r"^.*\s+(.*)", message) diff --git a/aprsd/plugins/notify.py b/aprsd/plugins/notify.py index 9009c66..01ac90f 100644 --- a/aprsd/plugins/notify.py +++ b/aprsd/plugins/notify.py @@ -1,8 +1,11 @@ import logging +from oslo_config import cfg + from aprsd import packets, plugin +CONF = cfg.CONF LOG = logging.getLogger("APRSD") @@ -20,7 +23,7 @@ class NotifySeenPlugin(plugin.APRSDWatchListPluginBase): def process(self, packet: packets.MessagePacket): LOG.info("NotifySeenPlugin") - notify_callsign = self.config["aprsd"]["watch_list"]["alert_callsign"] + notify_callsign = CONF.watch_list.alert_callsign fromcall = packet.from_call wl = packets.WatchList() @@ -38,7 +41,7 @@ class NotifySeenPlugin(plugin.APRSDWatchListPluginBase): packet_type = packet.__class__.__name__ # we shouldn't notify the alert user that they are online. pkt = packets.MessagePacket( - from_call=self.config["aprsd"]["callsign"], + from_call=CONF.callsign, to_call=notify_callsign, message_text=( f"{fromcall} was just seen by type:'{packet_type}'" diff --git a/aprsd/plugins/query.py b/aprsd/plugins/query.py index 04e6b0c..2d6d5ec 100644 --- a/aprsd/plugins/query.py +++ b/aprsd/plugins/query.py @@ -2,11 +2,14 @@ import datetime import logging import re +from oslo_config import cfg + from aprsd import packets, plugin from aprsd.packets import tracker from aprsd.utils import trace +CONF = cfg.CONF LOG = logging.getLogger("APRSD") @@ -17,6 +20,13 @@ class QueryPlugin(plugin.APRSDRegexCommandPluginBase): command_name = "query" short_description = "APRSD Owner command to query messages in the MsgTrack" + def setup(self): + """Do any plugin setup here.""" + if not CONF.query_plugin.callsign: + LOG.error("Config query_plugin.callsign not set. Disabling plugin") + self.enabled = False + self.enabled = True + @trace.trace def process(self, packet: packets.MessagePacket): LOG.info("Query COMMAND") @@ -32,7 +42,7 @@ class QueryPlugin(plugin.APRSDRegexCommandPluginBase): now.strftime("%H:%M:%S"), ) - searchstring = "^" + self.config["ham"]["callsign"] + ".*" + searchstring = "^" + CONF.query_plugin.callsign + ".*" # only I can do admin commands if re.search(searchstring, fromcall): diff --git a/aprsd/plugins/time.py b/aprsd/plugins/time.py index 8b5edd6..f6261b9 100644 --- a/aprsd/plugins/time.py +++ b/aprsd/plugins/time.py @@ -2,12 +2,14 @@ import logging import re import time +from oslo_config import cfg import pytz from aprsd import packets, plugin, plugin_utils from aprsd.utils import fuzzy, trace +CONF = cfg.CONF LOG = logging.getLogger("APRSD") @@ -74,7 +76,7 @@ class TimeOWMPlugin(TimePlugin, plugin.APRSFIKEYMixin): # if no second argument, search for calling station searchcall = fromcall - api_key = self.config["services"]["aprs.fi"]["apiKey"] + api_key = CONF.aprs_fi.apiKey try: aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall) except Exception as ex: diff --git a/aprsd/plugins/weather.py b/aprsd/plugins/weather.py index acf1208..b14ac98 100644 --- a/aprsd/plugins/weather.py +++ b/aprsd/plugins/weather.py @@ -2,12 +2,14 @@ import json import logging import re +from oslo_config import cfg import requests from aprsd import plugin, plugin_utils from aprsd.utils import trace +CONF = cfg.CONF LOG = logging.getLogger("APRSD") @@ -34,10 +36,10 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin) @trace.trace def process(self, packet): LOG.info("Weather Plugin") - fromcall = packet.get("from") + fromcall = packet.from_call # message = packet.get("message_text", None) # ack = packet.get("msgNo", "0") - api_key = self.config["services"]["aprs.fi"]["apiKey"] + api_key = CONF.aprs_fi.apiKey try: aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall) except Exception as ex: @@ -58,17 +60,23 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin) LOG.error(f"Couldn't fetch forecast.weather.gov '{ex}'") return "Unable to get weather" - reply = ( - "%sF(%sF/%sF) %s. %s, %s." - % ( - wx_data["currentobservation"]["Temp"], - wx_data["data"]["temperature"][0], - wx_data["data"]["temperature"][1], - wx_data["data"]["weather"][0], - wx_data["time"]["startPeriodName"][1], - wx_data["data"]["weather"][1], - ) - ).rstrip() + LOG.info(f"WX data {wx_data}") + + if wx_data["success"] == False: + # Failed to fetch the weather + reply = "Failed to fetch weather for location" + else: + reply = ( + "%sF(%sF/%sF) %s. %s, %s." + % ( + wx_data["currentobservation"]["Temp"], + wx_data["data"]["temperature"][0], + wx_data["data"]["temperature"][1], + wx_data["data"]["weather"][0], + wx_data["time"]["startPeriodName"][1], + wx_data["data"]["weather"][1], + ) + ).rstrip() LOG.debug(f"reply: '{reply}' ") return reply @@ -119,13 +127,7 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin): # if no second argument, search for calling station fromcall = fromcall - try: - self.config.exists(["services", "aprs.fi", "apiKey"]) - except Exception as ex: - LOG.error(f"Failed to find config aprs.fi:apikey {ex}") - return "No aprs.fi apikey found" - - api_key = self.config["services"]["aprs.fi"]["apiKey"] + api_key = CONF.aprs_fi.apiKey try: aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall) @@ -187,6 +189,13 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase): command_name = "OpenWeatherMap" short_description = "OpenWeatherMap weather of GPS Beacon location" + def setup(self): + if not CONF.owm_weather_plugin.apiKey: + LOG.error("Config.owm_weather_plugin.apiKey is not set. Disabling") + self.enabled = False + else: + self.enabled = True + def help(self): _help = [ "openweathermap: Send {} to get weather " @@ -209,13 +218,8 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase): else: searchcall = fromcall - try: - self.config.exists(["services", "aprs.fi", "apiKey"]) - except Exception as ex: - LOG.error(f"Failed to find config aprs.fi:apikey {ex}") - return "No aprs.fi apikey found" + api_key = CONF.aprs_fi.apiKey - api_key = self.config["services"]["aprs.fi"]["apiKey"] try: aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall) except Exception as ex: @@ -230,21 +234,8 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase): lat = aprs_data["entries"][0]["lat"] lon = aprs_data["entries"][0]["lng"] - try: - self.config.exists(["services", "openweathermap", "apiKey"]) - except Exception as ex: - LOG.error(f"Failed to find config openweathermap:apiKey {ex}") - return "No openweathermap apiKey found" - - try: - self.config.exists(["aprsd", "units"]) - except Exception: - LOG.debug("Couldn't find untis in aprsd:services:units") - units = "metric" - else: - units = self.config["aprsd"]["units"] - - api_key = self.config["services"]["openweathermap"]["apiKey"] + units = CONF.units + api_key = CONF.owm_weather_plugin.apiKey try: wx_data = plugin_utils.fetch_openweathermap( api_key, @@ -317,6 +308,16 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase): command_name = "AVWXWeather" short_description = "AVWX weather of GPS Beacon location" + def setup(self): + if not CONF.avwx_plugin.base_url: + LOG.error("Config avwx_plugin.base_url not specified. Disabling") + return False + elif not CONF.avwx_plugin.apiKey: + LOG.error("Config avwx_plugin.apiKey not specified. Disabling") + return False + else: + return True + def help(self): _help = [ "avwxweather: Send {} to get weather " @@ -339,13 +340,7 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase): else: searchcall = fromcall - try: - self.config.exists(["services", "aprs.fi", "apiKey"]) - except Exception as ex: - LOG.error(f"Failed to find config aprs.fi:apikey {ex}") - return "No aprs.fi apikey found" - - api_key = self.config["services"]["aprs.fi"]["apiKey"] + api_key = CONF.aprs_fi.apiKey try: aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall) except Exception as ex: @@ -360,21 +355,8 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase): lat = aprs_data["entries"][0]["lat"] lon = aprs_data["entries"][0]["lng"] - try: - self.config.exists(["services", "avwx", "apiKey"]) - except Exception as ex: - LOG.error(f"Failed to find config avwx:apiKey {ex}") - return "No avwx apiKey found" - - try: - self.config.exists(self.config, ["services", "avwx", "base_url"]) - except Exception as ex: - LOG.debug(f"Didn't find avwx:base_url {ex}") - base_url = "https://avwx.rest" - else: - base_url = self.config["services"]["avwx"]["base_url"] - - api_key = self.config["services"]["avwx"]["apiKey"] + api_key = CONF.avwx_plugin.apiKey + base_url = CONF.avwx_plugin.base_url token = f"TOKEN {api_key}" headers = {"Authorization": token} try: diff --git a/aprsd/stats.py b/aprsd/stats.py index 91b9b8c..a5ba446 100644 --- a/aprsd/stats.py +++ b/aprsd/stats.py @@ -2,12 +2,14 @@ import datetime import logging import threading +from oslo_config import cfg import wrapt import aprsd from aprsd import packets, plugin, utils +CONF = cfg.CONF LOG = logging.getLogger("APRSD") @@ -15,7 +17,6 @@ class APRSDStats: _instance = None lock = threading.Lock() - config = None start_time = None _aprsis_server = None @@ -67,10 +68,6 @@ class APRSDStats: cls._instance._aprsis_keepalive = datetime.datetime.now() return cls._instance - def __init__(self, config=None): - if config: - self.config = config - @wrapt.synchronized(lock) @property def uptime(self): @@ -191,7 +188,7 @@ class APRSDStats: "aprsd": { "version": aprsd.__version__, "uptime": utils.strfdelta(self.uptime), - "callsign": self.config["aprsd"]["callsign"], + "callsign": CONF.callsign, "memory_current": int(self.memory), "memory_current_str": utils.human_size(self.memory), "memory_peak": int(self.memory_peak), @@ -201,7 +198,7 @@ class APRSDStats: }, "aprs-is": { "server": str(self.aprsis_server), - "callsign": self.config["aprs"]["login"], + "callsign": CONF.aprs_network.login, "last_update": last_aprsis_keepalive, }, "packets": { @@ -215,7 +212,7 @@ class APRSDStats: "ack_sent": self._pkt_cnt["AckPacket"]["tx"], }, "email": { - "enabled": self.config["aprsd"]["email"]["enabled"], + "enabled": CONF.email_plugin.enabled, "sent": int(self._email_tx), "received": int(self._email_rx), "thread_last_update": last_update, diff --git a/aprsd/threads/keep_alive.py b/aprsd/threads/keep_alive.py index 9c8007f..a578f7c 100644 --- a/aprsd/threads/keep_alive.py +++ b/aprsd/threads/keep_alive.py @@ -3,10 +3,13 @@ import logging import time import tracemalloc +from oslo_config import cfg + from aprsd import client, packets, stats, utils from aprsd.threads import APRSDThread, APRSDThreadList +CONF = cfg.CONF LOG = logging.getLogger("APRSD") @@ -14,10 +17,9 @@ class KeepAliveThread(APRSDThread): cntr = 0 checker_time = datetime.datetime.now() - def __init__(self, config): + def __init__(self): tracemalloc.start() super().__init__("KeepAlive") - self.config = config max_timeout = {"hours": 0.0, "minutes": 2, "seconds": 0} self.max_delta = datetime.timedelta(**max_timeout) @@ -40,15 +42,9 @@ class KeepAliveThread(APRSDThread): stats_obj.set_memory(current) stats_obj.set_memory_peak(peak) - try: - login = self.config["aprsd"]["callsign"] - except KeyError: - login = self.config["ham"]["callsign"] + login = CONF.callsign - if pkt_tracker.is_initialized(): - tracked_packets = len(pkt_tracker) - else: - tracked_packets = 0 + tracked_packets = len(pkt_tracker) keepalive = ( "{} - Uptime {} RX:{} TX:{} Tracker:{} Msgs TX:{} RX:{} " @@ -77,7 +73,7 @@ class KeepAliveThread(APRSDThread): if delta > self.max_delta: # We haven't gotten a keepalive from aprs-is in a while # reset the connection.a - if not client.KISSClient.is_enabled(self.config): + if not client.KISSClient.is_enabled(): LOG.warning(f"Resetting connection to APRS-IS {delta}") client.factory.create().reset() diff --git a/aprsd/threads/rx.py b/aprsd/threads/rx.py index 2880d70..3689040 100644 --- a/aprsd/threads/rx.py +++ b/aprsd/threads/rx.py @@ -4,18 +4,19 @@ import queue import time import aprslib +from oslo_config import cfg from aprsd import client, packets, plugin from aprsd.threads import APRSDThread, tx +CONF = cfg.CONF LOG = logging.getLogger("APRSD") class APRSDRXThread(APRSDThread): - def __init__(self, config, packet_queue): + def __init__(self, packet_queue): super().__init__("RX_MSG") - self.config = config self.packet_queue = packet_queue self._client = client.factory.create() @@ -80,8 +81,7 @@ class APRSDProcessPacketThread(APRSDThread): will ack a message before sending the packet to the subclass for processing.""" - def __init__(self, config, packet_queue): - self.config = config + def __init__(self, packet_queue): self.packet_queue = packet_queue super().__init__("ProcessPKT") self._loop_cnt = 1 @@ -106,7 +106,7 @@ class APRSDProcessPacketThread(APRSDThread): def process_packet(self, packet): """Process a packet received from aprs-is server.""" LOG.debug(f"RXPKT-LOOP {self._loop_cnt}") - our_call = self.config["aprsd"]["callsign"].lower() + our_call = CONF.callsign.lower() from_call = packet.from_call if packet.addresse: @@ -133,7 +133,7 @@ class APRSDProcessPacketThread(APRSDThread): # send an ack last tx.send( packets.AckPacket( - from_call=self.config["aprsd"]["callsign"], + from_call=CONF.callsign, to_call=from_call, msgNo=msg_id, ), @@ -178,11 +178,11 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread): if isinstance(subreply, packets.Packet): tx.send(subreply) else: - wl = self.config["aprsd"]["watch_list"] + wl = CONF.watch_list to_call = wl["alert_callsign"] tx.send( packets.MessagePacket( - from_call=self.config["aprsd"]["callsign"], + from_call=CONF.callsign, to_call=to_call, message_text=subreply, ), @@ -219,7 +219,7 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread): else: tx.send( packets.MessagePacket( - from_call=self.config["aprsd"]["callsign"], + from_call=CONF.callsign, to_call=from_call, message_text=subreply, ), @@ -238,7 +238,7 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread): LOG.debug(f"Sending '{reply}'") tx.send( packets.MessagePacket( - from_call=self.config["aprsd"]["callsign"], + from_call=CONF.callsign, to_call=from_call, message_text=reply, ), @@ -246,12 +246,12 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread): # If the message was for us and we didn't have a # response, then we send a usage statement. - if to_call == self.config["aprsd"]["callsign"] and not replied: + if to_call == CONF.callsign and not replied: LOG.warning("Sending help!") message_text = "Unknown command! Send 'help' message for help" tx.send( packets.MessagePacket( - from_call=self.config["aprsd"]["callsign"], + from_call=CONF.callsign, to_call=from_call, message_text=message_text, ), @@ -260,11 +260,11 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread): LOG.error("Plugin failed!!!") LOG.exception(ex) # Do we need to send a reply? - if to_call == self.config["aprsd"]["callsign"]: + if to_call == CONF.callsign: reply = "A Plugin failed! try again?" tx.send( packets.MessagePacket( - from_call=self.config["aprsd"]["callsign"], + from_call=CONF.callsign, to_call=from_call, message_text=reply, ), diff --git a/aprsd/utils/objectstore.py b/aprsd/utils/objectstore.py index 4349213..b71ee7a 100644 --- a/aprsd/utils/objectstore.py +++ b/aprsd/utils/objectstore.py @@ -1,16 +1,16 @@ -import abc import logging import os import pathlib import pickle -from aprsd import config as aprsd_config +from oslo_config import cfg +CONF = cfg.CONF LOG = logging.getLogger("APRSD") -class ObjectStoreMixin(metaclass=abc.ABCMeta): +class ObjectStoreMixin: """Class 'MIXIN' intended to save/load object data. The asumption of how this mixin is used: @@ -24,13 +24,6 @@ class ObjectStoreMixin(metaclass=abc.ABCMeta): When APRSD Starts, it calls load() aprsd server -f (flush) will wipe all saved objects. """ - @abc.abstractmethod - def is_initialized(self): - """Return True if the class has been setup correctly. - - If this returns False, the ObjectStore doesn't save anything. - - """ def __len__(self): return len(self.data) @@ -44,25 +37,18 @@ class ObjectStoreMixin(metaclass=abc.ABCMeta): return self.data[id] def _init_store(self): - if self.is_initialized(): - sl = self._save_location() - if not os.path.exists(sl): - LOG.warning(f"Save location {sl} doesn't exist") - try: - os.makedirs(sl) - except Exception as ex: - LOG.exception(ex) - else: - LOG.warning(f"{self.__class__.__name__} is not initialized") - - def _save_location(self): - save_location = self.config.get("aprsd.save_location", None) - if not save_location: - save_location = aprsd_config.DEFAULT_CONFIG_DIR - return save_location + if not CONF.enable_save: + return + sl = CONF.save_location + if not os.path.exists(sl): + LOG.warning(f"Save location {sl} doesn't exist") + try: + os.makedirs(sl) + except Exception as ex: + LOG.exception(ex) def _save_filename(self): - save_location = self._save_location() + save_location = CONF.save_location return "{}/{}.p".format( save_location, @@ -79,45 +65,48 @@ class ObjectStoreMixin(metaclass=abc.ABCMeta): def save(self): """Save any queued to disk?""" - if self.is_initialized(): - if len(self) > 0: - LOG.info( - f"{self.__class__.__name__}::Saving" - f" {len(self)} entries to disk at" - f"{self._save_location()}", - ) - with open(self._save_filename(), "wb+") as fp: - pickle.dump(self._dump(), fp) - else: - LOG.debug( - "{} Nothing to save, flushing old save file '{}'".format( - self.__class__.__name__, - self._save_filename(), - ), - ) - self.flush() + if not CONF.enable_save: + return + if len(self) > 0: + LOG.info( + f"{self.__class__.__name__}::Saving" + f" {len(self)} entries to disk at" + f"{CONF.save_location}", + ) + with open(self._save_filename(), "wb+") as fp: + pickle.dump(self._dump(), fp) + else: + LOG.debug( + "{} Nothing to save, flushing old save file '{}'".format( + self.__class__.__name__, + self._save_filename(), + ), + ) + self.flush() def load(self): - if self.is_initialized(): - if os.path.exists(self._save_filename()): - try: - with open(self._save_filename(), "rb") as fp: - raw = pickle.load(fp) - if raw: - self.data = raw - LOG.debug( - f"{self.__class__.__name__}::Loaded {len(self)} entries from disk.", - ) - LOG.debug(f"{self.data}") - except (pickle.UnpicklingError, Exception) as ex: - LOG.error(f"Failed to UnPickle {self._save_filename()}") - LOG.error(ex) - self.data = {} + if not CONF.enable_save: + return + if os.path.exists(self._save_filename()): + try: + with open(self._save_filename(), "rb") as fp: + raw = pickle.load(fp) + if raw: + self.data = raw + LOG.debug( + f"{self.__class__.__name__}::Loaded {len(self)} entries from disk.", + ) + LOG.debug(f"{self.data}") + except (pickle.UnpicklingError, Exception) as ex: + LOG.error(f"Failed to UnPickle {self._save_filename()}") + LOG.error(ex) + self.data = {} def flush(self): """Nuke the old pickle file that stored the old results from last aprsd run.""" - if self.is_initialized(): - if os.path.exists(self._save_filename()): - pathlib.Path(self._save_filename()).unlink() - with self.lock: - self.data = {} + if not CONF.enable_save: + return + if os.path.exists(self._save_filename()): + pathlib.Path(self._save_filename()).unlink() + with self.lock: + self.data = {} diff --git a/dev-requirements.txt b/dev-requirements.txt index 93d34eb..9d3412f 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,12 +1,12 @@ # -# This file is autogenerated by pip-compile with Python 3.9 +# This file is autogenerated by pip-compile with Python 3.10 # by the following command: # # pip-compile --annotation-style=line --resolver=backtracking dev-requirements.in # add-trailing-comma==2.4.0 # via gray alabaster==0.7.12 # via sphinx -attrs==22.1.0 # via jsonschema, pytest +attrs==22.2.0 # via jsonschema, pytest autoflake==1.5.3 # via gray babel==2.11.0 # via sphinx black==22.12.0 # via gray @@ -20,21 +20,20 @@ click==8.1.3 # via black, pip-tools colorama==0.4.6 # via tox commonmark==0.9.1 # via rich configargparse==1.5.3 # via gray -coverage[toml]==6.5.0 # via pytest-cov +coverage[toml]==7.0.1 # via pytest-cov distlib==0.3.6 # via virtualenv docutils==0.19 # via sphinx -exceptiongroup==1.0.4 # via pytest +exceptiongroup==1.1.0 # via pytest filelock==3.8.2 # via tox, virtualenv fixit==0.1.4 # via gray flake8==6.0.0 # via -r dev-requirements.in, fixit, pep8-naming gray==0.13.0 # via -r dev-requirements.in -identify==2.5.9 # via pre-commit +identify==2.5.11 # via pre-commit idna==3.4 # via requests imagesize==1.4.1 # via sphinx -importlib-metadata==5.1.0 # via sphinx importlib-resources==5.10.1 # via fixit iniconfig==1.1.1 # via pytest -isort==5.11.2 # via -r dev-requirements.in, gray +isort==5.11.4 # via -r dev-requirements.in, gray jinja2==3.1.2 # via sphinx jsonschema==4.17.3 # via fixit libcst==0.4.9 # via fixit @@ -46,8 +45,8 @@ nodeenv==1.7.0 # via pre-commit packaging==22.0 # via build, pyproject-api, pytest, sphinx, tox pathspec==0.10.3 # via black pep517==0.13.0 # via build -pep8-naming==0.13.2 # via -r dev-requirements.in -pip-tools==6.12.0 # via -r dev-requirements.in +pep8-naming==0.13.3 # via -r dev-requirements.in +pip-tools==6.12.1 # via -r dev-requirements.in platformdirs==2.6.0 # via black, tox, virtualenv pluggy==1.0.0 # via pytest, tox pre-commit==2.20.0 # via -r dev-requirements.in @@ -58,7 +57,7 @@ pyproject-api==1.2.1 # via tox pyrsistent==0.19.2 # via jsonschema pytest==7.2.0 # via -r dev-requirements.in, pytest-cov pytest-cov==4.0.0 # via -r dev-requirements.in -pytz==2022.6 # via babel +pytz==2022.7 # via babel pyupgrade==3.3.1 # via gray pyyaml==6.0 # via fixit, libcst, pre-commit requests==2.28.1 # via sphinx @@ -74,15 +73,14 @@ sphinxcontrib-serializinghtml==1.1.5 # via sphinx tokenize-rt==5.0.0 # via add-trailing-comma, pyupgrade toml==0.10.2 # via autoflake, pre-commit tomli==2.0.1 # via black, build, coverage, mypy, pep517, pyproject-api, pytest, tox -tox==4.0.9 # via -r dev-requirements.in -typing-extensions==4.4.0 # via black, libcst, mypy, typing-inspect +tox==4.0.16 # via -r dev-requirements.in +typing-extensions==4.4.0 # via libcst, mypy, typing-inspect typing-inspect==0.8.0 # via libcst unify==0.5 # via gray untokenize==0.1.1 # via unify urllib3==1.26.13 # via requests virtualenv==20.17.1 # via pre-commit, tox wheel==0.38.4 # via pip-tools -zipp==3.11.0 # via importlib-metadata, importlib-resources # The following packages are considered to be unsafe in a requirements file: # pip diff --git a/requirements.in b/requirements.in index f80c069..aec3c2b 100644 --- a/requirements.in +++ b/requirements.in @@ -29,3 +29,4 @@ user-agents pyopenssl dataclasses dacite2 +oslo.config diff --git a/requirements.txt b/requirements.txt index a0e3277..5830f74 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,15 @@ # -# This file is autogenerated by pip-compile with Python 3.9 +# This file is autogenerated by pip-compile with Python 3.10 # by the following command: # # pip-compile --annotation-style=line --resolver=backtracking requirements.in # aprslib==0.7.2 # via -r requirements.in -attrs==22.1.0 # via -r requirements.in, ax253, kiss3 +attrs==22.2.0 # via -r requirements.in, ax253, kiss3 ax253==0.1.5.post1 # via kiss3 beautifulsoup4==4.11.1 # via -r requirements.in bidict==0.22.0 # via python-socketio -bitarray==2.6.0 # via ax253, kiss3 +bitarray==2.6.1 # via ax253, kiss3 certifi==2022.12.7 # via requests cffi==1.15.1 # via cryptography charset-normalizer==2.1.1 # via requests @@ -19,6 +19,7 @@ commonmark==0.9.1 # via rich cryptography==38.0.4 # via pyopenssl dacite2==2.0.0 # via -r requirements.in dataclasses==0.6 # via -r requirements.in +debtcollector==2.5.0 # via oslo-config dnspython==2.2.1 # via eventlet eventlet==0.33.2 # via -r requirements.in flask==2.1.2 # via -r requirements.in, flask-classful, flask-httpauth, flask-socketio @@ -28,12 +29,15 @@ flask-socketio==5.3.2 # via -r requirements.in greenlet==2.0.1 # via eventlet idna==3.4 # via requests imapclient==2.3.1 # via -r requirements.in -importlib-metadata==5.1.0 # via ax253, flask, kiss3 +importlib-metadata==5.2.0 # via ax253, kiss3 itsdangerous==2.1.2 # via flask jinja2==3.1.2 # via click-completion, flask kiss3==8.0.0 # via -r requirements.in markupsafe==2.1.1 # via jinja2 -pbr==5.11.0 # via -r requirements.in +netaddr==0.8.0 # via oslo-config +oslo-config==9.0.0 # via -r requirements.in +oslo-i18n==5.1.0 # via oslo-config +pbr==5.11.0 # via -r requirements.in, oslo-i18n, stevedore pluggy==1.0.0 # via -r requirements.in pycparser==2.21 # via cffi pygments==2.13.0 # via rich @@ -42,13 +46,15 @@ pyserial==3.5 # via pyserial-asyncio pyserial-asyncio==0.6 # via kiss3 python-engineio==4.3.4 # via python-socketio python-socketio==5.7.2 # via flask-socketio -pytz==2022.6 # via -r requirements.in -pyyaml==6.0 # via -r requirements.in -requests==2.28.1 # via -r requirements.in, update-checker +pytz==2022.7 # via -r requirements.in +pyyaml==6.0 # via -r requirements.in, oslo-config +requests==2.28.1 # via -r requirements.in, oslo-config, update-checker +rfc3986==2.0.0 # via oslo-config rich==12.6.0 # via -r requirements.in shellingham==1.5.0 # via click-completion six==1.16.0 # via -r requirements.in, click-completion, eventlet, imapclient soupsieve==2.3.2.post1 # via beautifulsoup4 +stevedore==4.1.1 # via oslo-config tabulate==0.9.0 # via -r requirements.in thesmuggler==1.0.1 # via -r requirements.in ua-parser==0.16.1 # via user-agents @@ -56,5 +62,5 @@ update-checker==0.18.0 # via -r requirements.in urllib3==1.26.13 # via requests user-agents==2.2.0 # via -r requirements.in werkzeug==2.1.2 # via -r requirements.in, flask -wrapt==1.14.1 # via -r requirements.in +wrapt==1.14.1 # via -r requirements.in, debtcollector zipp==3.11.0 # via importlib-metadata diff --git a/setup.cfg b/setup.cfg index a254fed..9af2e82 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,6 +34,10 @@ packages = [entry_points] console_scripts = aprsd = aprsd.aprsd:main +oslo.config.opts = + aprsd.conf = aprsd.conf.opts:list_opts +oslo.config.opts.defaults = + aprsd.conf = aprsd.conf:set_lib_defaults [build_sphinx] source-dir = docs diff --git a/tests/test_email.py b/tests/test_email.py index 4cd1528..cbc9651 100644 --- a/tests/test_email.py +++ b/tests/test_email.py @@ -5,21 +5,20 @@ from aprsd.plugins import email class TestEmail(unittest.TestCase): def test_get_email_from_shortcut(self): - config = {"aprsd": {"email": {"shortcuts": {}}}} email_address = "something@something.com" addr = f"-{email_address}" - actual = email.get_email_from_shortcut(config, addr) + actual = email.get_email_from_shortcut(addr) self.assertEqual(addr, actual) config = {"aprsd": {"email": {"nothing": "nothing"}}} - actual = email.get_email_from_shortcut(config, addr) + actual = email.get_email_from_shortcut(addr) self.assertEqual(addr, actual) config = {"aprsd": {"email": {"shortcuts": {"not_used": "empty"}}}} - actual = email.get_email_from_shortcut(config, addr) + actual = email.get_email_from_shortcut(addr) self.assertEqual(addr, actual) config = {"aprsd": {"email": {"shortcuts": {"-wb": email_address}}}} short = "-wb" - actual = email.get_email_from_shortcut(config, short) + actual = email.get_email_from_shortcut(short) self.assertEqual(email_address, actual) diff --git a/tests/test_main.py b/tests/test_main.py index bd62b55..ab1303d 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -17,6 +17,6 @@ class TestMain(unittest.TestCase): """Test to make sure we fail.""" imap_mock.return_value = None smtp_mock.return_value = {"smaiof": "fire"} - config = mock.MagicMock() + mock.MagicMock() - email.validate_email_config(config, True) + email.validate_email_config(True) From 7ccfc253cf4d881b4888c3b4040dc9e119fb74dc Mon Sep 17 00:00:00 2001 From: Hemna Date: Tue, 27 Dec 2022 14:30:03 -0500 Subject: [PATCH 2/7] Removed references to old custom config Also updated unittests to pass. --- aprsd/cli_helper.py | 1 + aprsd/cmds/dev.py | 42 ++-- aprsd/cmds/send_message.py | 27 +- aprsd/cmds/webchat.py | 118 +++------ aprsd/config.py | 404 ------------------------------ aprsd/flask.py | 92 +++---- aprsd/logging/log.py | 21 +- aprsd/packets/core.py | 3 + aprsd/packets/watch_list.py | 3 - aprsd/plugin.py | 66 +++-- aprsd/plugin_utils.py | 5 +- aprsd/plugins/email.py | 1 - aprsd/plugins/location.py | 1 - aprsd/plugins/query.py | 1 - aprsd/plugins/version.py | 3 +- aprsd/plugins/weather.py | 27 +- aprsd/threads/tx.py | 6 +- aprsd/web/admin/static/js/main.js | 14 +- tests/cmds/test_dev.py | 53 ++-- tests/cmds/test_send_message.py | 75 ++---- tests/cmds/test_webchat.py | 87 +++---- tests/plugins/test_fortune.py | 11 +- tests/plugins/test_location.py | 26 +- tests/plugins/test_notify.py | 72 +++--- tests/plugins/test_ping.py | 9 +- tests/plugins/test_query.py | 16 +- tests/plugins/test_time.py | 7 +- tests/plugins/test_version.py | 13 +- tests/plugins/test_weather.py | 65 +++-- tests/test_email.py | 16 +- tests/test_plugin.py | 81 +++--- 31 files changed, 436 insertions(+), 930 deletions(-) delete mode 100644 aprsd/config.py diff --git a/aprsd/cli_helper.py b/aprsd/cli_helper.py index f86963a..e1c8508 100644 --- a/aprsd/cli_helper.py +++ b/aprsd/cli_helper.py @@ -6,6 +6,7 @@ import click from oslo_config import cfg import aprsd +from aprsd import conf # noqa: F401 from aprsd.logging import log from aprsd.utils import trace diff --git a/aprsd/cmds/dev.py b/aprsd/cmds/dev.py index 16d6da1..ab8d07d 100644 --- a/aprsd/cmds/dev.py +++ b/aprsd/cmds/dev.py @@ -6,13 +6,15 @@ import logging import click +from oslo_config import cfg # local imports here -from aprsd import cli_helper, client, packets, plugin, stats, utils +from aprsd import cli_helper, client, conf, packets, plugin from aprsd.aprsd import cli from aprsd.utils import trace +CONF = cfg.CONF LOG = logging.getLogger("APRSD") CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) @@ -68,23 +70,16 @@ def test_plugin( message, ): """Test an individual APRSD plugin given a python path.""" - config = ctx.obj["config"] - flat_config = utils.flatten_dict(config) - LOG.info("Using CONFIG values:") - for x in flat_config: - if "password" in x or "aprsd.web.users.admin" in x: - LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX") - else: - LOG.info(f"{x} = {flat_config[x]}") + CONF.log_opt_values(LOG, logging.DEBUG) if not aprs_login: - if not config.exists("aprs.login"): + if CONF.aprs_network.login == conf.client.DEFAULT_LOGIN: click.echo("Must set --aprs_login or APRS_LOGIN") ctx.exit(-1) return else: - fromcall = config.get("aprs.login") + fromcall = CONF.aprs_network.login else: fromcall = aprs_login @@ -97,21 +92,17 @@ def test_plugin( if type(message) is tuple: message = " ".join(message) - if config["aprsd"].get("trace", False): + if CONF.trace_enabled: trace.setup_tracing(["method", "api"]) - client.Client(config) - stats.APRSDStats(config) - packets.PacketTrack(config=config) - packets.WatchList(config=config) - packets.SeenList(config=config) + client.Client() - pm = plugin.PluginManager(config) + pm = plugin.PluginManager() if load_all: pm.setup_plugins() else: pm._init() - obj = pm._create_class(plugin_path, plugin.APRSDPluginBase, config=config) + obj = pm._create_class(plugin_path, plugin.APRSDPluginBase) if not obj: click.echo(ctx.get_help()) click.echo("") @@ -125,14 +116,13 @@ def test_plugin( ), ) pm._pluggy_pm.register(obj) - login = config["aprs"]["login"] - packet = { - "from": fromcall, "addresse": login, - "message_text": message, - "format": "message", - "msgNo": 1, - } + packet = packets.MessagePacket( + from_call=fromcall, + to_call=CONF.callsign, + msgNo=1, + message_text=message, + ) LOG.info(f"P'{plugin_path}' F'{fromcall}' C'{message}'") for x in range(number): diff --git a/aprsd/cmds/send_message.py b/aprsd/cmds/send_message.py index 9f69fc5..c830448 100644 --- a/aprsd/cmds/send_message.py +++ b/aprsd/cmds/send_message.py @@ -5,13 +5,16 @@ import time import aprslib from aprslib.exceptions import LoginError import click +from oslo_config import cfg import aprsd from aprsd import cli_helper, client, packets +from aprsd import conf # noqa : F401 from aprsd.aprsd import cli from aprsd.threads import tx +CONF = cfg.CONF LOG = logging.getLogger("APRSD") @@ -62,24 +65,24 @@ def send_message( ): """Send a message to a callsign via APRS_IS.""" global got_ack, got_response - config = ctx.obj["config"] quiet = ctx.obj["quiet"] if not aprs_login: - if not config.exists("aprs.login"): + if CONF.aprs_network.login == conf.client.DEFAULT_LOGIN: click.echo("Must set --aprs_login or APRS_LOGIN") ctx.exit(-1) return - else: - config["aprs"]["login"] = aprs_login + else: + aprs_login = CONF.aprs_network.login if not aprs_password: - if not config.exists("aprs.password"): + LOG.warning(CONF.aprs_network.password) + if not CONF.aprs_network.password: click.echo("Must set --aprs-password or APRS_PASSWORD") ctx.exit(-1) return - else: - config["aprs"]["password"] = aprs_password + else: + aprs_password = CONF.aprs_network.password LOG.info(f"APRSD LISTEN Started version: {aprsd.__version__}") if type(command) is tuple: @@ -90,9 +93,9 @@ def send_message( else: LOG.info(f"L'{aprs_login}' To'{tocallsign}' C'{command}'") - packets.PacketList(config=config) - packets.WatchList(config=config) - packets.SeenList(config=config) + packets.PacketList() + packets.WatchList() + packets.SeenList() got_ack = False got_response = False @@ -109,7 +112,7 @@ def send_message( else: got_response = True from_call = packet.from_call - our_call = config["aprsd"]["callsign"].lower() + our_call = CONF.callsign.lower() tx.send( packets.AckPacket( from_call=our_call, @@ -127,7 +130,7 @@ def send_message( sys.exit(0) try: - client.ClientFactory.setup(config) + client.ClientFactory.setup() client.factory.create().client except LoginError: sys.exit(-1) diff --git a/aprsd/cmds/webchat.py b/aprsd/cmds/webchat.py index 261ba19..ff90f3f 100644 --- a/aprsd/cmds/webchat.py +++ b/aprsd/cmds/webchat.py @@ -15,23 +15,24 @@ from flask.logging import default_handler import flask_classful from flask_httpauth import HTTPBasicAuth from flask_socketio import Namespace, SocketIO +from oslo_config import cfg from user_agents import parse as ua_parse from werkzeug.security import check_password_hash, generate_password_hash import wrapt import aprsd -from aprsd import cli_helper, client -from aprsd import config as aprsd_config -from aprsd import packets, stats, threads, utils +from aprsd import cli_helper, client, conf, packets, stats, threads, utils from aprsd.aprsd import cli from aprsd.logging import rich as aprsd_logging from aprsd.threads import rx, tx from aprsd.utils import objectstore, trace +CONF = cfg.CONF LOG = logging.getLogger("APRSD") auth = HTTPBasicAuth() users = None +socketio = None def signal_handler(sig, frame): @@ -128,16 +129,16 @@ class SentMessages(objectstore.ObjectStoreMixin): def verify_password(username, password): global users - if username in users and check_password_hash(users.get(username), password): + if username in users and check_password_hash(users[username], password): return username class WebChatProcessPacketThread(rx.APRSDProcessPacketThread): """Class that handles packets being sent to us.""" - def __init__(self, config, packet_queue, socketio): + def __init__(self, packet_queue, socketio): self.socketio = socketio self.connected = False - super().__init__(config, packet_queue) + super().__init__(packet_queue) def process_ack_packet(self, packet: packets.AckPacket): super().process_ack_packet(packet) @@ -174,21 +175,16 @@ class WebChatProcessPacketThread(rx.APRSDProcessPacketThread): class WebChatFlask(flask_classful.FlaskView): - config = None - def set_config(self, config): + def set_config(self): global users - self.config = config self.users = {} - for user in self.config["aprsd"]["web"]["users"]: - self.users[user] = generate_password_hash( - self.config["aprsd"]["web"]["users"][user], - ) - + user = CONF.admin.user + self.users[user] = generate_password_hash(CONF.admin.password) users = self.users def _get_transport(self, stats): - if self.config["aprs"].get("enabled", True): + if CONF.aprs_network.enabled: transport = "aprs-is" aprs_connection = ( "APRS-IS Server: " @@ -196,27 +192,22 @@ class WebChatFlask(flask_classful.FlaskView): ) else: # We might be connected to a KISS socket? - if client.KISSClient.is_enabled(self.config): - transport = client.KISSClient.transport(self.config) + if client.KISSClient.is_enabled(): + transport = client.KISSClient.transport() if transport == client.TRANSPORT_TCPKISS: aprs_connection = ( "TCPKISS://{}:{}".format( - self.config["kiss"]["tcp"]["host"], - self.config["kiss"]["tcp"]["port"], + CONF.kiss_tcp.host, + CONF.kiss_tcp.port, ) ) elif transport == client.TRANSPORT_SERIALKISS: # for pep8 violation - kiss_default = aprsd_config.DEFAULT_DATE_FORMAT["kiss"] - default_baudrate = kiss_default["serial"]["baudrate"] aprs_connection = ( "SerialKISS://{}@{} baud".format( - self.config["kiss"]["serial"]["device"], - self.config["kiss"]["serial"].get( - "baudrate", - default_baudrate, - ), - ) + CONF.kiss_serial.device, + CONF.kiss_serial.baudrate, + ), ) return transport, aprs_connection @@ -250,7 +241,7 @@ class WebChatFlask(flask_classful.FlaskView): html_template, initial_stats=stats, aprs_connection=aprs_connection, - callsign=self.config["aprsd"]["callsign"], + callsign=CONF.callsign, version=aprsd.__version__, ) @@ -287,14 +278,12 @@ class WebChatFlask(flask_classful.FlaskView): class SendMessageNamespace(Namespace): """Class to handle the socketio interactions.""" - _config = None got_ack = False reply_sent = False msg = None request = None def __init__(self, namespace=None, config=None): - self._config = config super().__init__(namespace) def on_connect(self): @@ -312,7 +301,7 @@ class SendMessageNamespace(Namespace): global socketio LOG.debug(f"WS: on_send {data}") self.request = data - data["from"] = self._config["aprs"]["login"] + data["from"] = CONF.callsign pkt = packets.MessagePacket( from_call=data["from"], to_call=data["to"].upper(), @@ -338,7 +327,7 @@ class SendMessageNamespace(Namespace): tx.send( packets.GPSPacket( - from_call=self._config["aprs"]["login"], + from_call=CONF.callsign, to_call="APDW16", latitude=lat, longitude=long, @@ -354,25 +343,16 @@ class SendMessageNamespace(Namespace): LOG.debug(f"WS json {data}") -def setup_logging(config, flask_app, loglevel, quiet): +def setup_logging(flask_app, loglevel, quiet): flask_log = logging.getLogger("werkzeug") flask_app.logger.removeHandler(default_handler) flask_log.removeHandler(default_handler) - log_level = aprsd_config.LOG_LEVELS[loglevel] + log_level = conf.log.LOG_LEVELS[loglevel] flask_log.setLevel(log_level) - date_format = config["aprsd"].get( - "dateformat", - aprsd_config.DEFAULT_DATE_FORMAT, - ) + date_format = CONF.logging.date_format - if not config["aprsd"]["web"].get("logging_enabled", False): - # disable web logging - flask_log.disabled = True - flask_app.logger.disabled = True - # return - - if config["aprsd"].get("rich_logging", False) and not quiet: + if CONF.logging.rich_logging and not quiet: log_format = "%(message)s" log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) rh = aprsd_logging.APRSDRichHandler( @@ -382,13 +362,10 @@ def setup_logging(config, flask_app, loglevel, quiet): rh.setFormatter(log_formatter) flask_log.addHandler(rh) - log_file = config["aprsd"].get("logfile", None) + log_file = CONF.logging.logfile if log_file: - log_format = config["aprsd"].get( - "logformat", - aprsd_config.DEFAULT_LOG_FORMAT, - ) + log_format = CONF.loging.logformat log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) fh = RotatingFileHandler( log_file, maxBytes=(10248576 * 5), @@ -399,7 +376,7 @@ def setup_logging(config, flask_app, loglevel, quiet): @trace.trace -def init_flask(config, loglevel, quiet): +def init_flask(loglevel, quiet): global socketio flask_app = flask.Flask( @@ -408,9 +385,9 @@ def init_flask(config, loglevel, quiet): static_folder="web/chat/static", template_folder="web/chat/templates", ) - setup_logging(config, flask_app, loglevel, quiet) + setup_logging(flask_app, loglevel, quiet) server = WebChatFlask() - server.set_config(config) + server.set_config() flask_app.route("/", methods=["GET"])(server.index) flask_app.route("/stats", methods=["GET"])(server.stats) # flask_app.route("/send-message", methods=["GET"])(server.send_message) @@ -427,7 +404,7 @@ def init_flask(config, loglevel, quiet): socketio.on_namespace( SendMessageNamespace( - "/sendmsg", config=config, + "/sendmsg", ), ) return socketio, flask_app @@ -457,17 +434,12 @@ def init_flask(config, loglevel, quiet): @cli_helper.process_standard_options def webchat(ctx, flush, port): """Web based HAM Radio chat program!""" - ctx.obj["config_file"] loglevel = ctx.obj["loglevel"] quiet = ctx.obj["quiet"] - config = ctx.obj["config"] signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) - if not quiet: - click.echo("Load config") - level, msg = utils._check_version() if level: LOG.warning(msg) @@ -475,19 +447,11 @@ def webchat(ctx, flush, port): LOG.info(msg) LOG.info(f"APRSD Started version: {aprsd.__version__}") - flat_config = utils.flatten_dict(config) - LOG.info("Using CONFIG values:") - for x in flat_config: - if "password" in x or "aprsd.web.users.admin" in x: - LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX") - else: - LOG.info(f"{x} = {flat_config[x]}") - - stats.APRSDStats(config) + CONF.log_opt_values(LOG, logging.DEBUG) # Initialize the client factory and create # The correct client object ready for use - client.ClientFactory.setup(config) + client.ClientFactory.setup() # Make sure we have 1 client transport enabled if not client.factory.is_client_enabled(): LOG.error("No Clients are enabled in config.") @@ -497,32 +461,30 @@ def webchat(ctx, flush, port): LOG.error("APRS client is not properly configured in config file.") sys.exit(-1) - packets.PacketList(config=config) - packets.PacketTrack(config=config) - packets.WatchList(config=config) - packets.SeenList(config=config) + packets.PacketList() + packets.PacketTrack() + packets.WatchList() + packets.SeenList() - (socketio, app) = init_flask(config, loglevel, quiet) + (socketio, app) = init_flask(loglevel, quiet) rx_thread = rx.APRSDPluginRXThread( - config=config, packet_queue=threads.packet_queue, ) rx_thread.start() process_thread = WebChatProcessPacketThread( - config=config, packet_queue=threads.packet_queue, socketio=socketio, ) process_thread.start() - keepalive = threads.KeepAliveThread(config=config) + keepalive = threads.KeepAliveThread() LOG.info("Start KeepAliveThread") keepalive.start() LOG.info("Start socketio.run()") socketio.run( app, ssl_context="adhoc", - host=config["aprsd"]["web"]["host"], + host=CONF.admin.web_ip, port=port, ) diff --git a/aprsd/config.py b/aprsd/config.py deleted file mode 100644 index ca63e34..0000000 --- a/aprsd/config.py +++ /dev/null @@ -1,404 +0,0 @@ -import collections -import logging -import os -from pathlib import Path -import sys - -import click -import yaml - -from aprsd import exception, utils - - -home = str(Path.home()) -DEFAULT_CONFIG_DIR = f"{home}/.config/aprsd/" -DEFAULT_SAVE_FILE = f"{home}/.config/aprsd/aprsd.p" -DEFAULT_CONFIG_FILE = f"{home}/.config/aprsd/aprsd.yml" - -LOG_LEVELS = { - "CRITICAL": logging.CRITICAL, - "ERROR": logging.ERROR, - "WARNING": logging.WARNING, - "INFO": logging.INFO, - "DEBUG": logging.DEBUG, -} - -DEFAULT_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p" -DEFAULT_LOG_FORMAT = ( - "[%(asctime)s] [%(threadName)-20.20s] [%(levelname)-5.5s]" - " %(message)s - [%(pathname)s:%(lineno)d]" -) - -QUEUE_DATE_FORMAT = "[%m/%d/%Y] [%I:%M:%S %p]" -QUEUE_LOG_FORMAT = ( - "%(asctime)s [%(threadName)-20.20s] [%(levelname)-5.5s]" - " %(message)s - [%(pathname)s:%(lineno)d]" -) - -CORE_MESSAGE_PLUGINS = [ - "aprsd.plugins.email.EmailPlugin", - "aprsd.plugins.fortune.FortunePlugin", - "aprsd.plugins.location.LocationPlugin", - "aprsd.plugins.ping.PingPlugin", - "aprsd.plugins.query.QueryPlugin", - "aprsd.plugins.time.TimePlugin", - "aprsd.plugins.weather.USWeatherPlugin", - "aprsd.plugins.version.VersionPlugin", -] - -CORE_NOTIFY_PLUGINS = [ - "aprsd.plugins.notify.NotifySeenPlugin", -] - -ALL_PLUGINS = [] -for i in CORE_MESSAGE_PLUGINS: - ALL_PLUGINS.append(i) -for i in CORE_NOTIFY_PLUGINS: - ALL_PLUGINS.append(i) - -# an example of what should be in the ~/.aprsd/config.yml -DEFAULT_CONFIG_DICT = { - "ham": {"callsign": "NOCALL"}, - "aprs": { - "enabled": True, - # Only used as the login for aprsis. - "login": "CALLSIGN", - "password": "00000", - "host": "rotate.aprs2.net", - "port": 14580, - }, - "kiss": { - "tcp": { - "enabled": False, - "host": "direwolf.ip.address", - "port": "8001", - }, - "serial": { - "enabled": False, - "device": "/dev/ttyS0", - "baudrate": 9600, - }, - }, - "aprsd": { - # Callsign to use for all packets to/from aprsd instance - # regardless of the client (aprsis vs kiss) - "callsign": "NOCALL", - "logfile": "/tmp/aprsd.log", - "logformat": DEFAULT_LOG_FORMAT, - "dateformat": DEFAULT_DATE_FORMAT, - "save_location": DEFAULT_CONFIG_DIR, - "rich_logging": True, - "trace": False, - "enabled_plugins": ALL_PLUGINS, - "units": "imperial", - "watch_list": { - "enabled": False, - # Who gets the alert? - "alert_callsign": "NOCALL", - # 43200 is 12 hours - "alert_time_seconds": 43200, - # How many packets to save in a ring Buffer - # for a particular callsign - "packet_keep_count": 10, - "callsigns": [], - }, - "web": { - "enabled": True, - "logging_enabled": True, - "host": "0.0.0.0", - "port": 8001, - "users": { - "admin": "password-here", - }, - }, - "email": { - "enabled": True, - "shortcuts": { - "aa": "5551239999@vtext.com", - "cl": "craiglamparter@somedomain.org", - "wb": "555309@vtext.com", - }, - "smtp": { - "login": "SMTP_USERNAME", - "password": "SMTP_PASSWORD", - "host": "smtp.gmail.com", - "port": 465, - "use_ssl": False, - "debug": False, - }, - "imap": { - "login": "IMAP_USERNAME", - "password": "IMAP_PASSWORD", - "host": "imap.gmail.com", - "port": 993, - "use_ssl": True, - "debug": False, - }, - }, - }, - "services": { - "aprs.fi": {"apiKey": "APIKEYVALUE"}, - "openweathermap": {"apiKey": "APIKEYVALUE"}, - "opencagedata": {"apiKey": "APIKEYVALUE"}, - "avwx": {"base_url": "http://host:port", "apiKey": "APIKEYVALUE"}, - }, -} - - -class Config(collections.UserDict): - def _get(self, d, keys, default=None): - """ - Example: - d = {'meta': {'status': 'OK', 'status_code': 200}} - _get(d, ['meta', 'status_code']) # => 200 - _get(d, ['garbage', 'status_code']) # => None - _get(d, ['meta', 'garbage'], default='-') # => '-' - - """ - if type(keys) is str and "." in keys: - keys = keys.split(".") - - assert type(keys) is list - if d is None: - return default - - if not keys: - return d - - if type(d) is str: - return default - - return self._get(d.get(keys[0]), keys[1:], default) - - def get(self, path, default=None): - return self._get(self.data, path, default=default) - - def exists(self, path): - """See if a conf value exists.""" - test = "-3.14TEST41.3-" - return self.get(path, default=test) != test - - def check_option(self, path, default_fail=None): - """Make sure the config option doesn't have default value.""" - if not self.exists(path): - if type(path) is list: - path = ".".join(path) - raise exception.MissingConfigOptionException(path) - - val = self.get(path) - if val == default_fail: - # We have to fail and bail if the user hasn't edited - # this config option. - raise exception.ConfigOptionBogusDefaultException( - path, default_fail, - ) - - -def add_config_comments(raw_yaml): - end_idx = utils.end_substr(raw_yaml, "ham:") - if end_idx != -1: - # lets insert a comment - raw_yaml = utils.insert_str( - raw_yaml, - "\n # Callsign that owns this instance of APRSD.", - end_idx, - ) - end_idx = utils.end_substr(raw_yaml, "aprsd:") - if end_idx != -1: - # lets insert a comment - raw_yaml = utils.insert_str( - raw_yaml, - "\n # Callsign to use for all APRSD Packets as the to/from." - "\n # regardless of client type (aprsis vs tcpkiss vs serial)", - end_idx, - ) - end_idx = utils.end_substr(raw_yaml, "aprs:") - if end_idx != -1: - # lets insert a comment - raw_yaml = utils.insert_str( - raw_yaml, - "\n # Set enabled to False if there is no internet connectivity." - "\n # This is useful for a direwolf KISS aprs connection only. " - "\n" - "\n # Get the passcode for your callsign here: " - "\n # https://apps.magicbug.co.uk/passcode", - end_idx, - ) - - end_idx = utils.end_substr(raw_yaml, "aprs.fi:") - if end_idx != -1: - # lets insert a comment - raw_yaml = utils.insert_str( - raw_yaml, - "\n # Get the apiKey from your aprs.fi account here: " - "\n # http://aprs.fi/account", - end_idx, - ) - - end_idx = utils.end_substr(raw_yaml, "opencagedata:") - if end_idx != -1: - # lets insert a comment - raw_yaml = utils.insert_str( - raw_yaml, - "\n # (Optional for TimeOpenCageDataPlugin) " - "\n # Get the apiKey from your opencagedata account here: " - "\n # https://opencagedata.com/dashboard#api-keys", - end_idx, - ) - - end_idx = utils.end_substr(raw_yaml, "openweathermap:") - if end_idx != -1: - # lets insert a comment - raw_yaml = utils.insert_str( - raw_yaml, - "\n # (Optional for OWMWeatherPlugin) " - "\n # Get the apiKey from your " - "\n # openweathermap account here: " - "\n # https://home.openweathermap.org/api_keys", - end_idx, - ) - - end_idx = utils.end_substr(raw_yaml, "avwx:") - if end_idx != -1: - # lets insert a comment - raw_yaml = utils.insert_str( - raw_yaml, - "\n # (Optional for AVWXWeatherPlugin) " - "\n # Use hosted avwx-api here: https://avwx.rest " - "\n # or deploy your own from here: " - "\n # https://github.com/avwx-rest/avwx-api", - end_idx, - ) - - return raw_yaml - - -def dump_default_cfg(): - return add_config_comments( - yaml.dump( - DEFAULT_CONFIG_DICT, - indent=4, - ), - ) - - -def create_default_config(): - """Create a default config file.""" - # make sure the directory location exists - config_file_expanded = os.path.expanduser(DEFAULT_CONFIG_FILE) - config_dir = os.path.dirname(config_file_expanded) - if not os.path.exists(config_dir): - click.echo(f"Config dir '{config_dir}' doesn't exist, creating.") - utils.mkdir_p(config_dir) - with open(config_file_expanded, "w+") as cf: - cf.write(dump_default_cfg()) - - -def get_config(config_file): - """This tries to read the yaml config from .""" - config_file_expanded = os.path.expanduser(config_file) - if os.path.exists(config_file_expanded): - with open(config_file_expanded) as stream: - config = yaml.load(stream, Loader=yaml.FullLoader) - return Config(config) - else: - if config_file == DEFAULT_CONFIG_FILE: - click.echo( - f"{config_file_expanded} is missing, creating config file", - ) - create_default_config() - msg = ( - "Default config file created at {}. Please edit with your " - "settings.".format(config_file) - ) - click.echo(msg) - else: - # The user provided a config file path different from the - # Default, so we won't try and create it, just bitch and bail. - msg = f"Custom config file '{config_file}' is missing." - click.echo(msg) - - sys.exit(-1) - - -# This method tries to parse the config yaml file -# and consume the settings. -# If the required params don't exist, -# it will look in the environment -def parse_config(config_file): - config = get_config(config_file) - - def fail(msg): - click.echo(msg) - sys.exit(-1) - - def check_option(config, path, default_fail=None): - try: - config.check_option(path, default_fail=default_fail) - except Exception as ex: - fail(repr(ex)) - else: - return config - - # special check here to make sure user has edited the config file - # and changed the ham callsign - check_option( - config, - "ham.callsign", - default_fail=DEFAULT_CONFIG_DICT["ham"]["callsign"], - ) - check_option( - config, - ["aprsd"], - ) - check_option( - config, - "aprsd.callsign", - default_fail=DEFAULT_CONFIG_DICT["aprsd"]["callsign"], - ) - - # Ensure they change the admin password - if config.get("aprsd.web.enabled") is True: - check_option( - config, - ["aprsd", "web", "users", "admin"], - default_fail=DEFAULT_CONFIG_DICT["aprsd"]["web"]["users"]["admin"], - ) - - if config.get("aprsd.watch_list.enabled") is True: - check_option( - config, - ["aprsd", "watch_list", "alert_callsign"], - default_fail=DEFAULT_CONFIG_DICT["aprsd"]["watch_list"]["alert_callsign"], - ) - - if config.get("aprsd.email.enabled") is True: - # Check IMAP server settings - check_option(config, ["aprsd", "email", "imap", "host"]) - check_option(config, ["aprsd", "email", "imap", "port"]) - check_option( - config, - ["aprsd", "email", "imap", "login"], - default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["login"], - ) - check_option( - config, - ["aprsd", "email", "imap", "password"], - default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["password"], - ) - - # Check SMTP server settings - check_option(config, ["aprsd", "email", "smtp", "host"]) - check_option(config, ["aprsd", "email", "smtp", "port"]) - check_option( - config, - ["aprsd", "email", "smtp", "login"], - default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["login"], - ) - check_option( - config, - ["aprsd", "email", "smtp", "password"], - default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["password"], - ) - - return config diff --git a/aprsd/flask.py b/aprsd/flask.py index 3f374a3..9b1a302 100644 --- a/aprsd/flask.py +++ b/aprsd/flask.py @@ -13,19 +13,19 @@ from flask.logging import default_handler import flask_classful from flask_httpauth import HTTPBasicAuth from flask_socketio import Namespace, SocketIO +from oslo_config import cfg from werkzeug.security import check_password_hash, generate_password_hash import wrapt import aprsd -from aprsd import client -from aprsd import config as aprsd_config -from aprsd import packets, plugin, stats, threads, utils +from aprsd import client, conf, packets, plugin, stats, threads, utils from aprsd.clients import aprsis from aprsd.logging import log from aprsd.logging import rich as aprsd_logging from aprsd.threads import tx +CONF = cfg.CONF LOG = logging.getLogger("APRSD") auth = HTTPBasicAuth() @@ -117,8 +117,7 @@ class SendMessageThread(threads.APRSDRXThread): got_ack = False got_reply = False - def __init__(self, config, info, packet, namespace): - self.config = config + def __init__(self, info, packet, namespace): self.request = info self.packet = packet self.namespace = namespace @@ -133,8 +132,8 @@ class SendMessageThread(threads.APRSDRXThread): def setup_connection(self): user = self.request["from"] password = self.request["password"] - host = self.config["aprs"].get("host", "rotate.aprs.net") - port = self.config["aprs"].get("port", 14580) + host = CONF.aprs_network.host + port = CONF.aprs_network.port connected = False backoff = 1 while not connected: @@ -281,17 +280,12 @@ class SendMessageThread(threads.APRSDRXThread): class APRSDFlask(flask_classful.FlaskView): - config = None - def set_config(self, config): + def set_config(self): global users - self.config = config self.users = {} - for user in self.config["aprsd"]["web"]["users"]: - self.users[user] = generate_password_hash( - self.config["aprsd"]["web"]["users"][user], - ) - + user = CONF.admin.user + self.users[user] = generate_password_hash(CONF.admin.password) users = self.users @auth.login_required @@ -299,7 +293,7 @@ class APRSDFlask(flask_classful.FlaskView): stats = self._stats() LOG.debug( "watch list? {}".format( - self.config["aprsd"]["watch_list"], + CONF.watch_list.callsigns, ), ) wl = packets.WatchList() @@ -317,7 +311,7 @@ class APRSDFlask(flask_classful.FlaskView): plugins = pm.get_plugins() plugin_count = len(plugins) - if self.config["aprs"].get("enabled", True): + if CONF.aprs_network.enabled: transport = "aprs-is" aprs_connection = ( "APRS-IS Server: " @@ -325,33 +319,34 @@ class APRSDFlask(flask_classful.FlaskView): ) else: # We might be connected to a KISS socket? - if client.KISSClient.kiss_enabled(self.config): - transport = client.KISSClient.transport(self.config) + if client.KISSClient.kiss_enabled(): + transport = client.KISSClient.transport() if transport == client.TRANSPORT_TCPKISS: aprs_connection = ( "TCPKISS://{}:{}".format( - self.config["kiss"]["tcp"]["host"], - self.config["kiss"]["tcp"]["port"], + CONF.kiss_tcp.host, + CONF.kiss_tcp.port, ) ) elif transport == client.TRANSPORT_SERIALKISS: aprs_connection = ( "SerialKISS://{}@{} baud".format( - self.config["kiss"]["serial"]["device"], - self.config["kiss"]["serial"]["baudrate"], + CONF.kiss_serial.device, + CONF.kiss_serial.baudrate, ) ) stats["transport"] = transport stats["aprs_connection"] = aprs_connection + entries = conf.conf_to_dict() return flask.render_template( "index.html", initial_stats=stats, aprs_connection=aprs_connection, - callsign=self.config["aprs"]["login"], + callsign=CONF.callsign, version=aprsd.__version__, - config_json=json.dumps(self.config.data), + config_json=json.dumps(entries), watch_count=watch_count, watch_age=watch_age, seen_count=seen_count, @@ -381,7 +376,7 @@ class APRSDFlask(flask_classful.FlaskView): if request.method == "GET": return flask.render_template( "send-message.html", - callsign=self.config["aprs"]["login"], + callsign=CONF.callsign, version=aprsd.__version__, ) @@ -392,7 +387,6 @@ class APRSDFlask(flask_classful.FlaskView): for pkt in packet_list: tmp_list.append(pkt.json) - LOG.info(f"PACKETS {tmp_list}") return json.dumps(tmp_list) @auth.login_required @@ -453,14 +447,12 @@ class APRSDFlask(flask_classful.FlaskView): class SendMessageNamespace(Namespace): - _config = None got_ack = False reply_sent = False packet = None request = None - def __init__(self, namespace=None, config=None): - self._config = config + def __init__(self, namespace=None): super().__init__(namespace) def on_connect(self): @@ -492,13 +484,13 @@ class SendMessageNamespace(Namespace): ) socketio.start_background_task( - self._start, self._config, data, + self._start, data, self.packet, self, ) LOG.warning("WS: on_send: exit") - def _start(self, config, data, packet, namespace): - msg_thread = SendMessageThread(self._config, data, packet, self) + def _start(self, data, packet, namespace): + msg_thread = SendMessageThread(data, packet, self) msg_thread.start() def handle_message(self, data): @@ -566,25 +558,18 @@ class LoggingNamespace(Namespace): self.log_thread.stop() -def setup_logging(config, flask_app, loglevel, quiet): +def setup_logging(flask_app, loglevel, quiet): flask_log = logging.getLogger("werkzeug") flask_app.logger.removeHandler(default_handler) flask_log.removeHandler(default_handler) - log_level = aprsd_config.LOG_LEVELS[loglevel] + log_level = conf.log.LOG_LEVELS[loglevel] flask_log.setLevel(log_level) - date_format = config["aprsd"].get( - "dateformat", - aprsd_config.DEFAULT_DATE_FORMAT, - ) + date_format = CONF.logging.date_format + flask_log.disabled = True + flask_app.logger.disabled = True - if not config["aprsd"]["web"].get("logging_enabled", False): - # disable web logging - flask_log.disabled = True - flask_app.logger.disabled = True - return - - if config["aprsd"].get("rich_logging", False) and not quiet: + if CONF.logging.rich_logging: log_format = "%(message)s" log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) rh = aprsd_logging.APRSDRichHandler( @@ -594,13 +579,10 @@ def setup_logging(config, flask_app, loglevel, quiet): rh.setFormatter(log_formatter) flask_log.addHandler(rh) - log_file = config["aprsd"].get("logfile", None) + log_file = CONF.logging.logfile if log_file: - log_format = config["aprsd"].get( - "logformat", - aprsd_config.DEFAULT_LOG_FORMAT, - ) + log_format = CONF.logging.logformat log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) fh = RotatingFileHandler( log_file, maxBytes=(10248576 * 5), @@ -610,7 +592,7 @@ def setup_logging(config, flask_app, loglevel, quiet): flask_log.addHandler(fh) -def init_flask(config, loglevel, quiet): +def init_flask(loglevel, quiet): global socketio flask_app = flask.Flask( @@ -619,9 +601,9 @@ def init_flask(config, loglevel, quiet): static_folder="web/admin/static", template_folder="web/admin/templates", ) - setup_logging(config, flask_app, loglevel, quiet) + setup_logging(flask_app, loglevel, quiet) server = APRSDFlask() - server.set_config(config) + server.set_config() flask_app.route("/", methods=["GET"])(server.index) flask_app.route("/stats", methods=["GET"])(server.stats) flask_app.route("/messages", methods=["GET"])(server.messages) @@ -638,6 +620,6 @@ def init_flask(config, loglevel, quiet): # import eventlet # eventlet.monkey_patch() - socketio.on_namespace(SendMessageNamespace("/sendmsg", config=config)) + socketio.on_namespace(SendMessageNamespace("/sendmsg")) socketio.on_namespace(LoggingNamespace("/logs")) return socketio, flask_app diff --git a/aprsd/logging/log.py b/aprsd/logging/log.py index a148756..cb7066d 100644 --- a/aprsd/logging/log.py +++ b/aprsd/logging/log.py @@ -6,7 +6,7 @@ import sys from oslo_config import cfg -from aprsd import config as aprsd_config +from aprsd import conf from aprsd.logging import rich as aprsd_logging @@ -19,9 +19,9 @@ logging_queue = queue.Queue() # to disable logging to stdout, but still log to file # use the --quiet option on the cmdln def setup_logging(loglevel, quiet): - log_level = aprsd_config.LOG_LEVELS[loglevel] + log_level = conf.log.LOG_LEVELS[loglevel] LOG.setLevel(log_level) - date_format = CONF.logging.get("date_format", aprsd_config.DEFAULT_DATE_FORMAT) + date_format = CONF.logging.date_format rh = None fh = None @@ -51,16 +51,15 @@ def setup_logging(loglevel, quiet): imap_logger = logging.getLogger("imapclient.imaplib") imap_logger.setLevel(log_level) if rh: - imap_logger.addHandler(rh) + imap_logger.addHandler(rh) if fh: imap_logger.addHandler(fh) - - if CONF.admin.get("web_enabled", default=False): + if CONF.admin.web_enabled: qh = logging.handlers.QueueHandler(logging_queue) q_log_formatter = logging.Formatter( - fmt=aprsd_config.QUEUE_LOG_FORMAT, - datefmt=aprsd_config.QUEUE_DATE_FORMAT, + fmt=CONF.logging.logformat, + datefmt=CONF.logging.date_format, ) qh.setFormatter(q_log_formatter) LOG.addHandler(qh) @@ -74,10 +73,10 @@ def setup_logging(loglevel, quiet): def setup_logging_no_config(loglevel, quiet): - log_level = aprsd_config.LOG_LEVELS[loglevel] + log_level = conf.log.LOG_LEVELS[loglevel] LOG.setLevel(log_level) - log_format = aprsd_config.DEFAULT_LOG_FORMAT - date_format = aprsd_config.DEFAULT_DATE_FORMAT + log_format = CONF.logging.logformat + date_format = CONF.logging.date_format log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) fh = NullHandler() diff --git a/aprsd/packets/core.py b/aprsd/packets/core.py index 70a3abd..e364e31 100644 --- a/aprsd/packets/core.py +++ b/aprsd/packets/core.py @@ -84,6 +84,9 @@ class Packet(metaclass=abc.ABCMeta): else: return default + def update_timestamp(self): + self.timestamp = _int_timestamp() + def prepare(self): """Do stuff here that is needed prior to sending over the air.""" # now build the raw message for sending diff --git a/aprsd/packets/watch_list.py b/aprsd/packets/watch_list.py index 1a47d91..dee0631 100644 --- a/aprsd/packets/watch_list.py +++ b/aprsd/packets/watch_list.py @@ -30,9 +30,6 @@ class WatchList(objectstore.ObjectStoreMixin): def __init__(self, config=None): ring_size = CONF.watch_list.packet_keep_count - if not self.is_enabled(): - LOG.info("Watch List is disabled.") - if CONF.watch_list.callsigns: for callsign in CONF.watch_list.callsigns: call = callsign.replace("*", "") diff --git a/aprsd/plugin.py b/aprsd/plugin.py index aeb45ef..6181626 100644 --- a/aprsd/plugin.py +++ b/aprsd/plugin.py @@ -59,8 +59,7 @@ class APRSDPluginBase(metaclass=abc.ABCMeta): # Set this in setup() enabled = False - def __init__(self, config): - self.config = config + def __init__(self): self.message_counter = 0 self.setup() self.threads = self.create_threads() or [] @@ -142,15 +141,10 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta): def setup(self): # if we have a watch list enabled, we need to add filtering # to enable seeing packets from the watch list. - if "watch_list" in self.config["aprsd"] and self.config["aprsd"][ - "watch_list" - ].get("enabled", False): + if CONF.watch_list.enabled: # watch list is enabled self.enabled = True - watch_list = self.config["aprsd"]["watch_list"].get( - "callsigns", - [], - ) + watch_list = CONF.watch_list.callsigns # make sure the timeout is set or this doesn't work if watch_list: aprs_client = client.factory.create().client @@ -214,39 +208,39 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta): @hookimpl def filter(self, packet: packets.core.MessagePacket): if not self.enabled: - LOG.info(f"{self.__class__.__name__} is not enabled.") - return None + result = f"{self.__class__.__name__} isn't enabled" + LOG.warning(result) + return result + + if not isinstance(packet, packets.core.MessagePacket): + LOG.warning(f"Got a {packet.__class__.__name__} ignoring") + return packets.NULL_MESSAGE result = None - message = packet.get("message_text", None) - msg_format = packet.get("format", None) - tocall = packet.get("addresse", None) + message = packet.message_text + tocall = packet.to_call # Only process messages destined for us # and is an APRS message format and has a message. if ( tocall == CONF.callsign - and msg_format == "message" + and isinstance(packet, packets.core.MessagePacket) and message ): if re.search(self.command_regex, message): self.rx_inc() - if self.enabled: - try: - result = self.process(packet) - except Exception as ex: - LOG.error( - "Plugin {} failed to process packet {}".format( - self.__class__, ex, - ), - ) - LOG.exception(ex) - if result: - self.tx_inc() - else: - result = f"{self.__class__.__name__} isn't enabled" - LOG.warning(result) + try: + result = self.process(packet) + except Exception as ex: + LOG.error( + "Plugin {} failed to process packet {}".format( + self.__class__, ex, + ), + ) + LOG.exception(ex) + if result: + self.tx_inc() return result @@ -376,12 +370,17 @@ class PluginManager: :param kwargs: parameters to pass :return: """ - module_name, class_name = module_class_string.rsplit(".", 1) + module_name = None + class_name = None try: + module_name, class_name = module_class_string.rsplit(".", 1) module = importlib.import_module(module_name) module = importlib.reload(module) except Exception as ex: - LOG.error(f"Failed to load Plugin '{module_name}' : '{ex}'") + if not module_name: + LOG.error(f"Failed to load Plugin {module_class_string}") + else: + LOG.error(f"Failed to load Plugin '{module_name}' : '{ex}'") return assert hasattr(module, class_name), "class {} is not in {}".format( @@ -411,7 +410,6 @@ class PluginManager: plugin_obj = self._create_class( plugin_name, APRSDPluginBase, - config=self.config, ) if plugin_obj: if isinstance(plugin_obj, APRSDWatchListPluginBase): @@ -452,7 +450,7 @@ class PluginManager: LOG.info("Loading APRSD Plugins") self._init() # Help plugin is always enabled. - _help = HelpPlugin(self.config) + _help = HelpPlugin() self._pluggy_pm.register(_help) enabled_plugins = CONF.enabled_plugins diff --git a/aprsd/plugin_utils.py b/aprsd/plugin_utils.py index 750e73a..5b8234f 100644 --- a/aprsd/plugin_utils.py +++ b/aprsd/plugin_utils.py @@ -25,6 +25,7 @@ def get_aprs_fi(api_key, callsign): def get_weather_gov_for_gps(lat, lon): + # FIXME(hemna) This is currently BROKEN LOG.debug(f"Fetch station at {lat}, {lon}") headers = requests.utils.default_headers() headers.update( @@ -32,8 +33,8 @@ def get_weather_gov_for_gps(lat, lon): ) try: url2 = ( - #"https://forecast.weather.gov/MapClick.php?lat=%s" - #"&lon=%s&FcstType=json" % (lat, lon) + # "https://forecast.weather.gov/MapClick.php?lat=%s" + # "&lon=%s&FcstType=json" % (lat, lon) f"https://api.weather.gov/points/{lat},{lon}" ) LOG.debug(f"Fetching weather '{url2}'") diff --git a/aprsd/plugins/email.py b/aprsd/plugins/email.py index b7748ab..876cdbd 100644 --- a/aprsd/plugins/email.py +++ b/aprsd/plugins/email.py @@ -272,7 +272,6 @@ def _build_shortcuts_dict(): else: shortcuts_dict = {} - LOG.info(f"Shortcuts Dict {shortcuts_dict}") return shortcuts_dict diff --git a/aprsd/plugins/location.py b/aprsd/plugins/location.py index 84c21c5..05b7f11 100644 --- a/aprsd/plugins/location.py +++ b/aprsd/plugins/location.py @@ -27,7 +27,6 @@ class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin): LOG.info("Location Plugin") fromcall = packet.from_call message = packet.get("message_text", None) - # ack = packet.get("msgNo", "0") api_key = CONF.aprs_fi.apiKey diff --git a/aprsd/plugins/query.py b/aprsd/plugins/query.py index 2d6d5ec..871b249 100644 --- a/aprsd/plugins/query.py +++ b/aprsd/plugins/query.py @@ -33,7 +33,6 @@ class QueryPlugin(plugin.APRSDRegexCommandPluginBase): fromcall = packet.from_call message = packet.get("message_text", None) - # ack = packet.get("msgNo", "0") pkt_tracker = tracker.PacketTrack() now = datetime.datetime.now() diff --git a/aprsd/plugins/version.py b/aprsd/plugins/version.py index 80ce257..e9351d9 100644 --- a/aprsd/plugins/version.py +++ b/aprsd/plugins/version.py @@ -2,7 +2,6 @@ import logging import aprsd from aprsd import plugin, stats -from aprsd.utils import trace LOG = logging.getLogger("APRSD") @@ -19,7 +18,6 @@ class VersionPlugin(plugin.APRSDRegexCommandPluginBase): # five mins {int:int} email_sent_dict = {} - @trace.trace def process(self, packet): LOG.info("Version COMMAND") # fromcall = packet.get("from") @@ -27,6 +25,7 @@ class VersionPlugin(plugin.APRSDRegexCommandPluginBase): # ack = packet.get("msgNo", "0") stats_obj = stats.APRSDStats() s = stats_obj.stats() + print(s) return "APRSD ver:{} uptime:{}".format( aprsd.__version__, s["aprsd"]["uptime"], diff --git a/aprsd/plugins/weather.py b/aprsd/plugins/weather.py index b14ac98..16a4330 100644 --- a/aprsd/plugins/weather.py +++ b/aprsd/plugins/weather.py @@ -62,21 +62,17 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin) LOG.info(f"WX data {wx_data}") - if wx_data["success"] == False: - # Failed to fetch the weather - reply = "Failed to fetch weather for location" - else: - reply = ( - "%sF(%sF/%sF) %s. %s, %s." - % ( - wx_data["currentobservation"]["Temp"], - wx_data["data"]["temperature"][0], - wx_data["data"]["temperature"][1], - wx_data["data"]["weather"][0], - wx_data["time"]["startPeriodName"][1], - wx_data["data"]["weather"][1], - ) - ).rstrip() + reply = ( + "%sF(%sF/%sF) %s. %s, %s." + % ( + wx_data["currentobservation"]["Temp"], + wx_data["data"]["temperature"][0], + wx_data["data"]["temperature"][1], + wx_data["data"]["weather"][0], + wx_data["time"]["startPeriodName"][1], + wx_data["data"]["weather"][1], + ) + ).rstrip() LOG.debug(f"reply: '{reply}' ") return reply @@ -105,6 +101,7 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin): @trace.trace def process(self, packet): + print("FISTY") fromcall = packet.get("from") message = packet.get("message_text", None) # ack = packet.get("msgNo", "0") diff --git a/aprsd/threads/tx.py b/aprsd/threads/tx.py index 1902ad0..23f8f3f 100644 --- a/aprsd/threads/tx.py +++ b/aprsd/threads/tx.py @@ -4,7 +4,7 @@ import time from aprsd import client from aprsd import threads as aprsd_threads -from aprsd.packets import core, packet_list, tracker +from aprsd.packets import core, tracker LOG = logging.getLogger("APRSD") @@ -27,9 +27,9 @@ def send(packet: core.Packet, direct=False, aprs_client=None): else: cl = client.factory.create() + packet.update_timestamp() packet.log(header="TX") cl.send(packet) - packet_list.PacketList().tx(packet) class SendPacketThread(aprsd_threads.APRSDThread): @@ -94,8 +94,8 @@ class SendPacketThread(aprsd_threads.APRSDThread): if send_now: # no attempt time, so lets send it, and start # tracking the time. - send(packet, direct=True) packet.last_send_time = datetime.datetime.now() + send(packet, direct=True) packet.send_count += 1 time.sleep(1) diff --git a/aprsd/web/admin/static/js/main.js b/aprsd/web/admin/static/js/main.js index 491b894..f02ab55 100644 --- a/aprsd/web/admin/static/js/main.js +++ b/aprsd/web/admin/static/js/main.js @@ -107,18 +107,24 @@ function update_packets( data ) { if (size_dict(packet_list) == 0 && size_dict(data) > 0) { packetsdiv.html('') } + console.log("PACKET_LIST") + console.log(packet_list); jQuery.each(data, function(i, val) { pkt = JSON.parse(val); + console.log("PACKET"); + console.log(pkt); + console.log(pkt.timestamp); + update_watchlist_from_packet(pkt['from_call'], pkt); - if ( packet_list.hasOwnProperty(val["timestamp"]) == false ) { + if ( packet_list.hasOwnProperty(pkt.timestamp) == false ) { // Store the packet - packet_list[pkt["timestamp"]] = pkt; + packet_list[pkt.timestamp] = pkt; //ts_str = val["timestamp"].toString(); //ts = ts_str.split(".")[0]*1000; - ts = pkt["timestamp"] + ts = pkt.timestamp var d = new Date(ts).toLocaleDateString("en-US"); var t = new Date(ts).toLocaleTimeString("en-US"); - var from_call = pkt['from_call']; + var from_call = pkt.from_call; if (from_call == our_callsign) { title_id = 'title_tx'; } else { diff --git a/tests/cmds/test_dev.py b/tests/cmds/test_dev.py index c1f2933..aaaee26 100644 --- a/tests/cmds/test_dev.py +++ b/tests/cmds/test_dev.py @@ -3,62 +3,39 @@ import unittest from unittest import mock from click.testing import CliRunner +from oslo_config import cfg -from aprsd import config as aprsd_config +from aprsd import conf # noqa: F401 from aprsd.aprsd import cli from aprsd.cmds import dev # noqa +from .. import fake + +CONF = cfg.CONF F = t.TypeVar("F", bound=t.Callable[..., t.Any]) class TestDevTestPluginCommand(unittest.TestCase): - def _build_config(self, login=None, password=None): - config = { - "aprs": {}, - "aprsd": { - "trace": False, - "watch_list": {}, - }, - } + def config_and_init(self, login=None, password=None): + CONF.callsign = fake.FAKE_TO_CALLSIGN + CONF.trace_enabled = False + CONF.watch_list.packet_keep_count = 1 if login: - config["aprs"]["login"] = login - + CONF.aprs_network.login = login if password: - config["aprs"]["password"] = password + CONF.aprs_network.password = password - return aprsd_config.Config(config) + CONF.admin.user = "admin" + CONF.admin.password = "password" - @mock.patch("aprsd.config.parse_config") @mock.patch("aprsd.logging.log.setup_logging") - def test_no_login(self, mock_logging, mock_parse_config): + def test_no_plugin_arg(self, mock_logging): """Make sure we get an error if there is no login and config.""" runner = CliRunner() - mock_parse_config.return_value = self._build_config() - - result = runner.invoke( - cli, [ - "dev", "test-plugin", - "-p", "aprsd.plugins.version.VersionPlugin", - "bogus command", - ], - catch_exceptions=False, - ) - # rich.print(f"EXIT CODE {result.exit_code}") - # rich.print(f"Exception {result.exception}") - # rich.print(f"OUTPUT {result.output}") - assert result.exit_code == -1 - assert "Must set --aprs_login or APRS_LOGIN" in result.output - - @mock.patch("aprsd.config.parse_config") - @mock.patch("aprsd.logging.log.setup_logging") - def test_no_plugin_arg(self, mock_logging, mock_parse_config): - """Make sure we get an error if there is no login and config.""" - - runner = CliRunner() - mock_parse_config.return_value = self._build_config(login="something") + self.config_and_init(login="something") result = runner.invoke( cli, ["dev", "test-plugin", "bogus command"], diff --git a/tests/cmds/test_send_message.py b/tests/cmds/test_send_message.py index 4e3c157..fbeef57 100644 --- a/tests/cmds/test_send_message.py +++ b/tests/cmds/test_send_message.py @@ -3,78 +3,42 @@ import unittest from unittest import mock from click.testing import CliRunner +from oslo_config import cfg -from aprsd import config as aprsd_config +from aprsd import conf # noqa : F401 from aprsd.aprsd import cli from aprsd.cmds import send_message # noqa +from .. import fake + +CONF = cfg.CONF F = t.TypeVar("F", bound=t.Callable[..., t.Any]) class TestSendMessageCommand(unittest.TestCase): - def _build_config(self, login=None, password=None): - config = { - "aprs": {}, - "aprsd": { - "trace": False, - "watch_list": {}, - }, - } + def config_and_init(self, login=None, password=None): + CONF.callsign = fake.FAKE_TO_CALLSIGN + CONF.trace_enabled = False + CONF.watch_list.packet_keep_count = 1 if login: - config["aprs"]["login"] = login - + CONF.aprs_network.login = login if password: - config["aprs"]["password"] = password + CONF.aprs_network.password = password - return aprsd_config.Config(config) + CONF.admin.user = "admin" + CONF.admin.password = "password" - @mock.patch("aprsd.config.parse_config") @mock.patch("aprsd.logging.log.setup_logging") - def test_no_login(self, mock_logging, mock_parse_config): - """Make sure we get an error if there is no login and config.""" - return - - runner = CliRunner() - mock_parse_config.return_value = self._build_config() - - result = runner.invoke( - cli, ["send-message", "WB4BOR", "wx"], - catch_exceptions=False, - ) - # rich.print(f"EXIT CODE {result.exit_code}") - # rich.print(f"Exception {result.exception}") - # rich.print(f"OUTPUT {result.output}") - assert result.exit_code == -1 - assert "Must set --aprs_login or APRS_LOGIN" in result.output - - @mock.patch("aprsd.config.parse_config") - @mock.patch("aprsd.logging.log.setup_logging") - def test_no_password(self, mock_logging, mock_parse_config): - """Make sure we get an error if there is no password and config.""" - - return - runner = CliRunner() - mock_parse_config.return_value = self._build_config(login="something") - - result = runner.invoke( - cli, ["send-message", "WB4BOR", "wx"], - catch_exceptions=False, - ) - assert result.exit_code == -1 - assert "Must set --aprs-password or APRS_PASSWORD" in result.output - - @mock.patch("aprsd.config.parse_config") - @mock.patch("aprsd.logging.log.setup_logging") - def test_no_tocallsign(self, mock_logging, mock_parse_config): + def test_no_tocallsign(self, mock_logging): """Make sure we get an error if there is no tocallsign.""" - runner = CliRunner() - mock_parse_config.return_value = self._build_config( + self.config_and_init( login="something", password="another", ) + runner = CliRunner() result = runner.invoke( cli, ["send-message"], @@ -83,16 +47,15 @@ class TestSendMessageCommand(unittest.TestCase): assert result.exit_code == 2 assert "Error: Missing argument 'TOCALLSIGN'" in result.output - @mock.patch("aprsd.config.parse_config") @mock.patch("aprsd.logging.log.setup_logging") - def test_no_command(self, mock_logging, mock_parse_config): + def test_no_command(self, mock_logging): """Make sure we get an error if there is no command.""" - runner = CliRunner() - mock_parse_config.return_value = self._build_config( + self.config_and_init( login="something", password="another", ) + runner = CliRunner() result = runner.invoke( cli, ["send-message", "WB4BOR"], diff --git a/tests/cmds/test_webchat.py b/tests/cmds/test_webchat.py index 6bd08f8..ed77fff 100644 --- a/tests/cmds/test_webchat.py +++ b/tests/cmds/test_webchat.py @@ -5,112 +5,81 @@ from unittest import mock from click.testing import CliRunner import flask import flask_socketio +from oslo_config import cfg -from aprsd import config as aprsd_config -from aprsd import packets +from aprsd import conf # noqa: F401 from aprsd.cmds import webchat # noqa from aprsd.packets import core from .. import fake +CONF = cfg.CONF F = t.TypeVar("F", bound=t.Callable[..., t.Any]) class TestSendMessageCommand(unittest.TestCase): - def _build_config(self, login=None, password=None): - config = { - "aprs": {}, - "aprsd": { - "trace": False, - "web": { - "users": {"admin": "password"}, - }, - "watch_list": {"packet_keep_count": 1}, - }, - } + def config_and_init(self, login=None, password=None): + CONF.callsign = fake.FAKE_TO_CALLSIGN + CONF.trace_enabled = False + CONF.watch_list.packet_keep_count = 1 if login: - config["aprs"]["login"] = login - + CONF.aprs_network.login = login if password: - config["aprs"]["password"] = password + CONF.aprs_network.password = password - return aprsd_config.Config(config) + CONF.admin.user = "admin" + CONF.admin.password = "password" - @mock.patch("aprsd.config.parse_config") - def test_missing_config(self, mock_parse_config): - CliRunner() - cfg = self._build_config() - del cfg["aprsd"]["web"]["users"] - mock_parse_config.return_value = cfg - - server = webchat.WebChatFlask() - self.assertRaises( - KeyError, - server.set_config, cfg, - ) - - @mock.patch("aprsd.config.parse_config") @mock.patch("aprsd.logging.log.setup_logging") - def test_init_flask(self, mock_logging, mock_parse_config): + def test_init_flask(self, mock_logging): """Make sure we get an error if there is no login and config.""" CliRunner() - cfg = self._build_config() - mock_parse_config.return_value = cfg + self.config_and_init() - socketio, flask_app = webchat.init_flask(cfg, "DEBUG", False) + socketio, flask_app = webchat.init_flask("DEBUG", False) self.assertIsInstance(socketio, flask_socketio.SocketIO) self.assertIsInstance(flask_app, flask.Flask) - @mock.patch("aprsd.config.parse_config") @mock.patch("aprsd.packets.tracker.PacketTrack.remove") - @mock.patch("aprsd.cmds.webchat.socketio.emit") + @mock.patch("aprsd.cmds.webchat.socketio") def test_process_ack_packet( - self, mock_parse_config, - mock_remove, mock_emit, + self, + mock_remove, mock_socketio, ): - config = self._build_config() - mock_parse_config.return_value = config + self.config_and_init() + mock_socketio.emit = mock.MagicMock() packet = fake.fake_packet( message="blah", msg_number=1, message_format=core.PACKET_TYPE_ACK, ) socketio = mock.MagicMock() - packets.PacketList(config=config) - packets.PacketTrack(config=config) - packets.WatchList(config=config) - packets.SeenList(config=config) - wcp = webchat.WebChatProcessPacketThread(config, packet, socketio) + wcp = webchat.WebChatProcessPacketThread(packet, socketio) wcp.process_ack_packet(packet) mock_remove.called_once() - mock_emit.called_once() + mock_socketio.called_once() - @mock.patch("aprsd.config.parse_config") @mock.patch("aprsd.packets.PacketList.rx") - @mock.patch("aprsd.cmds.webchat.socketio.emit") + @mock.patch("aprsd.cmds.webchat.socketio") def test_process_our_message_packet( - self, mock_parse_config, + self, mock_packet_add, - mock_emit, + mock_socketio, ): - config = self._build_config() - mock_parse_config.return_value = config + self.config_and_init() + mock_socketio.emit = mock.MagicMock() packet = fake.fake_packet( message="blah", msg_number=1, message_format=core.PACKET_TYPE_MESSAGE, ) socketio = mock.MagicMock() - packets.PacketList(config=config) - packets.PacketTrack(config=config) - packets.WatchList(config=config) - packets.SeenList(config=config) - wcp = webchat.WebChatProcessPacketThread(config, packet, socketio) + wcp = webchat.WebChatProcessPacketThread(packet, socketio) wcp.process_our_message_packet(packet) mock_packet_add.called_once() - mock_emit.called_once() + mock_socketio.called_once() diff --git a/tests/plugins/test_fortune.py b/tests/plugins/test_fortune.py index d0622fd..bf1c371 100644 --- a/tests/plugins/test_fortune.py +++ b/tests/plugins/test_fortune.py @@ -1,15 +1,21 @@ from unittest import mock +from oslo_config import cfg + +from aprsd import conf # noqa: F401 from aprsd.plugins import fortune as fortune_plugin from .. import fake, test_plugin +CONF = cfg.CONF + + class TestFortunePlugin(test_plugin.TestPlugin): @mock.patch("shutil.which") def test_fortune_fail(self, mock_which): mock_which.return_value = None - fortune = fortune_plugin.FortunePlugin(self.config) + fortune = fortune_plugin.FortunePlugin() expected = "FortunePlugin isn't enabled" packet = fake.fake_packet(message="fortune") actual = fortune.filter(packet) @@ -20,7 +26,8 @@ class TestFortunePlugin(test_plugin.TestPlugin): def test_fortune_success(self, mock_which, mock_output): mock_which.return_value = "/usr/bin/games/fortune" mock_output.return_value = "Funny fortune" - fortune = fortune_plugin.FortunePlugin(self.config) + CONF.callsign = fake.FAKE_TO_CALLSIGN + fortune = fortune_plugin.FortunePlugin() expected = "Funny fortune" packet = fake.fake_packet(message="fortune") diff --git a/tests/plugins/test_location.py b/tests/plugins/test_location.py index ae5893b..b71e54b 100644 --- a/tests/plugins/test_location.py +++ b/tests/plugins/test_location.py @@ -1,18 +1,24 @@ from unittest import mock +from oslo_config import cfg + +from aprsd import conf # noqa: F401 from aprsd.plugins import location as location_plugin from .. import fake, test_plugin +CONF = cfg.CONF + + class TestLocationPlugin(test_plugin.TestPlugin): - @mock.patch("aprsd.config.Config.check_option") - def test_location_not_enabled_missing_aprs_fi_key(self, mock_check): + def test_location_not_enabled_missing_aprs_fi_key(self): # When the aprs.fi api key isn't set, then # the LocationPlugin will be disabled. - mock_check.side_effect = Exception - fortune = location_plugin.LocationPlugin(self.config) + CONF.callsign = fake.FAKE_TO_CALLSIGN + CONF.aprs_fi.apiKey = None + fortune = location_plugin.LocationPlugin() expected = "LocationPlugin isn't enabled" packet = fake.fake_packet(message="location") actual = fortune.filter(packet) @@ -23,7 +29,8 @@ class TestLocationPlugin(test_plugin.TestPlugin): # When the aprs.fi api key isn't set, then # the LocationPlugin will be disabled. mock_check.side_effect = Exception - fortune = location_plugin.LocationPlugin(self.config) + CONF.callsign = fake.FAKE_TO_CALLSIGN + fortune = location_plugin.LocationPlugin() expected = "Failed to fetch aprs.fi location" packet = fake.fake_packet(message="location") actual = fortune.filter(packet) @@ -34,7 +41,8 @@ class TestLocationPlugin(test_plugin.TestPlugin): # When the aprs.fi api key isn't set, then # the LocationPlugin will be disabled. mock_check.return_value = {"entries": []} - fortune = location_plugin.LocationPlugin(self.config) + CONF.callsign = fake.FAKE_TO_CALLSIGN + fortune = location_plugin.LocationPlugin() expected = "Failed to fetch aprs.fi location" packet = fake.fake_packet(message="location") actual = fortune.filter(packet) @@ -57,7 +65,8 @@ class TestLocationPlugin(test_plugin.TestPlugin): } mock_weather.side_effect = Exception mock_time.return_value = 10 - fortune = location_plugin.LocationPlugin(self.config) + CONF.callsign = fake.FAKE_TO_CALLSIGN + fortune = location_plugin.LocationPlugin() expected = "KFAKE: Unknown Location 0' 10,11 0.0h ago" packet = fake.fake_packet(message="location") actual = fortune.filter(packet) @@ -82,7 +91,8 @@ class TestLocationPlugin(test_plugin.TestPlugin): wx_data = {"location": {"areaDescription": expected_town}} mock_weather.return_value = wx_data mock_time.return_value = 10 - fortune = location_plugin.LocationPlugin(self.config) + CONF.callsign = fake.FAKE_TO_CALLSIGN + fortune = location_plugin.LocationPlugin() expected = f"KFAKE: {expected_town} 0' 10,11 0.0h ago" packet = fake.fake_packet(message="location") actual = fortune.filter(packet) diff --git a/tests/plugins/test_notify.py b/tests/plugins/test_notify.py index 2c95e6c..1880f5d 100644 --- a/tests/plugins/test_notify.py +++ b/tests/plugins/test_notify.py @@ -1,14 +1,16 @@ from unittest import mock -from aprsd import client -from aprsd import config as aprsd_config -from aprsd import packets +from oslo_config import cfg + +from aprsd import client, packets +from aprsd import conf # noqa: F401 from aprsd.plugins import notify as notify_plugin from .. import fake, test_plugin -DEFAULT_WATCHLIST_CALLSIGNS = [fake.FAKE_FROM_CALLSIGN] +CONF = cfg.CONF +DEFAULT_WATCHLIST_CALLSIGNS = fake.FAKE_FROM_CALLSIGN class TestWatchListPlugin(test_plugin.TestPlugin): @@ -16,7 +18,7 @@ class TestWatchListPlugin(test_plugin.TestPlugin): self.fromcall = fake.FAKE_FROM_CALLSIGN self.ack = 1 - def _config( + def config_and_init( self, watchlist_enabled=True, watchlist_alert_callsign=None, @@ -24,39 +26,33 @@ class TestWatchListPlugin(test_plugin.TestPlugin): watchlist_packet_keep_count=None, watchlist_callsigns=DEFAULT_WATCHLIST_CALLSIGNS, ): - _config = aprsd_config.Config(aprsd_config.DEFAULT_CONFIG_DICT) - default_wl = aprsd_config.DEFAULT_CONFIG_DICT["aprsd"]["watch_list"] - - _config["ham"]["callsign"] = self.fromcall - _config["aprsd"]["callsign"] = self.fromcall - _config["aprs"]["login"] = self.fromcall - _config["services"]["aprs.fi"]["apiKey"] = "something" + CONF.callsign = self.fromcall + CONF.aprs_network.login = self.fromcall + CONF.aprs_fi.apiKey = "something" # Set the watchlist specific config options + CONF.watch_list.enabled = watchlist_enabled - _config["aprsd"]["watch_list"]["enabled"] = watchlist_enabled if not watchlist_alert_callsign: watchlist_alert_callsign = fake.FAKE_TO_CALLSIGN - _config["aprsd"]["watch_list"]["alert_callsign"] = watchlist_alert_callsign + CONF.watch_list.alert_callsign = watchlist_alert_callsign if not watchlist_alert_time_seconds: - watchlist_alert_time_seconds = default_wl["alert_time_seconds"] - _config["aprsd"]["watch_list"]["alert_time_seconds"] = watchlist_alert_time_seconds + watchlist_alert_time_seconds = CONF.watch_list.alert_time_seconds + CONF.watch_list.alert_time_seconds = watchlist_alert_time_seconds if not watchlist_packet_keep_count: - watchlist_packet_keep_count = default_wl["packet_keep_count"] - _config["aprsd"]["watch_list"]["packet_keep_count"] = watchlist_packet_keep_count + watchlist_packet_keep_count = CONF.watch_list.packet_keep_count + CONF.watch_list.packet_keep_count = watchlist_packet_keep_count - _config["aprsd"]["watch_list"]["callsigns"] = watchlist_callsigns - return _config + CONF.watch_list.callsigns = watchlist_callsigns class TestAPRSDWatchListPluginBase(TestWatchListPlugin): def test_watchlist_not_enabled(self): - config = self._config(watchlist_enabled=False) - self.config_and_init(config=config) - plugin = fake.FakeWatchListPlugin(self.config) + self.config_and_init(watchlist_enabled=False) + plugin = fake.FakeWatchListPlugin() packet = fake.fake_packet( message="version", @@ -69,9 +65,8 @@ class TestAPRSDWatchListPluginBase(TestWatchListPlugin): @mock.patch("aprsd.client.ClientFactory", autospec=True) def test_watchlist_not_in_watchlist(self, mock_factory): client.factory = mock_factory - config = self._config() - self.config_and_init(config=config) - plugin = fake.FakeWatchListPlugin(self.config) + self.config_and_init() + plugin = fake.FakeWatchListPlugin() packet = fake.fake_packet( fromcall="FAKE", @@ -86,9 +81,8 @@ class TestAPRSDWatchListPluginBase(TestWatchListPlugin): class TestNotifySeenPlugin(TestWatchListPlugin): def test_disabled(self): - config = self._config(watchlist_enabled=False) - self.config_and_init(config=config) - plugin = notify_plugin.NotifySeenPlugin(self.config) + self.config_and_init(watchlist_enabled=False) + plugin = notify_plugin.NotifySeenPlugin() packet = fake.fake_packet( message="version", @@ -101,9 +95,8 @@ class TestNotifySeenPlugin(TestWatchListPlugin): @mock.patch("aprsd.client.ClientFactory", autospec=True) def test_callsign_not_in_watchlist(self, mock_factory): client.factory = mock_factory - config = self._config(watchlist_enabled=False) - self.config_and_init(config=config) - plugin = notify_plugin.NotifySeenPlugin(self.config) + self.config_and_init(watchlist_enabled=False) + plugin = notify_plugin.NotifySeenPlugin() packet = fake.fake_packet( message="version", @@ -118,12 +111,11 @@ class TestNotifySeenPlugin(TestWatchListPlugin): def test_callsign_in_watchlist_not_old(self, mock_is_old, mock_factory): client.factory = mock_factory mock_is_old.return_value = False - config = self._config( + self.config_and_init( watchlist_enabled=True, watchlist_callsigns=["WB4BOR"], ) - self.config_and_init(config=config) - plugin = notify_plugin.NotifySeenPlugin(self.config) + plugin = notify_plugin.NotifySeenPlugin() packet = fake.fake_packet( fromcall="WB4BOR", @@ -139,13 +131,12 @@ class TestNotifySeenPlugin(TestWatchListPlugin): def test_callsign_in_watchlist_old_same_alert_callsign(self, mock_is_old, mock_factory): client.factory = mock_factory mock_is_old.return_value = True - config = self._config( + self.config_and_init( watchlist_enabled=True, watchlist_alert_callsign="WB4BOR", watchlist_callsigns=["WB4BOR"], ) - self.config_and_init(config=config) - plugin = notify_plugin.NotifySeenPlugin(self.config) + plugin = notify_plugin.NotifySeenPlugin() packet = fake.fake_packet( fromcall="WB4BOR", @@ -163,13 +154,12 @@ class TestNotifySeenPlugin(TestWatchListPlugin): mock_is_old.return_value = True notify_callsign = fake.FAKE_TO_CALLSIGN fromcall = "WB4BOR" - config = self._config( + self.config_and_init( watchlist_enabled=True, watchlist_alert_callsign=notify_callsign, watchlist_callsigns=["WB4BOR"], ) - self.config_and_init(config=config) - plugin = notify_plugin.NotifySeenPlugin(self.config) + plugin = notify_plugin.NotifySeenPlugin() packet = fake.fake_packet( fromcall=fromcall, diff --git a/tests/plugins/test_ping.py b/tests/plugins/test_ping.py index 22b69eb..2539e4c 100644 --- a/tests/plugins/test_ping.py +++ b/tests/plugins/test_ping.py @@ -1,10 +1,16 @@ from unittest import mock +from oslo_config import cfg + +from aprsd import conf # noqa: F401 from aprsd.plugins import ping as ping_plugin from .. import fake, test_plugin +CONF = cfg.CONF + + class TestPingPlugin(test_plugin.TestPlugin): @mock.patch("time.localtime") def test_ping(self, mock_time): @@ -14,7 +20,8 @@ class TestPingPlugin(test_plugin.TestPlugin): s = fake_time.tm_sec = 55 mock_time.return_value = fake_time - ping = ping_plugin.PingPlugin(self.config) + CONF.callsign = fake.FAKE_TO_CALLSIGN + ping = ping_plugin.PingPlugin() packet = fake.fake_packet( message="location", diff --git a/tests/plugins/test_query.py b/tests/plugins/test_query.py index f16fd8d..945d650 100644 --- a/tests/plugins/test_query.py +++ b/tests/plugins/test_query.py @@ -1,5 +1,7 @@ from unittest import mock +from oslo_config import cfg + from aprsd import packets from aprsd.packets import tracker from aprsd.plugins import query as query_plugin @@ -7,11 +9,18 @@ from aprsd.plugins import query as query_plugin from .. import fake, test_plugin +CONF = cfg.CONF + + class TestQueryPlugin(test_plugin.TestPlugin): @mock.patch("aprsd.packets.tracker.PacketTrack.flush") def test_query_flush(self, mock_flush): packet = fake.fake_packet(message="!delete") - query = query_plugin.QueryPlugin(self.config) + CONF.callsign = fake.FAKE_TO_CALLSIGN + CONF.save_enabled = True + CONF.query_plugin.callsign = fake.FAKE_FROM_CALLSIGN + query = query_plugin.QueryPlugin() + query.enabled = True expected = "Deleted ALL pending msgs." actual = query.filter(packet) @@ -20,10 +29,13 @@ class TestQueryPlugin(test_plugin.TestPlugin): @mock.patch("aprsd.packets.tracker.PacketTrack.restart_delayed") def test_query_restart_delayed(self, mock_restart): + CONF.callsign = fake.FAKE_TO_CALLSIGN + CONF.save_enabled = True + CONF.query_plugin.callsign = fake.FAKE_FROM_CALLSIGN track = tracker.PacketTrack() track.data = {} packet = fake.fake_packet(message="!4") - query = query_plugin.QueryPlugin(self.config) + query = query_plugin.QueryPlugin() expected = "No pending msgs to resend" actual = query.filter(packet) diff --git a/tests/plugins/test_time.py b/tests/plugins/test_time.py index befba45..22d2527 100644 --- a/tests/plugins/test_time.py +++ b/tests/plugins/test_time.py @@ -1,5 +1,6 @@ from unittest import mock +from oslo_config import cfg import pytz from aprsd.plugins import time as time_plugin @@ -8,6 +9,9 @@ from aprsd.utils import fuzzy from .. import fake, test_plugin +CONF = cfg.CONF + + class TestTimePlugins(test_plugin.TestPlugin): @mock.patch("aprsd.plugins.time.TimePlugin._get_local_tz") @@ -25,7 +29,8 @@ class TestTimePlugins(test_plugin.TestPlugin): h = int(local_t.strftime("%H")) m = int(local_t.strftime("%M")) fake_time.tm_sec = 13 - time = time_plugin.TimePlugin(self.config) + CONF.callsign = fake.FAKE_TO_CALLSIGN + time = time_plugin.TimePlugin() packet = fake.fake_packet( message="location", diff --git a/tests/plugins/test_version.py b/tests/plugins/test_version.py index c6e9055..32d66e2 100644 --- a/tests/plugins/test_version.py +++ b/tests/plugins/test_version.py @@ -1,4 +1,4 @@ -from unittest import mock +from oslo_config import cfg import aprsd from aprsd.plugins import version as version_plugin @@ -6,11 +6,16 @@ from aprsd.plugins import version as version_plugin from .. import fake, test_plugin +CONF = cfg.CONF + + class TestVersionPlugin(test_plugin.TestPlugin): - @mock.patch("aprsd.plugin.PluginManager.get_plugins") - def test_version(self, mock_get_plugins): + + def test_version(self): expected = f"APRSD ver:{aprsd.__version__} uptime:00:00:00" - version = version_plugin.VersionPlugin(self.config) + CONF.callsign = fake.FAKE_TO_CALLSIGN + version = version_plugin.VersionPlugin() + version.enabled = True packet = fake.fake_packet( message="No", diff --git a/tests/plugins/test_weather.py b/tests/plugins/test_weather.py index d607e0b..8a85e0b 100644 --- a/tests/plugins/test_weather.py +++ b/tests/plugins/test_weather.py @@ -1,18 +1,24 @@ from unittest import mock +from oslo_config import cfg + +from aprsd import conf # noqa: F401 from aprsd.plugins import weather as weather_plugin from .. import fake, test_plugin +CONF = cfg.CONF + + class TestUSWeatherPluginPlugin(test_plugin.TestPlugin): - @mock.patch("aprsd.config.Config.check_option") - def test_not_enabled_missing_aprs_fi_key(self, mock_check): + def test_not_enabled_missing_aprs_fi_key(self): # When the aprs.fi api key isn't set, then # the LocationPlugin will be disabled. - mock_check.side_effect = Exception - wx = weather_plugin.USWeatherPlugin(self.config) + CONF.aprs_fi.apiKey = None + CONF.callsign = fake.FAKE_TO_CALLSIGN + wx = weather_plugin.USWeatherPlugin() expected = "USWeatherPlugin isn't enabled" packet = fake.fake_packet(message="weather") actual = wx.filter(packet) @@ -23,7 +29,9 @@ class TestUSWeatherPluginPlugin(test_plugin.TestPlugin): # When the aprs.fi api key isn't set, then # the Plugin will be disabled. mock_check.side_effect = Exception - wx = weather_plugin.USWeatherPlugin(self.config) + CONF.aprs_fi.apiKey = "abc123" + CONF.callsign = fake.FAKE_TO_CALLSIGN + wx = weather_plugin.USWeatherPlugin() expected = "Failed to fetch aprs.fi location" packet = fake.fake_packet(message="weather") actual = wx.filter(packet) @@ -34,7 +42,10 @@ class TestUSWeatherPluginPlugin(test_plugin.TestPlugin): # When the aprs.fi api key isn't set, then # the Plugin will be disabled. mock_check.return_value = {"entries": []} - wx = weather_plugin.USWeatherPlugin(self.config) + CONF.aprs_fi.apiKey = "abc123" + CONF.callsign = fake.FAKE_TO_CALLSIGN + wx = weather_plugin.USWeatherPlugin() + wx.enabled = True expected = "Failed to fetch aprs.fi location" packet = fake.fake_packet(message="weather") actual = wx.filter(packet) @@ -55,7 +66,10 @@ class TestUSWeatherPluginPlugin(test_plugin.TestPlugin): ], } mock_weather.side_effect = Exception - wx = weather_plugin.USWeatherPlugin(self.config) + CONF.aprs_fi.apiKey = "abc123" + CONF.callsign = fake.FAKE_TO_CALLSIGN + wx = weather_plugin.USWeatherPlugin() + wx.enabled = True expected = "Unable to get weather" packet = fake.fake_packet(message="weather") actual = wx.filter(packet) @@ -83,7 +97,10 @@ class TestUSWeatherPluginPlugin(test_plugin.TestPlugin): }, "time": {"startPeriodName": ["ignored", "sometime"]}, } - wx = weather_plugin.USWeatherPlugin(self.config) + CONF.aprs_fi.apiKey = "abc123" + CONF.callsign = fake.FAKE_TO_CALLSIGN + wx = weather_plugin.USWeatherPlugin() + wx.enabled = True expected = "400F(10F/11F) test. sometime, another." packet = fake.fake_packet(message="weather") actual = wx.filter(packet) @@ -92,12 +109,11 @@ class TestUSWeatherPluginPlugin(test_plugin.TestPlugin): class TestUSMetarPlugin(test_plugin.TestPlugin): - @mock.patch("aprsd.config.Config.check_option") - def test_not_enabled_missing_aprs_fi_key(self, mock_check): + def test_not_enabled_missing_aprs_fi_key(self): # When the aprs.fi api key isn't set, then # the LocationPlugin will be disabled. - mock_check.side_effect = Exception - wx = weather_plugin.USMetarPlugin(self.config) + CONF.aprs_fi.apiKey = None + wx = weather_plugin.USMetarPlugin() expected = "USMetarPlugin isn't enabled" packet = fake.fake_packet(message="metar") actual = wx.filter(packet) @@ -108,7 +124,10 @@ class TestUSMetarPlugin(test_plugin.TestPlugin): # When the aprs.fi api key isn't set, then # the Plugin will be disabled. mock_check.side_effect = Exception - wx = weather_plugin.USMetarPlugin(self.config) + CONF.aprs_fi.apiKey = "abc123" + CONF.callsign = fake.FAKE_TO_CALLSIGN + wx = weather_plugin.USMetarPlugin() + wx.enabled = True expected = "Failed to fetch aprs.fi location" packet = fake.fake_packet(message="metar") actual = wx.filter(packet) @@ -119,7 +138,10 @@ class TestUSMetarPlugin(test_plugin.TestPlugin): # When the aprs.fi api key isn't set, then # the Plugin will be disabled. mock_check.return_value = {"entries": []} - wx = weather_plugin.USMetarPlugin(self.config) + CONF.aprs_fi.apiKey = "abc123" + CONF.callsign = fake.FAKE_TO_CALLSIGN + wx = weather_plugin.USMetarPlugin() + wx.enabled = True expected = "Failed to fetch aprs.fi location" packet = fake.fake_packet(message="metar") actual = wx.filter(packet) @@ -128,7 +150,10 @@ class TestUSMetarPlugin(test_plugin.TestPlugin): @mock.patch("aprsd.plugin_utils.get_weather_gov_metar") def test_gov_metar_fetch_fails(self, mock_metar): mock_metar.side_effect = Exception - wx = weather_plugin.USMetarPlugin(self.config) + CONF.aprs_fi.apiKey = "abc123" + CONF.callsign = fake.FAKE_TO_CALLSIGN + wx = weather_plugin.USMetarPlugin() + wx.enabled = True expected = "Unable to find station METAR" packet = fake.fake_packet(message="metar KPAO") actual = wx.filter(packet) @@ -141,7 +166,10 @@ class TestUSMetarPlugin(test_plugin.TestPlugin): text = '{"properties": {"rawMessage": "BOGUSMETAR"}}' mock_metar.return_value = Response() - wx = weather_plugin.USMetarPlugin(self.config) + CONF.aprs_fi.apiKey = "abc123" + CONF.callsign = fake.FAKE_TO_CALLSIGN + wx = weather_plugin.USMetarPlugin() + wx.enabled = True expected = "BOGUSMETAR" packet = fake.fake_packet(message="metar KPAO") actual = wx.filter(packet) @@ -169,7 +197,10 @@ class TestUSMetarPlugin(test_plugin.TestPlugin): } mock_metar.return_value = Response() - wx = weather_plugin.USMetarPlugin(self.config) + CONF.aprs_fi.apiKey = "abc123" + CONF.callsign = fake.FAKE_TO_CALLSIGN + wx = weather_plugin.USMetarPlugin() + wx.enabled = True expected = "BOGUSMETAR" packet = fake.fake_packet(message="metar") actual = wx.filter(packet) diff --git a/tests/test_email.py b/tests/test_email.py index cbc9651..9a752b2 100644 --- a/tests/test_email.py +++ b/tests/test_email.py @@ -1,24 +1,32 @@ import unittest +from oslo_config import cfg + +from aprsd import conf # noqa: F401 from aprsd.plugins import email +CONF = cfg.CONF + + class TestEmail(unittest.TestCase): def test_get_email_from_shortcut(self): + CONF.email_plugin.shortcuts = None email_address = "something@something.com" addr = f"-{email_address}" actual = email.get_email_from_shortcut(addr) self.assertEqual(addr, actual) - config = {"aprsd": {"email": {"nothing": "nothing"}}} + CONF.email_plugin.shortcuts = None actual = email.get_email_from_shortcut(addr) self.assertEqual(addr, actual) - config = {"aprsd": {"email": {"shortcuts": {"not_used": "empty"}}}} + CONF.email_plugin.shortcuts = None actual = email.get_email_from_shortcut(addr) self.assertEqual(addr, actual) - config = {"aprsd": {"email": {"shortcuts": {"-wb": email_address}}}} - short = "-wb" + CONF.email_plugin.email_shortcuts = ["wb=something@something.com"] + email.shortcuts_dict = None + short = "wb" actual = email.get_email_from_shortcut(short) self.assertEqual(email_address, actual) diff --git a/tests/test_plugin.py b/tests/test_plugin.py index b27ce39..e69ede3 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -1,7 +1,9 @@ import unittest from unittest import mock -from aprsd import config as aprsd_config +from oslo_config import cfg + +from aprsd import conf # noqa: F401 from aprsd import packets from aprsd import plugin as aprsd_plugin from aprsd import plugins, stats @@ -10,6 +12,9 @@ from aprsd.packets import core from . import fake +CONF = cfg.CONF + + class TestPluginManager(unittest.TestCase): def setUp(self) -> None: @@ -21,34 +26,26 @@ class TestPluginManager(unittest.TestCase): aprsd_plugin.PluginManager._instance = None def config_and_init(self): - self.config = aprsd_config.Config(aprsd_config.DEFAULT_CONFIG_DICT) - self.config["ham"]["callsign"] = self.fromcall - self.config["aprs"]["login"] = fake.FAKE_TO_CALLSIGN - self.config["services"]["aprs.fi"]["apiKey"] = "something" - self.config["aprsd"]["enabled_plugins"] = [ - "aprsd.plugins.ping.PingPlugin", - ] - print(self.config) - - def test_init_no_config(self): - pm = aprsd_plugin.PluginManager() - self.assertEqual(None, pm.config) - - def test_init_with_config(self): - pm = aprsd_plugin.PluginManager(self.config) - self.assertEqual(self.config, pm.config) + CONF.callsign = self.fromcall + CONF.aprs_network.login = fake.FAKE_TO_CALLSIGN + CONF.aprs_fi.apiKey = "something" + CONF.enabled_plugins = "aprsd.plugins.ping.PingPlugin" + CONF.enable_save = False def test_get_plugins_no_plugins(self): - pm = aprsd_plugin.PluginManager(self.config) + CONF.enabled_plugins = [] + pm = aprsd_plugin.PluginManager() plugin_list = pm.get_plugins() self.assertEqual([], plugin_list) def test_get_plugins_with_plugins(self): - pm = aprsd_plugin.PluginManager(self.config) + CONF.enabled_plugins = ["aprsd.plugins.ping.PingPlugin"] + pm = aprsd_plugin.PluginManager() plugin_list = pm.get_plugins() self.assertEqual([], plugin_list) pm.setup_plugins() plugin_list = pm.get_plugins() + print(plugin_list) self.assertIsInstance(plugin_list, list) self.assertIsInstance( plugin_list[0], @@ -59,7 +56,7 @@ class TestPluginManager(unittest.TestCase): ) def test_get_watchlist_plugins(self): - pm = aprsd_plugin.PluginManager(self.config) + pm = aprsd_plugin.PluginManager() plugin_list = pm.get_plugins() self.assertEqual([], plugin_list) pm.setup_plugins() @@ -68,7 +65,8 @@ class TestPluginManager(unittest.TestCase): self.assertEqual(0, len(plugin_list)) def test_get_message_plugins(self): - pm = aprsd_plugin.PluginManager(self.config) + CONF.enabled_plugins = ["aprsd.plugins.ping.PingPlugin"] + pm = aprsd_plugin.PluginManager() plugin_list = pm.get_plugins() self.assertEqual([], plugin_list) pm.setup_plugins() @@ -98,27 +96,19 @@ class TestPlugin(unittest.TestCase): packets.PacketTrack._instance = None self.config = None - def config_and_init(self, config=None): - if not config: - self.config = aprsd_config.Config(aprsd_config.DEFAULT_CONFIG_DICT) - self.config["ham"]["callsign"] = self.fromcall - self.config["aprs"]["login"] = fake.FAKE_TO_CALLSIGN - self.config["services"]["aprs.fi"]["apiKey"] = "something" - else: - self.config = config - - # Inintialize the stats object with the config - stats.APRSDStats(self.config) - packets.WatchList(config=self.config) - packets.SeenList(config=self.config) - packets.PacketTrack(config=self.config) + def config_and_init(self): + CONF.callsign = self.fromcall + CONF.aprs_network.login = fake.FAKE_TO_CALLSIGN + CONF.aprs_fi.apiKey = "something" + CONF.enabled_plugins = "aprsd.plugins.ping.PingPlugin" + CONF.enable_save = False class TestPluginBase(TestPlugin): @mock.patch.object(fake.FakeBaseNoThreadsPlugin, "process") def test_base_plugin_no_threads(self, mock_process): - p = fake.FakeBaseNoThreadsPlugin(self.config) + p = fake.FakeBaseNoThreadsPlugin() expected = [] actual = p.create_threads() @@ -139,19 +129,20 @@ class TestPluginBase(TestPlugin): @mock.patch.object(fake.FakeBaseThreadsPlugin, "create_threads") def test_base_plugin_threads_created(self, mock_create): - p = fake.FakeBaseThreadsPlugin(self.config) + p = fake.FakeBaseThreadsPlugin() mock_create.assert_called_once() p.stop_threads() def test_base_plugin_threads(self): - p = fake.FakeBaseThreadsPlugin(self.config) + p = fake.FakeBaseThreadsPlugin() actual = p.create_threads() self.assertTrue(isinstance(actual, fake.FakeThread)) p.stop_threads() @mock.patch.object(fake.FakeRegexCommandPlugin, "process") def test_regex_base_not_called(self, mock_process): - p = fake.FakeRegexCommandPlugin(self.config) + CONF.callsign = fake.FAKE_TO_CALLSIGN + p = fake.FakeRegexCommandPlugin() packet = fake.fake_packet(message="a") expected = None actual = p.filter(packet) @@ -165,32 +156,32 @@ class TestPluginBase(TestPlugin): mock_process.assert_not_called() packet = fake.fake_packet( - message="F", message_format=core.PACKET_TYPE_MICE, ) - expected = None + expected = packets.NULL_MESSAGE actual = p.filter(packet) self.assertEqual(expected, actual) mock_process.assert_not_called() packet = fake.fake_packet( - message="f", message_format=core.PACKET_TYPE_ACK, ) - expected = None + expected = packets.NULL_MESSAGE actual = p.filter(packet) self.assertEqual(expected, actual) mock_process.assert_not_called() @mock.patch.object(fake.FakeRegexCommandPlugin, "process") def test_regex_base_assert_called(self, mock_process): - p = fake.FakeRegexCommandPlugin(self.config) + CONF.callsign = fake.FAKE_TO_CALLSIGN + p = fake.FakeRegexCommandPlugin() packet = fake.fake_packet(message="f") p.filter(packet) mock_process.assert_called_once() def test_regex_base_process_called(self): - p = fake.FakeRegexCommandPlugin(self.config) + CONF.callsign = fake.FAKE_TO_CALLSIGN + p = fake.FakeRegexCommandPlugin() packet = fake.fake_packet(message="f") expected = fake.FAKE_MESSAGE_TEXT From f4a6dfc8a0ccff5d6cdf3abf511eaae537e60eaa Mon Sep 17 00:00:00 2001 From: Hemna Date: Tue, 27 Dec 2022 14:46:41 -0500 Subject: [PATCH 3/7] Added missing conf --- aprsd/conf/__init__.py | 56 +++++++++++++++ aprsd/conf/client.py | 102 +++++++++++++++++++++++++++ aprsd/conf/common.py | 133 ++++++++++++++++++++++++++++++++++++ aprsd/conf/log.py | 61 +++++++++++++++++ aprsd/conf/opts.py | 80 ++++++++++++++++++++++ aprsd/conf/plugin_common.py | 83 ++++++++++++++++++++++ aprsd/conf/plugin_email.py | 106 ++++++++++++++++++++++++++++ 7 files changed, 621 insertions(+) create mode 100644 aprsd/conf/__init__.py create mode 100644 aprsd/conf/client.py create mode 100644 aprsd/conf/common.py create mode 100644 aprsd/conf/log.py create mode 100644 aprsd/conf/opts.py create mode 100644 aprsd/conf/plugin_common.py create mode 100644 aprsd/conf/plugin_email.py diff --git a/aprsd/conf/__init__.py b/aprsd/conf/__init__.py new file mode 100644 index 0000000..db28c55 --- /dev/null +++ b/aprsd/conf/__init__.py @@ -0,0 +1,56 @@ +from oslo_config import cfg + +from aprsd.conf import client, common, log, plugin_common, plugin_email + + +CONF = cfg.CONF + +log.register_opts(CONF) +common.register_opts(CONF) +client.register_opts(CONF) + +# plugins +plugin_common.register_opts(CONF) +plugin_email.register_opts(CONF) + + +def set_lib_defaults(): + """Update default value for configuration options from other namespace. + Example, oslo lib config options. This is needed for + config generator tool to pick these default value changes. + https://docs.openstack.org/oslo.config/latest/cli/ + generator.html#modifying-defaults-from-other-namespaces + """ + + # Update default value of oslo_log default_log_levels and + # logging_context_format_string config option. + set_log_defaults() + + +def set_log_defaults(): + # logging.set_defaults(default_log_levels=logging.get_default_log_levels()) + pass + + +def conf_to_dict(): + """Convert the CONF options to a single level dictionary.""" + entries = {} + + def _sanitize(opt, value): + """Obfuscate values of options declared secret.""" + return value if not opt.secret else "*" * 4 + + for opt_name in sorted(CONF._opts): + opt = CONF._get_opt_info(opt_name)["opt"] + val = str(_sanitize(opt, getattr(CONF, opt_name))) + entries[str(opt)] = val + + for group_name in list(CONF._groups): + group_attr = CONF.GroupAttr(CONF, CONF._get_group(group_name)) + for opt_name in sorted(CONF._groups[group_name]._opts): + opt = CONF._get_opt_info(opt_name, group_name)["opt"] + val = str(_sanitize(opt, getattr(group_attr, opt_name))) + gname_opt_name = f"{group_name}.{opt_name}" + entries[gname_opt_name] = val + + return entries diff --git a/aprsd/conf/client.py b/aprsd/conf/client.py new file mode 100644 index 0000000..e7e85de --- /dev/null +++ b/aprsd/conf/client.py @@ -0,0 +1,102 @@ +""" +The options for logging setup +""" + +from oslo_config import cfg + + +DEFAULT_LOGIN = "NOCALL" + +aprs_group = cfg.OptGroup( + name="aprs_network", + title="APRS-IS Network settings", +) +kiss_serial_group = cfg.OptGroup( + name="kiss_serial", + title="KISS Serial device connection", +) +kiss_tcp_group = cfg.OptGroup( + name="kiss_tcp", + title="KISS TCP/IP Device connection", +) +aprs_opts = [ + cfg.BoolOpt( + "enabled", + default=True, + help="Set enabled to False if there is no internet connectivity." + "This is useful for a direwolf KISS aprs connection only.", + ), + cfg.StrOpt( + "login", + default=DEFAULT_LOGIN, + help="APRS Username", + ), + cfg.StrOpt( + "password", + secret=True, + help="APRS Password " + "Get the passcode for your callsign here: " + "https://apps.magicbug.co.uk/passcode", + ), + cfg.HostnameOpt( + "host", + default="noam.aprs2.net", + help="The APRS-IS hostname", + ), + cfg.PortOpt( + "port", + default=14580, + help="APRS-IS port", + ), +] + +kiss_serial_opts = [ + cfg.BoolOpt( + "enabled", + default=False, + help="Enable Serial KISS interface connection.", + ), + cfg.StrOpt( + "device", + help="Serial Device file to use. /dev/ttyS0", + ), + cfg.IntOpt( + "baudrate", + default=9600, + help="The Serial device baud rate for communication", + ), +] + +kiss_tcp_opts = [ + cfg.BoolOpt( + "enabled", + default=False, + help="Enable Serial KISS interface connection.", + ), + cfg.HostnameOpt( + "host", + help="The KISS TCP Host to connect to.", + ), + cfg.PortOpt( + "port", + default=8001, + help="The KISS TCP/IP network port", + ), +] + + +def register_opts(config): + config.register_group(aprs_group) + config.register_opts(aprs_opts, group=aprs_group) + config.register_group(kiss_serial_group) + config.register_group(kiss_tcp_group) + config.register_opts(kiss_serial_opts, group=kiss_serial_group) + config.register_opts(kiss_tcp_opts, group=kiss_tcp_group) + + +def list_opts(): + return { + aprs_group.name: aprs_opts, + kiss_serial_group.name: kiss_serial_opts, + kiss_tcp_group.name: kiss_tcp_opts, + } diff --git a/aprsd/conf/common.py b/aprsd/conf/common.py new file mode 100644 index 0000000..0691d3c --- /dev/null +++ b/aprsd/conf/common.py @@ -0,0 +1,133 @@ +from oslo_config import cfg + + +admin_group = cfg.OptGroup( + name="admin", + title="Admin web interface settings", +) +watch_list_group = cfg.OptGroup( + name="watch_list", + title="Watch List settings", +) + + +aprsd_opts = [ + cfg.StrOpt( + "callsign", + required=True, + help="Callsign to use for messages sent by APRSD", + ), + cfg.BoolOpt( + "enable_save", + default=True, + help="Enable saving of watch list, packet tracker between restarts.", + ), + cfg.StrOpt( + "save_location", + default="~/.config/aprsd", + help="Save location for packet tracking files.", + ), + cfg.BoolOpt( + "trace_enabled", + default=False, + help="Enable code tracing", + ), + cfg.StrOpt( + "units", + default="imperial", + help="Units for display, imperial or metric", + ), +] + +watch_list_opts = [ + cfg.BoolOpt( + "enabled", + default=False, + help="Enable the watch list feature. Still have to enable " + "the correct plugin. Built-in plugin to use is " + "aprsd.plugins.notify.NotifyPlugin", + ), + cfg.ListOpt( + "callsigns", + help="Callsigns to watch for messsages", + ), + cfg.StrOpt( + "alert_callsign", + help="The Ham Callsign to send messages to for watch list alerts.", + ), + cfg.IntOpt( + "packet_keep_count", + default=10, + help="The number of packets to store.", + ), + cfg.IntOpt( + "alert_time_seconds", + default=3600, + help="Time to wait before alert is sent on new message for " + "users in callsigns.", + ), +] + +admin_opts = [ + cfg.BoolOpt( + "web_enabled", + default=False, + help="Enable the Admin Web Interface", + ), + cfg.IPOpt( + "web_ip", + default="0.0.0.0", + help="The ip address to listen on", + ), + cfg.PortOpt( + "web_port", + default=8001, + help="The port to listen on", + ), + cfg.StrOpt( + "user", + default="admin", + help="The admin user for the admin web interface", + ), + cfg.StrOpt( + "password", + secret=True, + help="Admin interface password", + ), +] + +enabled_plugins_opts = [ + cfg.ListOpt( + "enabled_plugins", + default=[ + "aprsd.plugins.email.EmailPlugin", + "aprsd.plugins.fortune.FortunePlugin", + "aprsd.plugins.location.LocationPlugin", + "aprsd.plugins.ping.PingPlugin", + "aprsd.plugins.query.QueryPlugin", + "aprsd.plugins.time.TimePlugin", + "aprsd.plugins.weather.OWMWeatherPlugin", + "aprsd.plugins.version.VersionPlugin", + ], + help="Comma separated list of enabled plugins for APRSD." + "To enable installed external plugins add them here." + "The full python path to the class name must be used", + ), +] + + +def register_opts(config): + config.register_opts(aprsd_opts) + config.register_opts(enabled_plugins_opts) + config.register_group(admin_group) + config.register_opts(admin_opts, group=admin_group) + config.register_group(watch_list_group) + config.register_opts(watch_list_opts, group=watch_list_group) + + +def list_opts(): + return { + "DEFAULT": (aprsd_opts + enabled_plugins_opts), + admin_group.name: admin_opts, + watch_list_group.name: watch_list_opts, + } diff --git a/aprsd/conf/log.py b/aprsd/conf/log.py new file mode 100644 index 0000000..d48ae30 --- /dev/null +++ b/aprsd/conf/log.py @@ -0,0 +1,61 @@ +""" +The options for logging setup +""" +import logging + +from oslo_config import cfg + + +LOG_LEVELS = { + "CRITICAL": logging.CRITICAL, + "ERROR": logging.ERROR, + "WARNING": logging.WARNING, + "INFO": logging.INFO, + "DEBUG": logging.DEBUG, +} + +DEFAULT_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p" +DEFAULT_LOG_FORMAT = ( + "[%(asctime)s] [%(threadName)-20.20s] [%(levelname)-5.5s]" + " %(message)s - [%(pathname)s:%(lineno)d]" +) + +logging_group = cfg.OptGroup( + name="logging", + title="Logging options", +) +logging_opts = [ + cfg.StrOpt( + "date_format", + default=DEFAULT_DATE_FORMAT, + help="Date format for log entries", + ), + cfg.BoolOpt( + "rich_logging", + default=True, + help="Enable Rich logging", + ), + cfg.StrOpt( + "logfile", + default=None, + help="File to log to", + ), + cfg.StrOpt( + "logformat", + default=DEFAULT_LOG_FORMAT, + help="Log file format, unless rich_logging enabled.", + ), +] + + +def register_opts(config): + config.register_group(logging_group) + config.register_opts(logging_opts, group=logging_group) + + +def list_opts(): + return { + logging_group.name: ( + logging_opts + ), + } diff --git a/aprsd/conf/opts.py b/aprsd/conf/opts.py new file mode 100644 index 0000000..70618d1 --- /dev/null +++ b/aprsd/conf/opts.py @@ -0,0 +1,80 @@ +# Copyright 2015 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +This is the single point of entry to generate the sample configuration +file for Nova. It collects all the necessary info from the other modules +in this package. It is assumed that: + +* every other module in this package has a 'list_opts' function which + return a dict where + * the keys are strings which are the group names + * the value of each key is a list of config options for that group +* the nova.conf package doesn't have further packages with config options +* this module is only used in the context of sample file generation +""" + +import collections +import importlib +import os +import pkgutil + + +LIST_OPTS_FUNC_NAME = "list_opts" + + +def _tupleize(dct): + """Take the dict of options and convert to the 2-tuple format.""" + return [(key, val) for key, val in dct.items()] + + +def list_opts(): + opts = collections.defaultdict(list) + module_names = _list_module_names() + imported_modules = _import_modules(module_names) + _append_config_options(imported_modules, opts) + return _tupleize(opts) + + +def _list_module_names(): + module_names = [] + package_path = os.path.dirname(os.path.abspath(__file__)) + for _, modname, ispkg in pkgutil.iter_modules(path=[package_path]): + if modname == "opts" or ispkg: + continue + else: + module_names.append(modname) + return module_names + + +def _import_modules(module_names): + imported_modules = [] + for modname in module_names: + mod = importlib.import_module("aprsd.conf." + modname) + if not hasattr(mod, LIST_OPTS_FUNC_NAME): + msg = "The module 'aprsd.conf.%s' should have a '%s' "\ + "function which returns the config options." % \ + (modname, LIST_OPTS_FUNC_NAME) + raise Exception(msg) + else: + imported_modules.append(mod) + return imported_modules + + +def _append_config_options(imported_modules, config_options): + for mod in imported_modules: + configs = mod.list_opts() + for key, val in configs.items(): + config_options[key].extend(val) diff --git a/aprsd/conf/plugin_common.py b/aprsd/conf/plugin_common.py new file mode 100644 index 0000000..4d43f3e --- /dev/null +++ b/aprsd/conf/plugin_common.py @@ -0,0 +1,83 @@ +from oslo_config import cfg + + +aprsfi_group = cfg.OptGroup( + name="aprs_fi", + title="APRS.FI website settings", +) +query_group = cfg.OptGroup( + name="query_plugin", + title="Options for the Query Plugin", +) +avwx_group = cfg.OptGroup( + name="avwx_plugin", + title="Options for the AVWXWeatherPlugin", +) +owm_wx_group = cfg.OptGroup( + name="owm_weather_plugin", + title="Options for the OWMWeatherPlugin", +) + +aprsfi_opts = [ + cfg.StrOpt( + "apiKey", + help="Get the apiKey from your aprs.fi account here:" + "http://aprs.fi/account", + ), +] + +query_plugin_opts = [ + cfg.StrOpt( + "callsign", + help="The Ham callsign to allow access to the query plugin from RF.", + ), +] + +owm_wx_opts = [ + cfg.StrOpt( + "apiKey", + help="OWMWeatherPlugin api key to OpenWeatherMap's API." + "This plugin uses the openweathermap API to fetch" + "location and weather information." + "To use this plugin you need to get an openweathermap" + "account and apikey." + "https://home.openweathermap.org/api_keys", + ), +] + +avwx_opts = [ + cfg.StrOpt( + "apiKey", + help="avwx-api is an opensource project that has" + "a hosted service here: https://avwx.rest/" + "You can launch your own avwx-api in a container" + "by cloning the githug repo here:" + "https://github.com/avwx-rest/AVWX-API", + ), + cfg.StrOpt( + "base_url", + default="https://avwx.rest", + help="The base url for the avwx API. If you are hosting your own" + "Here is where you change the url to point to yours.", + ), +] + + +def register_opts(config): + config.register_group(aprsfi_group) + config.register_opts(aprsfi_opts, group=aprsfi_group) + config.register_group(query_group) + config.register_opts(query_plugin_opts, group=query_group) + config.register_group(owm_wx_group) + config.register_opts(owm_wx_opts, group=owm_wx_group) + config.register_group(avwx_group) + config.register_opts(avwx_opts, group=avwx_group) + + +def list_opts(): + return { + aprsfi_group.name: aprsfi_opts, + query_group.name: query_plugin_opts, + owm_wx_group.name: owm_wx_opts, + avwx_group.name: avwx_opts, + } diff --git a/aprsd/conf/plugin_email.py b/aprsd/conf/plugin_email.py new file mode 100644 index 0000000..2c60281 --- /dev/null +++ b/aprsd/conf/plugin_email.py @@ -0,0 +1,106 @@ +from oslo_config import cfg + + +email_group = cfg.OptGroup( + name="email_plugin", + title="Options for the APRSD Email plugin", +) + +email_opts = [ + cfg.StrOpt( + "callsign", + required=True, + help="(Required) Callsign to validate for doing email commands." + "Only this callsign can check email. This is also where the " + "email notifications for new emails will be sent.", + ), + cfg.BoolOpt( + "enabled", + default=False, + help="Enable the Email plugin?", + ), + cfg.BoolOpt( + "debug", + default=False, + help="Enable the Email plugin Debugging?", + ), +] + +email_imap_opts = [ + cfg.StrOpt( + "imap_login", + help="Login username/email for IMAP server", + ), + cfg.StrOpt( + "imap_password", + secret=True, + help="Login password for IMAP server", + ), + cfg.HostnameOpt( + "imap_host", + help="Hostname/IP of the IMAP server", + ), + cfg.PortOpt( + "imap_port", + default=993, + help="Port to use for IMAP server", + ), + cfg.BoolOpt( + "imap_use_ssl", + default=True, + help="Use SSL for connection to IMAP Server", + ), +] + +email_smtp_opts = [ + cfg.StrOpt( + "smtp_login", + help="Login username/email for SMTP server", + ), + cfg.StrOpt( + "smtp_password", + secret=True, + help="Login password for SMTP server", + ), + cfg.HostnameOpt( + "smtp_host", + help="Hostname/IP of the SMTP server", + ), + cfg.PortOpt( + "smtp_port", + default=465, + help="Port to use for SMTP server", + ), + cfg.BoolOpt( + "smtp_use_ssl", + default=True, + help="Use SSL for connection to SMTP Server", + ), +] + +email_shortcuts_opts = [ + cfg.ListOpt( + "email_shortcuts", + help="List of email shortcuts for checking/sending email " + "For Exmaple: wb=walt@walt.com,cl=cl@cl.com\n" + "Means use 'wb' to send an email to walt@walt.com", + ), +] + +ALL_OPTS = ( + email_opts + + email_imap_opts + + email_smtp_opts + + email_shortcuts_opts +) + + +def register_opts(config): + config.register_group(email_group) + config.register_opts(ALL_OPTS, group=email_group) + + +def list_opts(): + return { + email_group.name: ALL_OPTS, + } From e9a954a8fd79059995f8e44832bea040c9a5320a Mon Sep 17 00:00:00 2001 From: Hemna Date: Tue, 27 Dec 2022 15:31:49 -0500 Subject: [PATCH 4/7] Fix some unit tests and loading of CONF w/o file --- aprsd/cli_helper.py | 13 +++++++----- aprsd/cmds/dev.py | 3 ++- tests/cmds/test_dev.py | 45 ------------------------------------------ 3 files changed, 10 insertions(+), 51 deletions(-) delete mode 100644 tests/cmds/test_dev.py diff --git a/aprsd/cli_helper.py b/aprsd/cli_helper.py index e1c8508..ed8acce 100644 --- a/aprsd/cli_helper.py +++ b/aprsd/cli_helper.py @@ -65,12 +65,15 @@ def process_standard_options(f: F) -> F: default_config_files = [kwargs["config_file"]] else: default_config_files = None - CONF( - [], project="aprsd", version=aprsd.__version__, - default_config_files=default_config_files, - ) + try: + CONF( + [], project="aprsd", version=aprsd.__version__, + default_config_files=default_config_files, + ) + except cfg.ConfigFilesNotFoundError: + pass ctx.obj["loglevel"] = kwargs["loglevel"] - ctx.obj["config_file"] = kwargs["config_file"] + # ctx.obj["config_file"] = kwargs["config_file"] ctx.obj["quiet"] = kwargs["quiet"] log.setup_logging( ctx.obj["loglevel"], diff --git a/aprsd/cmds/dev.py b/aprsd/cmds/dev.py index ab8d07d..cda428a 100644 --- a/aprsd/cmds/dev.py +++ b/aprsd/cmds/dev.py @@ -86,7 +86,8 @@ def test_plugin( if not plugin_path: click.echo(ctx.get_help()) click.echo("") - ctx.fail("Failed to provide -p option to test a plugin") + click.echo("Failed to provide -p option to test a plugin") + ctx.exit(-1) return if type(message) is tuple: diff --git a/tests/cmds/test_dev.py b/tests/cmds/test_dev.py deleted file mode 100644 index aaaee26..0000000 --- a/tests/cmds/test_dev.py +++ /dev/null @@ -1,45 +0,0 @@ -import typing as t -import unittest -from unittest import mock - -from click.testing import CliRunner -from oslo_config import cfg - -from aprsd import conf # noqa: F401 -from aprsd.aprsd import cli -from aprsd.cmds import dev # noqa - -from .. import fake - - -CONF = cfg.CONF -F = t.TypeVar("F", bound=t.Callable[..., t.Any]) - - -class TestDevTestPluginCommand(unittest.TestCase): - - def config_and_init(self, login=None, password=None): - CONF.callsign = fake.FAKE_TO_CALLSIGN - CONF.trace_enabled = False - CONF.watch_list.packet_keep_count = 1 - if login: - CONF.aprs_network.login = login - if password: - CONF.aprs_network.password = password - - CONF.admin.user = "admin" - CONF.admin.password = "password" - - @mock.patch("aprsd.logging.log.setup_logging") - def test_no_plugin_arg(self, mock_logging): - """Make sure we get an error if there is no login and config.""" - - runner = CliRunner() - self.config_and_init(login="something") - - result = runner.invoke( - cli, ["dev", "test-plugin", "bogus command"], - catch_exceptions=False, - ) - assert result.exit_code == 2 - assert "Failed to provide -p option to test a plugin" in result.output From 02e4f78d0e3be5098333263cfafb2cc3debdff81 Mon Sep 17 00:00:00 2001 From: Hemna Date: Tue, 27 Dec 2022 15:44:32 -0500 Subject: [PATCH 5/7] Dockerfile now produces aprsd.conf This patch updates Dockerfile and Dockerfile-dev to produce aprsd.conf instead of aprsd.yaml --- docker/Dockerfile | 4 ++-- docker/Dockerfile-dev | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index ec8109a..ae52a68 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -41,7 +41,7 @@ RUN pip install aprsd==$APRSD_PIP_VERSION # Ensure /config is there with a default config file USER root RUN mkdir -p /config -RUN aprsd sample-config > /config/aprsd.yml +RUN aprsd sample-config > /config/aprsd.conf RUN chown -R $APRS_USER:$APRS_USER /config # override this to run another configuration @@ -53,4 +53,4 @@ ADD bin/run.sh /usr/local/bin ENTRYPOINT ["/usr/local/bin/run.sh"] HEALTHCHECK --interval=5m --timeout=12s --start-period=30s \ - CMD aprsd healthcheck --config /config/aprsd.yml --url http://localhost:8001/stats + CMD aprsd healthcheck --config /config/aprsd.conf --url http://localhost:8001/stats diff --git a/docker/Dockerfile-dev b/docker/Dockerfile-dev index f1a9dc0..76ce948 100644 --- a/docker/Dockerfile-dev +++ b/docker/Dockerfile-dev @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1 -FROM ubuntu:focal as aprsd +FROM ubuntu:22.04 # Dockerfile for building a container during aprsd development. ARG BRANCH=master @@ -55,7 +55,7 @@ RUN ls -al /usr/local/bin RUN ls -al /usr/bin RUN which aprsd RUN mkdir -p /config -RUN aprsd sample-config > /config/aprsd.yml +RUN aprsd sample-config > /config/aprsd.conf RUN chown -R $APRS_USER:$APRS_USER /config # override this to run another configuration @@ -67,4 +67,4 @@ ADD bin/run.sh $HOME/ ENTRYPOINT ["/home/aprs/run.sh"] HEALTHCHECK --interval=5m --timeout=12s --start-period=30s \ - CMD aprsd healthcheck --config /config/aprsd.yml --url http://localhost:8001/stats + CMD aprsd healthcheck --config /config/aprsd.conf --url http://localhost:8001/stats From ff392395ed9e4eb8fcc46e8ec475ff82f033c45b Mon Sep 17 00:00:00 2001 From: Hemna Date: Wed, 28 Dec 2022 15:44:49 -0500 Subject: [PATCH 6/7] Decouple admin web interface from server command This patch introduces rpyc based RPC client/server for the flask web interface to call into the running aprsd server command to fetch stats, logs, etc to send to the browser. This allows running the web interface via gunicorn command gunicorn -k gevent --reload --threads 10 -w 1 aprsd.flask:app --log-level DEBUG --- aprsd/aprsd.py | 3 + aprsd/cmds/server.py | 28 +- aprsd/conf/common.py | 32 ++ aprsd/flask.py | 597 ++++++++------------ aprsd/rpc_server.py | 90 +++ aprsd/stats.py | 2 +- aprsd/threads/log_monitor.py | 77 +++ aprsd/web/admin/static/js/main.js | 5 - aprsd/web/admin/static/js/send-message.js | 30 - aprsd/web/admin/templates/index.html | 24 - aprsd/web/admin/templates/messages.html | 15 - aprsd/web/admin/templates/send-message.html | 74 --- dev-requirements.txt | 6 +- requirements.in | 3 +- requirements.txt | 2 + 15 files changed, 447 insertions(+), 541 deletions(-) create mode 100644 aprsd/rpc_server.py create mode 100644 aprsd/threads/log_monitor.py delete mode 100644 aprsd/web/admin/templates/messages.html delete mode 100644 aprsd/web/admin/templates/send-message.html diff --git a/aprsd/aprsd.py b/aprsd/aprsd.py index 7021e1b..43d079a 100644 --- a/aprsd/aprsd.py +++ b/aprsd/aprsd.py @@ -40,9 +40,11 @@ from aprsd import cli_helper, packets, stats, threads, utils # setup the global logger # logging.basicConfig(level=logging.DEBUG) # level=10 +CONF = cfg.CONF LOG = logging.getLogger("APRSD") CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) flask_enabled = False +rpc_serv = None def custom_startswith(string, incomplete): @@ -92,6 +94,7 @@ def signal_handler(sig, frame): LOG.info(stats.APRSDStats()) # signal.signal(signal.SIGTERM, sys.exit(0)) # sys.exit(0) + if flask_enabled: signal.signal(signal.SIGTERM, sys.exit(0)) diff --git a/aprsd/cmds/server.py b/aprsd/cmds/server.py index d936352..dec6be1 100644 --- a/aprsd/cmds/server.py +++ b/aprsd/cmds/server.py @@ -6,8 +6,10 @@ import click from oslo_config import cfg import aprsd +from aprsd import ( + cli_helper, client, packets, plugin, rpc_server, threads, utils, +) from aprsd import aprsd as aprsd_main -from aprsd import cli_helper, client, flask, packets, plugin, threads, utils from aprsd.aprsd import cli from aprsd.threads import rx @@ -32,15 +34,9 @@ LOG = logging.getLogger("APRSD") @cli_helper.process_standard_options def server(ctx, flush): """Start the aprsd server gateway process.""" - loglevel = ctx.obj["loglevel"] - quiet = ctx.obj["quiet"] - signal.signal(signal.SIGINT, aprsd_main.signal_handler) signal.signal(signal.SIGTERM, aprsd_main.signal_handler) - if not quiet: - click.echo("Load config") - level, msg = utils._check_version() if level: LOG.warning(msg) @@ -99,18 +95,10 @@ def server(ctx, flush): keepalive = threads.KeepAliveThread() keepalive.start() - web_enabled = CONF.admin.web_enabled + if CONF.rpc_settings.enabled: + rpc = rpc_server.APRSDRPCThread() + rpc.start() + log_monitor = threads.log_monitor.LogMonitorThread() + log_monitor.start() - if web_enabled: - aprsd_main.flask_enabled = True - (socketio, app) = flask.init_flask(loglevel, quiet) - socketio.run( - app, - allow_unsafe_werkzeug=True, - host=CONF.admin.web_ip, - port=CONF.admin.web_port, - ) - - # If there are items in the msgTracker, then save them - LOG.info("APRSD Exiting.") return 0 diff --git a/aprsd/conf/common.py b/aprsd/conf/common.py index 0691d3c..a24bd55 100644 --- a/aprsd/conf/common.py +++ b/aprsd/conf/common.py @@ -1,6 +1,8 @@ from oslo_config import cfg +APRSD_DEFAULT_MAGIC_WORD = "CHANGEME!!!" + admin_group = cfg.OptGroup( name="admin", title="Admin web interface settings", @@ -9,6 +11,10 @@ watch_list_group = cfg.OptGroup( name="watch_list", title="Watch List settings", ) +rpc_group = cfg.OptGroup( + name="rpc_settings", + title="RPC Settings for admin <--> web", +) aprsd_opts = [ @@ -96,6 +102,29 @@ admin_opts = [ ), ] +rpc_opts = [ + cfg.BoolOpt( + "enabled", + default=True, + help="Enable RPC calls", + ), + cfg.StrOpt( + "ip", + default="localhost", + help="The ip address to listen on", + ), + cfg.PortOpt( + "port", + default=18861, + help="The port to listen on", + ), + cfg.StrOpt( + "magic_word", + default=APRSD_DEFAULT_MAGIC_WORD, + help="Magic word to authenticate requests between client/server", + ), +] + enabled_plugins_opts = [ cfg.ListOpt( "enabled_plugins", @@ -123,6 +152,8 @@ def register_opts(config): config.register_opts(admin_opts, group=admin_group) config.register_group(watch_list_group) config.register_opts(watch_list_opts, group=watch_list_group) + config.register_group(rpc_group) + config.register_opts(rpc_opts, group=rpc_group) def list_opts(): @@ -130,4 +161,5 @@ def list_opts(): "DEFAULT": (aprsd_opts + enabled_plugins_opts), admin_group.name: admin_opts, watch_list_group.name: watch_list_opts, + rpc_group.name: rpc_opts, } diff --git a/aprsd/flask.py b/aprsd/flask.py index 9b1a302..58a2575 100644 --- a/aprsd/flask.py +++ b/aprsd/flask.py @@ -2,27 +2,21 @@ import datetime import json import logging from logging.handlers import RotatingFileHandler -import threading import time -import aprslib -from aprslib.exceptions import LoginError import flask -from flask import request from flask.logging import default_handler import flask_classful from flask_httpauth import HTTPBasicAuth from flask_socketio import Namespace, SocketIO from oslo_config import cfg +import rpyc from werkzeug.security import check_password_hash, generate_password_hash -import wrapt import aprsd -from aprsd import client, conf, packets, plugin, stats, threads, utils -from aprsd.clients import aprsis -from aprsd.logging import log +from aprsd import cli_helper, client, conf, packets, plugin, threads +from aprsd.conf import common from aprsd.logging import rich as aprsd_logging -from aprsd.threads import tx CONF = cfg.CONF @@ -30,72 +24,20 @@ LOG = logging.getLogger("APRSD") auth = HTTPBasicAuth() users = None +app = None -class SentMessages: - _instance = None - lock = threading.Lock() +class AuthSocketStream(rpyc.SocketStream): + """Used to authenitcate the RPC stream to remote.""" - msgs = {} + @classmethod + def connect(cls, *args, authorizer=None, **kwargs): + stream_obj = super().connect(*args, **kwargs) - def __new__(cls, *args, **kwargs): - """This magic turns this into a singleton.""" - if cls._instance is None: - cls._instance = super().__new__(cls) - # Put any initialization here. - return cls._instance + if callable(authorizer): + authorizer(stream_obj.sock) - @wrapt.synchronized(lock) - def add(self, packet): - self.msgs[packet.msgNo] = self._create(packet.msgNo) - self.msgs[packet.msgNo]["from"] = packet.from_call - self.msgs[packet.msgNo]["to"] = packet.to_call - self.msgs[packet.msgNo]["message"] = packet.message_text.rstrip("\n") - packet._build_raw() - self.msgs[packet.msgNo]["raw"] = packet.raw.rstrip("\n") - - def _create(self, id): - return { - "id": id, - "ts": time.time(), - "ack": False, - "from": None, - "to": None, - "raw": None, - "message": None, - "status": None, - "last_update": None, - "reply": None, - } - - @wrapt.synchronized(lock) - def __len__(self): - return len(self.msgs.keys()) - - @wrapt.synchronized(lock) - def get(self, id): - if id in self.msgs: - return self.msgs[id] - - @wrapt.synchronized(lock) - def get_all(self): - return self.msgs - - @wrapt.synchronized(lock) - def set_status(self, id, status): - self.msgs[id]["last_update"] = str(datetime.datetime.now()) - self.msgs[id]["status"] = status - - @wrapt.synchronized(lock) - def ack(self, id): - """The message got an ack!""" - self.msgs[id]["last_update"] = str(datetime.datetime.now()) - self.msgs[id]["ack"] = True - - @wrapt.synchronized(lock) - def reply(self, id, packet): - """We got a packet back from the sent message.""" - self.msgs[id]["reply"] = packet + return stream_obj # HTTPBasicAuth doesn't work on a class method. @@ -109,174 +51,129 @@ def verify_password(username, password): return username -class SendMessageThread(threads.APRSDRXThread): - """Thread for sending a message from web.""" +class RPCClient: + _instance = None + _rpc_client = None - aprsis_client = None - request = None - got_ack = False - got_reply = False + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance - def __init__(self, info, packet, namespace): - self.request = info - self.packet = packet - self.namespace = namespace - self.start_time = datetime.datetime.now() - msg = "({} -> {}) : {}".format( - info["from"], - info["to"], - info["message"], - ) - super().__init__(f"WEB_SEND_MSG-{msg}") + def __init__(self): + self._check_settings() + self.get_rpc_client() - def setup_connection(self): - user = self.request["from"] - password = self.request["password"] - host = CONF.aprs_network.host - port = CONF.aprs_network.port - connected = False - backoff = 1 - while not connected: - try: - LOG.info("Creating aprslib client") + def _check_settings(self): + if not CONF.rpc_settings.enabled: + LOG.error("RPC is not enabled, no way to get stats!!") - aprs_client = aprsis.Aprsdis( - user, - passwd=password, - host=host, - port=port, - ) - # Force the logging to be the same - aprs_client.logger = LOG - aprs_client.connect() - connected = True - backoff = 1 - except LoginError as e: - LOG.error(f"Failed to login to APRS-IS Server '{e}'") - connected = False - raise e - except Exception as e: - LOG.error(f"Unable to connect to APRS-IS server. '{e}' ") - time.sleep(backoff) - backoff = backoff * 2 - continue - LOG.debug(f"Logging in to APRS-IS with user '{user}'") - return aprs_client + if CONF.rpc_settings.magic_word == 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") - def run(self): - LOG.debug("Starting") - from_call = self.request["from"] - to_call = self.request["to"] - message = self.request["message"] - LOG.info( - "From: '{}' To: '{}' Send '{}'".format( - from_call, - to_call, - message, - ), - ) + def _rpyc_connect( + self, host, port, + service=rpyc.VoidService, + config={}, ipv6=False, + keepalive=False, authorizer=None, + ): + print(f"Connecting to RPC host {host}:{port}") try: - self.aprs_client = self.setup_connection() - except LoginError as e: - f"Failed to setup Connection {e}" - - tx.send( - self.packet, - direct=True, - aprs_client=self.aprs_client, - ) - SentMessages().set_status(self.packet.msgNo, "Sent") - - while not self.thread_stop: - can_loop = self.loop() - if not can_loop: - self.stop() - threads.APRSDThreadList().remove(self) - LOG.debug("Exiting") - - def process_ack_packet(self, packet): - global socketio - ack_num = packet.msgNo - LOG.info(f"We got ack for our sent message {ack_num}") - packet.log("RXACK") - SentMessages().ack(self.packet.msgNo) - stats.APRSDStats().ack_rx_inc() - socketio.emit( - "ack", SentMessages().get(self.packet.msgNo), - namespace="/sendmsg", - ) - if self.request["wait_reply"] == "0" or self.got_reply: - # We aren't waiting for a reply, so we can bail - self.stop() - self.thread_stop = self.aprs_client.thread_stop = True - - def process_our_message_packet(self, packet): - global socketio - packets.PacketList().rx(packet) - stats.APRSDStats().msgs_rx_inc() - msg_number = packet.msgNo - SentMessages().reply(self.packet.msgNo, packet) - SentMessages().set_status(self.packet.msgNo, "Got Reply") - socketio.emit( - "reply", SentMessages().get(self.packet.msgNo), - namespace="/sendmsg", - ) - tx.send( - packets.AckPacket( - from_call=self.request["from"], - to_call=packet.from_call, - msgNo=msg_number, - ), - direct=True, - aprs_client=self.aprsis_client, - ) - SentMessages().set_status(self.packet.msgNo, "Ack Sent") - - # Now we can exit, since we are done. - self.got_reply = True - if self.got_ack: - self.stop() - self.thread_stop = self.aprs_client.thread_stop = True - - def process_packet(self, *args, **kwargs): - packet = self._client.decode_packet(*args, **kwargs) - packet.log(header="RX Packet") - - if isinstance(packet, packets.AckPacket): - self.process_ack_packet(packet) - else: - self.process_our_message_packet(packet) - - def loop(self): - # we have a general time limit expecting results of - # around 120 seconds before we exit - now = datetime.datetime.now() - start_delta = str(now - self.start_time) - delta = utils.parse_delta_str(start_delta) - d = datetime.timedelta(**delta) - max_timeout = {"hours": 0.0, "minutes": 1, "seconds": 0} - max_delta = datetime.timedelta(**max_timeout) - if d > max_delta: - LOG.error("XXXXXX Haven't completed everything in 60 seconds. BAIL!") - return False - - if self.got_ack and self.got_reply: - LOG.warning("We got everything already. BAIL") - return False - - try: - # This will register a packet consumer with aprslib - # When new packets come in the consumer will process - # the packet - self.aprs_client.consumer( - self.process_packet, raw=False, blocking=False, + s = AuthSocketStream.connect( + host, port, ipv6=ipv6, keepalive=keepalive, + authorizer=authorizer, ) - except aprslib.exceptions.ConnectionDrop: - LOG.error("Connection dropped.") - return False + return rpyc.utils.factory.connect_stream(s, service, config=config) + except ConnectionRefusedError: + LOG.error(f"Failed to connect to RPC host {host}") + return None - return True + def get_rpc_client(self): + if not self._rpc_client: + magic = 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()), + ) + return self._rpc_client + + def get_stats_dict(self): + cl = self.get_rpc_client() + result = {} + if not cl: + return result + + try: + rpc_stats_dict = cl.root.get_stats() + result = json.loads(rpc_stats_dict) + except EOFError: + LOG.error("Lost connection to RPC Host") + self._rpc_client = None + return result + + def get_packet_track(self): + cl = self.get_rpc_client() + result = None + if not cl: + return result + try: + result = cl.root.get_packet_track() + except EOFError: + LOG.error("Lost connection to RPC Host") + self._rpc_client = None + return result + + def get_packet_list(self): + cl = self.get_rpc_client() + result = None + if not cl: + return result + try: + result = cl.root.get_packet_list() + except EOFError: + LOG.error("Lost connection to RPC Host") + self._rpc_client = None + return result + + def get_watch_list(self): + cl = self.get_rpc_client() + result = None + if not cl: + return result + try: + result = cl.root.get_watch_list() + except EOFError: + LOG.error("Lost connection to RPC Host") + self._rpc_client = None + return result + + def get_seen_list(self): + cl = self.get_rpc_client() + result = None + if not cl: + return result + try: + result = cl.root.get_seen_list() + except EOFError: + LOG.error("Lost connection to RPC Host") + self._rpc_client = None + return result + + def get_log_entries(self): + cl = self.get_rpc_client() + result = None + if not cl: + return result + try: + result_str = cl.root.get_log_entries() + result = json.loads(result_str) + except EOFError: + LOG.error("Lost connection to RPC Host") + self._rpc_client = None + return result class APRSDFlask(flask_classful.FlaskView): @@ -291,21 +188,25 @@ class APRSDFlask(flask_classful.FlaskView): @auth.login_required def index(self): stats = self._stats() + print(stats) LOG.debug( "watch list? {}".format( CONF.watch_list.callsigns, ), ) - wl = packets.WatchList() - if wl.is_enabled(): + wl = RPCClient().get_watch_list() + if wl and wl.is_enabled(): watch_count = len(wl) watch_age = wl.max_delta() else: watch_count = 0 watch_age = 0 - sl = packets.SeenList() - seen_count = len(sl) + sl = RPCClient().get_seen_list() + if sl: + seen_count = len(sl) + else: + seen_count = 0 pm = plugin.PluginManager() plugins = pm.get_plugins() @@ -346,7 +247,10 @@ class APRSDFlask(flask_classful.FlaskView): aprs_connection=aprs_connection, callsign=CONF.callsign, version=aprsd.__version__, - config_json=json.dumps(entries), + config_json=json.dumps( + entries, indent=4, + sort_keys=True, default=str, + ), watch_count=watch_count, watch_age=watch_age, seen_count=seen_count, @@ -363,31 +267,18 @@ class APRSDFlask(flask_classful.FlaskView): return flask.render_template("messages.html", messages=json.dumps(msgs)) - @auth.login_required - def send_message_status(self): - LOG.debug(request) - msgs = SentMessages() - info = msgs.get_all() - return json.dumps(info) - - @auth.login_required - def send_message(self): - LOG.debug(request) - if request.method == "GET": - return flask.render_template( - "send-message.html", - callsign=CONF.callsign, - version=aprsd.__version__, - ) - @auth.login_required def packets(self): - packet_list = packets.PacketList().get() - tmp_list = [] - for pkt in packet_list: - tmp_list.append(pkt.json) + packet_list = RPCClient().get_packet_list() + if packet_list: + packets = packet_list.get() + tmp_list = [] + for pkt in packets: + tmp_list.append(pkt.json) - return json.dumps(tmp_list) + return json.dumps(tmp_list) + else: + return json.dumps([]) @auth.login_required def plugins(self): @@ -404,39 +295,69 @@ class APRSDFlask(flask_classful.FlaskView): return json.dumps({"messages": "saved"}) def _stats(self): - stats_obj = stats.APRSDStats() - track = packets.PacketTrack() + track = RPCClient().get_packet_track() now = datetime.datetime.now() time_format = "%m-%d-%Y %H:%M:%S" - stats_dict = stats_obj.stats() - - # Convert the watch_list entries to age - wl = packets.WatchList() - new_list = {} - for call in wl.get_all(): - # call_date = datetime.datetime.strptime( - # str(wl.last_seen(call)), - # "%Y-%m-%d %H:%M:%S.%f", - # ) - new_list[call] = { - "last": wl.age(call), - "packets": wl.get(call)["packets"].get(), + stats_dict = RPCClient().get_stats_dict() + if not stats_dict: + stats_dict = { + "aprsd": {}, + "aprs-is": {"server": ""}, + "messages": { + "sent": 0, + "received": 0, + }, + "email": { + "sent": 0, + "received": 0, + }, + "seen_list": { + "sent": 0, + "received": 0, + }, } + # Convert the watch_list entries to age + wl = RPCClient().get_watch_list() + new_list = {} + if wl: + for call in wl.get_all(): + # call_date = datetime.datetime.strptime( + # str(wl.last_seen(call)), + # "%Y-%m-%d %H:%M:%S.%f", + # ) + + # We have to convert the RingBuffer to a real list + # so that json.dumps works. + # pkts = [] + # for pkt in wl.get(call)["packets"].get(): + # pkts.append(pkt) + + new_list[call] = { + "last": wl.age(call), + # "packets": pkts + } + stats_dict["aprsd"]["watch_list"] = new_list - packet_list = packets.PacketList() - rx = packet_list.total_rx() - tx = packet_list.total_tx() + packet_list = RPCClient().get_packet_list() + rx = tx = 0 + if packet_list: + rx = packet_list.total_rx() + tx = packet_list.total_tx() stats_dict["packets"] = { "sent": tx, "received": rx, } + if track: + size_tracker = len(track) + else: + size_tracker = 0 result = { "time": now.strftime(time_format), - "size_tracker": len(track), + "size_tracker": size_tracker, "stats": stats_dict, } @@ -446,116 +367,44 @@ class APRSDFlask(flask_classful.FlaskView): return json.dumps(self._stats()) -class SendMessageNamespace(Namespace): - got_ack = False - reply_sent = False - packet = None - request = None - - def __init__(self, namespace=None): - super().__init__(namespace) - - def on_connect(self): - global socketio - LOG.debug("Web socket connected") - socketio.emit( - "connected", {"data": "/sendmsg Connected"}, - namespace="/sendmsg", - ) - - def on_disconnect(self): - LOG.debug("WS Disconnected") - - def on_send(self, data): - global socketio - LOG.debug(f"WS: on_send {data}") - self.request = data - self.packet = packets.MessagePacket( - from_call=data["from"], - to_call=data["to"], - message_text=data["message"], - ) - msgs = SentMessages() - msgs.add(self.packet) - msgs.set_status(self.packet.msgNo, "Sending") - socketio.emit( - "sent", SentMessages().get(self.packet.msgNo), - namespace="/sendmsg", - ) - - socketio.start_background_task( - self._start, data, - self.packet, self, - ) - LOG.warning("WS: on_send: exit") - - def _start(self, data, packet, namespace): - msg_thread = SendMessageThread(data, packet, self) - msg_thread.start() - - def handle_message(self, data): - LOG.debug(f"WS Data {data}") - - def handle_json(self, data): - LOG.debug(f"WS json {data}") - - -class LogMonitorThread(threads.APRSDThread): +class LogUpdateThread(threads.APRSDThread): def __init__(self): - super().__init__("LogMonitorThread") + super().__init__("LogUpdate") def loop(self): global socketio - try: - record = log.logging_queue.get(block=True, timeout=5) - json_record = self.json_record(record) - socketio.emit( - "log_entry", json_record, - namespace="/logs", - ) - except Exception: - # Just ignore thi - pass + if socketio: + log_entries = RPCClient().get_log_entries() + + if log_entries: + for entry in log_entries: + socketio.emit( + "log_entry", entry, + namespace="/logs", + ) + + time.sleep(5) return True - def json_record(self, record): - entry = {} - entry["filename"] = record.filename - entry["funcName"] = record.funcName - entry["levelname"] = record.levelname - entry["lineno"] = record.lineno - entry["module"] = record.module - entry["name"] = record.name - entry["pathname"] = record.pathname - entry["process"] = record.process - entry["processName"] = record.processName - if hasattr(record, "stack_info"): - entry["stack_info"] = record.stack_info - else: - entry["stack_info"] = None - entry["thread"] = record.thread - entry["threadName"] = record.threadName - entry["message"] = record.getMessage() - return entry - class LoggingNamespace(Namespace): + log_thread = None def on_connect(self): global socketio - LOG.debug("Web socket connected") socketio.emit( "connected", {"data": "/logs Connected"}, namespace="/logs", ) - self.log_thread = LogMonitorThread() + self.log_thread = LogUpdateThread() self.log_thread.start() def on_disconnect(self): - LOG.debug("WS Disconnected") - self.log_thread.stop() + LOG.debug("LOG Disconnected") + if self.log_thread: + self.log_thread.stop() def setup_logging(flask_app, loglevel, quiet): @@ -608,8 +457,6 @@ def init_flask(loglevel, quiet): flask_app.route("/stats", methods=["GET"])(server.stats) flask_app.route("/messages", methods=["GET"])(server.messages) flask_app.route("/packets", methods=["GET"])(server.packets) - flask_app.route("/send-message", methods=["GET"])(server.send_message) - flask_app.route("/send-message-status", methods=["GET"])(server.send_message_status) flask_app.route("/save", methods=["GET"])(server.save) flask_app.route("/plugins", methods=["GET"])(server.plugins) @@ -619,7 +466,21 @@ def init_flask(loglevel, quiet): ) # import eventlet # eventlet.monkey_patch() + gunicorn_logger = logging.getLogger("gunicorn.error") + flask_app.logger.handlers = gunicorn_logger.handlers + flask_app.logger.setLevel(gunicorn_logger.level) - socketio.on_namespace(SendMessageNamespace("/sendmsg")) socketio.on_namespace(LoggingNamespace("/logs")) return socketio, flask_app + + +if __name__ == "aprsd.flask": + try: + default_config_file = cli_helper.DEFAULT_CONFIG_FILE + CONF( + [], project="aprsd", version=aprsd.__version__, + default_config_files=[default_config_file], + ) + except cfg.ConfigFilesNotFoundError: + pass + sio, app = init_flask("DEBUG", False) diff --git a/aprsd/rpc_server.py b/aprsd/rpc_server.py new file mode 100644 index 0000000..040a93d --- /dev/null +++ b/aprsd/rpc_server.py @@ -0,0 +1,90 @@ +import json +import logging + +from oslo_config import cfg +import rpyc +from rpyc.utils.authenticators import AuthenticationError +from rpyc.utils.server import ThreadPoolServer + +from aprsd import conf # noqa: F401 +from aprsd import packets, stats, threads +from aprsd.threads import log_monitor + + +CONF = cfg.CONF +LOG = logging.getLogger("APRSD") + + +def magic_word_authenticator(sock): + magic = sock.recv(len(CONF.rpc_settings.magic_word)).decode() + if magic != CONF.rpc_settings.magic_word: + raise AuthenticationError(f"wrong magic word {magic}") + return sock, None + + +class APRSDRPCThread(threads.APRSDThread): + def __init__(self): + super().__init__(name="RPCThread") + self.thread = ThreadPoolServer( + APRSDService, + port=CONF.rpc_settings.port, + protocol_config={"allow_public_attrs": True}, + authenticator=magic_word_authenticator, + ) + + def stop(self): + if self.thread: + self.thread.close() + self.thread_stop = True + + def loop(self): + # there is no loop as run is blocked + if self.thread and not self.thread_stop: + # This is a blocking call + self.thread.start() + + +@rpyc.service +class APRSDService(rpyc.Service): + def on_connect(self, conn): + # code that runs when a connection is created + # (to init the service, if needed) + LOG.info("Connected") + self._conn = conn + + def on_disconnect(self, conn): + # code that runs after the connection has already closed + # (to finalize the service, if needed) + LOG.info("Disconnected") + self._conn = None + + @rpyc.exposed + def get_stats(self): + stat = stats.APRSDStats() + stats_dict = stat.stats() + return json.dumps(stats_dict, indent=4, sort_keys=True, default=str) + + @rpyc.exposed + def get_stats_obj(self): + return stats.APRSDStats() + + @rpyc.exposed + def get_packet_list(self): + return packets.PacketList() + + @rpyc.exposed + def get_packet_track(self): + return packets.PacketTrack() + + @rpyc.exposed + def get_watch_list(self): + return packets.WatchList() + + @rpyc.exposed + def get_seen_list(self): + return packets.SeenList() + + @rpyc.exposed + def get_log_entries(self): + entries = log_monitor.LogEntries().get_all_and_purge() + return json.dumps(entries, default=str) diff --git a/aprsd/stats.py b/aprsd/stats.py index a5ba446..e52a224 100644 --- a/aprsd/stats.py +++ b/aprsd/stats.py @@ -63,7 +63,7 @@ class APRSDStats: def __new__(cls, *args, **kwargs): if cls._instance is None: cls._instance = super().__new__(cls) - # any initializetion here + # any init here cls._instance.start_time = datetime.datetime.now() cls._instance._aprsis_keepalive = datetime.datetime.now() return cls._instance diff --git a/aprsd/threads/log_monitor.py b/aprsd/threads/log_monitor.py new file mode 100644 index 0000000..90bda55 --- /dev/null +++ b/aprsd/threads/log_monitor.py @@ -0,0 +1,77 @@ +import logging +import threading + +import wrapt + +from aprsd import threads +from aprsd.logging import log + + +LOG = logging.getLogger("APRSD") + + +class LogEntries: + entries = [] + lock = threading.Lock() + _instance = None + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + @wrapt.synchronized(lock) + def add(self, entry): + self.entries.append(entry) + + @wrapt.synchronized(lock) + def get_all_and_purge(self): + entries = self.entries.copy() + self.entries = [] + return entries + + @wrapt.synchronized(lock) + def __len__(self): + return len(self.entries) + + +class LogMonitorThread(threads.APRSDThread): + + def __init__(self): + super().__init__("LogMonitorThread") + + def loop(self): + try: + record = log.logging_queue.get(block=True, timeout=2) + if isinstance(record, list): + for item in record: + entry = self.json_record(item) + LogEntries().add(entry) + else: + entry = self.json_record(record) + LogEntries().add(entry) + except Exception: + # Just ignore thi + pass + + return True + + def json_record(self, record): + entry = {} + entry["filename"] = record.filename + entry["funcName"] = record.funcName + entry["levelname"] = record.levelname + entry["lineno"] = record.lineno + entry["module"] = record.module + entry["name"] = record.name + entry["pathname"] = record.pathname + entry["process"] = record.process + entry["processName"] = record.processName + if hasattr(record, "stack_info"): + entry["stack_info"] = record.stack_info + else: + entry["stack_info"] = None + entry["thread"] = record.thread + entry["threadName"] = record.threadName + entry["message"] = record.getMessage() + return entry diff --git a/aprsd/web/admin/static/js/main.js b/aprsd/web/admin/static/js/main.js index f02ab55..749f3b9 100644 --- a/aprsd/web/admin/static/js/main.js +++ b/aprsd/web/admin/static/js/main.js @@ -107,13 +107,8 @@ function update_packets( data ) { if (size_dict(packet_list) == 0 && size_dict(data) > 0) { packetsdiv.html('') } - console.log("PACKET_LIST") - console.log(packet_list); jQuery.each(data, function(i, val) { pkt = JSON.parse(val); - console.log("PACKET"); - console.log(pkt); - console.log(pkt.timestamp); update_watchlist_from_packet(pkt['from_call'], pkt); if ( packet_list.hasOwnProperty(pkt.timestamp) == false ) { diff --git a/aprsd/web/admin/static/js/send-message.js b/aprsd/web/admin/static/js/send-message.js index 51b71c4..9bcb470 100644 --- a/aprsd/web/admin/static/js/send-message.js +++ b/aprsd/web/admin/static/js/send-message.js @@ -28,36 +28,6 @@ function init_messages() { update_msg(msg); }); - $("#sendform").submit(function(event) { - event.preventDefault(); - - var $checkboxes = $(this).find('input[type=checkbox]'); - - //loop through the checkboxes and change to hidden fields - $checkboxes.each(function() { - if ($(this)[0].checked) { - $(this).attr('type', 'hidden'); - $(this).val(1); - } else { - $(this).attr('type', 'hidden'); - $(this).val(0); - } - }); - - msg = {'from': $('#from').val(), - 'password': $('#password').val(), - 'to': $('#to').val(), - 'message': $('#message').val(), - 'wait_reply': $('#wait_reply').val(), - } - - socket.emit("send", msg); - - //loop through the checkboxes and change to hidden fields - $checkboxes.each(function() { - $(this).attr('type', 'checkbox'); - }); - }); } function add_msg(msg) { diff --git a/aprsd/web/admin/templates/index.html b/aprsd/web/admin/templates/index.html index b7e4f21..ec452bc 100644 --- a/aprsd/web/admin/templates/index.html +++ b/aprsd/web/admin/templates/index.html @@ -82,7 +82,6 @@
Watch List
Plugins
Config
-
Send Message
LogFile
Raw JSON
@@ -160,29 +159,6 @@
{{ config_json|safe }}
-
-

Send Message

-
-
-

-

-

-

-

-

-

-

-

- -

- -
-
-
Messages
-
-
-
-

LOGFILE

diff --git a/aprsd/web/admin/templates/messages.html b/aprsd/web/admin/templates/messages.html deleted file mode 100644 index c3f6beb..0000000 --- a/aprsd/web/admin/templates/messages.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - -

-
-    
-
-
diff --git a/aprsd/web/admin/templates/send-message.html b/aprsd/web/admin/templates/send-message.html
deleted file mode 100644
index 566eaba..0000000
--- a/aprsd/web/admin/templates/send-message.html
+++ /dev/null
@@ -1,74 +0,0 @@
-
-    
-        
-        
-        
-        
-        
-
-
-        
-        
-        
-
-        
-        
-
-        
-        
-        
-
-        
-
-    
-
-    
-        
-

APRSD {{ version }}

-
- -
-
- {{ callsign }} - connected to - NONE -
- -
- NONE -
-
- -

Send Message Form

-
-

-

-

-

- -

-

- -

-

- -

- -

- - -
- -

Messages (0)

-
-
Messages
-
- - - - - diff --git a/dev-requirements.txt b/dev-requirements.txt index 9d3412f..16e5a38 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -49,7 +49,7 @@ pep8-naming==0.13.3 # via -r dev-requirements.in pip-tools==6.12.1 # via -r dev-requirements.in platformdirs==2.6.0 # via black, tox, virtualenv pluggy==1.0.0 # via pytest, tox -pre-commit==2.20.0 # via -r dev-requirements.in +pre-commit==2.21.0 # via -r dev-requirements.in pycodestyle==2.10.0 # via flake8 pyflakes==3.0.1 # via autoflake, flake8 pygments==2.13.0 # via rich, sphinx @@ -71,9 +71,9 @@ sphinxcontrib-jsmath==1.0.1 # via sphinx sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.5 # via sphinx tokenize-rt==5.0.0 # via add-trailing-comma, pyupgrade -toml==0.10.2 # via autoflake, pre-commit +toml==0.10.2 # via autoflake tomli==2.0.1 # via black, build, coverage, mypy, pep517, pyproject-api, pytest, tox -tox==4.0.16 # via -r dev-requirements.in +tox==4.0.18 # via -r dev-requirements.in typing-extensions==4.4.0 # via libcst, mypy, typing-inspect typing-inspect==0.8.0 # via libcst unify==0.5 # via gray diff --git a/requirements.in b/requirements.in index aec3c2b..0ce8236 100644 --- a/requirements.in +++ b/requirements.in @@ -23,10 +23,11 @@ beautifulsoup4 wrapt # kiss3 uses attrs kiss3 -attrs==22.1.0 +attrs # for mobile checking user-agents pyopenssl dataclasses dacite2 oslo.config +rpyc diff --git a/requirements.txt b/requirements.txt index 5830f74..6ed3153 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,6 +39,7 @@ oslo-config==9.0.0 # via -r requirements.in oslo-i18n==5.1.0 # via oslo-config pbr==5.11.0 # via -r requirements.in, oslo-i18n, stevedore pluggy==1.0.0 # via -r requirements.in +plumbum==1.8.0 # via rpyc pycparser==2.21 # via cffi pygments==2.13.0 # via rich pyopenssl==22.1.0 # via -r requirements.in @@ -51,6 +52,7 @@ pyyaml==6.0 # via -r requirements.in, oslo-config requests==2.28.1 # via -r requirements.in, oslo-config, update-checker rfc3986==2.0.0 # via oslo-config rich==12.6.0 # via -r requirements.in +rpyc==5.3.0 # via -r requirements.in shellingham==1.5.0 # via click-completion six==1.16.0 # via -r requirements.in, click-completion, eventlet, imapclient soupsieve==2.3.2.post1 # via beautifulsoup4 From c929689647159804209f1bd3711a563ba395cb18 Mon Sep 17 00:00:00 2001 From: Hemna Date: Wed, 28 Dec 2022 16:50:34 -0500 Subject: [PATCH 7/7] Update documentation and README This updates the documentation in prep for 3.0.0 --- ChangeLog | 6 + Makefile | 2 +- README.rst | 205 +++++----------- aprsd/cli_helper.py | 8 +- docs/apidoc/aprsd.plugins.rst | 8 - docs/apidoc/aprsd.rst | 118 +++------- docs/changelog.rst | 330 ++++++++++++++++++++++++++ docs/conf.py | 2 +- docs/configure.rst | 348 +++++++++++++++++++++++---- docs/plugin.rst | 6 +- docs/readme.rst | 431 +++++++++++++++------------------- 11 files changed, 938 insertions(+), 526 deletions(-) diff --git a/ChangeLog b/ChangeLog index b6c6378..93b31c3 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,12 @@ CHANGES ======= +* Decouple admin web interface from server command +* Dockerfile now produces aprsd.conf +* Fix some unit tests and loading of CONF w/o file +* Added missing conf +* Removed references to old custom config +* Convert config to oslo\_config * Added rain formatting unit tests to WeatherPacket * Fix Rain reporting in WeatherPacket send * Removed Packet.send() diff --git a/Makefile b/Makefile index 22d468c..0063236 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ dev: venv ## Create a python virtual environment for development of aprsd run: venv ## Create a virtual environment for running aprsd commands -docs: build +docs: dev cp README.rst docs/readme.rst cp Changelog docs/changelog.rst tox -edocs diff --git a/README.rst b/README.rst index 7390700..caaf8b1 100644 --- a/README.rst +++ b/README.rst @@ -52,39 +52,49 @@ Current list of built-in plugins :: โ””โ”€> aprsd list-plugins - ๐Ÿ APRSD Built-in Plugins ๐Ÿ - โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“ - โ”ƒ Plugin Name โ”ƒ Info โ”ƒ Type โ”ƒ Plugin Path โ”ƒ - โ”กโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ฉ - โ”‚ AVWXWeatherPlugin โ”‚ AVWX weather of GPS Beacon location โ”‚ RegexCommand โ”‚ aprsd.plugins.weather.AVWXWeatherPlugin โ”‚ - โ”‚ EmailPlugin โ”‚ Send and Receive email โ”‚ RegexCommand โ”‚ aprsd.plugins.email.EmailPlugin โ”‚ - โ”‚ FortunePlugin โ”‚ Give me a fortune โ”‚ RegexCommand โ”‚ aprsd.plugins.fortune.FortunePlugin โ”‚ - โ”‚ LocationPlugin โ”‚ Where in the world is a CALLSIGN's last GPS beacon? โ”‚ RegexCommand โ”‚ aprsd.plugins.location.LocationPlugin โ”‚ - โ”‚ NotifySeenPlugin โ”‚ Notify me when a CALLSIGN is recently seen on APRS-IS โ”‚ WatchList โ”‚ aprsd.plugins.notify.NotifySeenPlugin โ”‚ - โ”‚ OWMWeatherPlugin โ”‚ OpenWeatherMap weather of GPS Beacon location โ”‚ RegexCommand โ”‚ aprsd.plugins.weather.OWMWeatherPlugin โ”‚ - โ”‚ PingPlugin โ”‚ reply with a Pong! โ”‚ RegexCommand โ”‚ aprsd.plugins.ping.PingPlugin โ”‚ - โ”‚ QueryPlugin โ”‚ APRSD Owner command to query messages in the MsgTrack โ”‚ RegexCommand โ”‚ aprsd.plugins.query.QueryPlugin โ”‚ - โ”‚ TimeOWMPlugin โ”‚ Current time of GPS beacon's timezone. Uses OpenWeatherMap โ”‚ RegexCommand โ”‚ aprsd.plugins.time.TimeOWMPlugin โ”‚ - โ”‚ TimeOpenCageDataPlugin โ”‚ Current time of GPS beacon timezone. Uses OpenCage โ”‚ RegexCommand โ”‚ aprsd.plugins.time.TimeOpenCageDataPlugin โ”‚ - โ”‚ TimePlugin โ”‚ What is the current local time. โ”‚ RegexCommand โ”‚ aprsd.plugins.time.TimePlugin โ”‚ - โ”‚ USMetarPlugin โ”‚ USA only METAR of GPS Beacon location โ”‚ RegexCommand โ”‚ aprsd.plugins.weather.USMetarPlugin โ”‚ - โ”‚ USWeatherPlugin โ”‚ Provide USA only weather of GPS Beacon location โ”‚ RegexCommand โ”‚ aprsd.plugins.weather.USWeatherPlugin โ”‚ - โ”‚ VersionPlugin โ”‚ What is the APRSD Version โ”‚ RegexCommand โ”‚ aprsd.plugins.version.VersionPlugin โ”‚ - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + ๐Ÿ APRSD Built-in Plugins ๐Ÿ + โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“ + โ”ƒ Plugin Name โ”ƒ Info โ”ƒ Type โ”ƒ Plugin Path โ”ƒ + โ”กโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ฉ + โ”‚ AVWXWeatherPlugin โ”‚ AVWX weather of GPS Beacon location โ”‚ RegexCommand โ”‚ aprsd.plugins.weather.AVWXWeatherPlugin โ”‚ + โ”‚ EmailPlugin โ”‚ Send and Receive email โ”‚ RegexCommand โ”‚ aprsd.plugins.email.EmailPlugin โ”‚ + โ”‚ FortunePlugin โ”‚ Give me a fortune โ”‚ RegexCommand โ”‚ aprsd.plugins.fortune.FortunePlugin โ”‚ + โ”‚ LocationPlugin โ”‚ Where in the world is a CALLSIGN's last GPS beacon? โ”‚ RegexCommand โ”‚ aprsd.plugins.location.LocationPlugin โ”‚ + โ”‚ NotifySeenPlugin โ”‚ Notify me when a CALLSIGN is recently seen on APRS-IS โ”‚ WatchList โ”‚ aprsd.plugins.notify.NotifySeenPlugin โ”‚ + โ”‚ OWMWeatherPlugin โ”‚ OpenWeatherMap weather of GPS Beacon location โ”‚ RegexCommand โ”‚ aprsd.plugins.weather.OWMWeatherPlugin โ”‚ + โ”‚ PingPlugin โ”‚ reply with a Pong! โ”‚ RegexCommand โ”‚ aprsd.plugins.ping.PingPlugin โ”‚ + โ”‚ QueryPlugin โ”‚ APRSD Owner command to query messages in the MsgTrack โ”‚ RegexCommand โ”‚ aprsd.plugins.query.QueryPlugin โ”‚ + โ”‚ TimeOWMPlugin โ”‚ Current time of GPS beacon's timezone. Uses OpenWeatherMap โ”‚ RegexCommand โ”‚ aprsd.plugins.time.TimeOWMPlugin โ”‚ + โ”‚ TimePlugin โ”‚ What is the current local time. โ”‚ RegexCommand โ”‚ aprsd.plugins.time.TimePlugin โ”‚ + โ”‚ USMetarPlugin โ”‚ USA only METAR of GPS Beacon location โ”‚ RegexCommand โ”‚ aprsd.plugins.weather.USMetarPlugin โ”‚ + โ”‚ USWeatherPlugin โ”‚ Provide USA only weather of GPS Beacon location โ”‚ RegexCommand โ”‚ aprsd.plugins.weather.USWeatherPlugin โ”‚ + โ”‚ VersionPlugin โ”‚ What is the APRSD Version โ”‚ RegexCommand โ”‚ aprsd.plugins.version.VersionPlugin โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - Pypi.org APRSD Installable Plugin Packages + Pypi.org APRSD Installable Plugin Packages - Install any of the following plugins with pip install - โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“ - โ”ƒ Plugin Package Name โ”ƒ Description โ”ƒ Version โ”ƒ Released โ”ƒ Installed? โ”ƒ - โ”กโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ฉ - โ”‚ ๐Ÿ“‚ aprsd-stock-plugin โ”‚ Ham Radio APRSD Plugin for fetching stock quotes โ”‚ 0.1.2 โ”‚ Nov 9, 2021 โ”‚ No โ”‚ - โ”‚ ๐Ÿ“‚ aprsd-weewx-plugin โ”‚ HAM Radio APRSD that reports weather from a weewx weather station. โ”‚ 0.1.4 โ”‚ Dec 7, 2021 โ”‚ No โ”‚ - โ”‚ ๐Ÿ“‚ aprsd-telegram-plugin โ”‚ Ham Radio APRS APRSD plugin for Telegram IM service โ”‚ 0.1.2 โ”‚ Nov 9, 2021 โ”‚ No โ”‚ - โ”‚ ๐Ÿ“‚ aprsd-twitter-plugin โ”‚ Python APRSD plugin to send tweets โ”‚ 0.3.0 โ”‚ Dec 7, 2021 โ”‚ No โ”‚ - โ”‚ ๐Ÿ“‚ aprsd-slack-plugin โ”‚ Amateur radio APRS daemon which listens for messages and responds โ”‚ 1.0.4 โ”‚ Jan 15, 2021 โ”‚ No โ”‚ - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + Install any of the following plugins with 'pip install ' + โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“ + โ”ƒ Plugin Package Name โ”ƒ Description โ”ƒ Version โ”ƒ Released โ”ƒ Installed? โ”ƒ + โ”กโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ฉ + โ”‚ ๐Ÿ“‚ aprsd-stock-plugin โ”‚ Ham Radio APRSD Plugin for fetching stock quotes โ”‚ 0.1.3 โ”‚ Dec 2, 2022 โ”‚ No โ”‚ + โ”‚ ๐Ÿ“‚ aprsd-sentry-plugin โ”‚ Ham radio APRSD plugin that does.... โ”‚ 0.1.2 โ”‚ Dec 2, 2022 โ”‚ No โ”‚ + โ”‚ ๐Ÿ“‚ aprsd-timeopencage-plugin โ”‚ APRSD plugin for fetching time based on GPS location โ”‚ 0.1.0 โ”‚ Dec 2, 2022 โ”‚ No โ”‚ + โ”‚ ๐Ÿ“‚ aprsd-weewx-plugin โ”‚ HAM Radio APRSD that reports weather from a weewx weather station. โ”‚ 0.1.4 โ”‚ Dec 7, 2021 โ”‚ Yes โ”‚ + โ”‚ ๐Ÿ“‚ aprsd-repeat-plugins โ”‚ APRSD Plugins for the REPEAT service โ”‚ 1.0.12 โ”‚ Dec 2, 2022 โ”‚ No โ”‚ + โ”‚ ๐Ÿ“‚ aprsd-telegram-plugin โ”‚ Ham Radio APRS APRSD plugin for Telegram IM service โ”‚ 0.1.3 โ”‚ Dec 2, 2022 โ”‚ No โ”‚ + โ”‚ ๐Ÿ“‚ aprsd-twitter-plugin โ”‚ Python APRSD plugin to send tweets โ”‚ 0.3.0 โ”‚ Dec 7, 2021 โ”‚ No โ”‚ + โ”‚ ๐Ÿ“‚ aprsd-slack-plugin โ”‚ Amateur radio APRS daemon which listens for messages and responds โ”‚ 1.0.5 โ”‚ Dec 18, 2022 โ”‚ No โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + + ๐Ÿ APRSD Installed 3rd party Plugins ๐Ÿ + โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“ + โ”ƒ Package Name โ”ƒ Plugin Name โ”ƒ Version โ”ƒ Type โ”ƒ Plugin Path โ”ƒ + โ”กโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ฉ + โ”‚ aprsd-weewx-plugin โ”‚ WeewxMQTTPlugin โ”‚ 1.0 โ”‚ RegexCommand โ”‚ aprsd_weewx_plugin.weewx.WeewxMQTTPlugin โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Installation ============= @@ -99,10 +109,10 @@ Example usage ``aprsd -h`` Help ----- +==== :: - โ””โ”€[$] > aprsd -h + โ””โ”€> aprsd -h Usage: aprsd [OPTIONS] COMMAND [ARGS]... Options: @@ -116,18 +126,19 @@ Help healthcheck Check the health of the running aprsd server. list-plugins List the built in plugins available to APRSD. listen Listen to packets on the APRS-IS Network based on FILTER. - sample-config This dumps the config to stdout. + sample-config Generate a sample Config file from aprsd and all... send-message Send a message to a callsign via APRS_IS. server Start the aprsd server gateway process. version Show the APRSD version. + webchat Web based HAM Radio chat program! Commands --------- +======== Configuration -^^^^^^^^^^^^^ +============= This command outputs a sample config yml formatted block that you can edit and use to pass in to ``aprsd`` with ``-c``. By default aprsd looks in ``~/.config/aprsd/aprsd.yml`` @@ -136,108 +147,10 @@ and use to pass in to ``aprsd`` with ``-c``. By default aprsd looks in ``~/.con :: โ””โ”€> aprsd sample-config - aprs: - # Set enabled to False if there is no internet connectivity. - # This is useful for a direwolf KISS aprs connection only. - - # Get the passcode for your callsign here: - # https://apps.magicbug.co.uk/passcode - enabled: true - host: rotate.aprs2.net - login: CALLSIGN - password: '00000' - port: 14580 - aprsd: - dateformat: '%m/%d/%Y %I:%M:%S %p' - email: - enabled: true - imap: - debug: false - host: imap.gmail.com - login: IMAP_USERNAME - password: IMAP_PASSWORD - port: 993 - use_ssl: true - shortcuts: - aa: 5551239999@vtext.com - cl: craiglamparter@somedomain.org - wb: 555309@vtext.com - smtp: - debug: false - host: smtp.gmail.com - login: SMTP_USERNAME - password: SMTP_PASSWORD - port: 465 - use_ssl: false - enabled_plugins: - - aprsd.plugins.email.EmailPlugin - - aprsd.plugins.fortune.FortunePlugin - - aprsd.plugins.location.LocationPlugin - - aprsd.plugins.ping.PingPlugin - - aprsd.plugins.query.QueryPlugin - - aprsd.plugins.stock.StockPlugin - - aprsd.plugins.time.TimePlugin - - aprsd.plugins.weather.USWeatherPlugin - - aprsd.plugins.version.VersionPlugin - logfile: /tmp/aprsd.log - logformat: '[%(asctime)s] [%(threadName)-20.20s] [%(levelname)-5.5s] %(message)s - - [%(pathname)s:%(lineno)d]' - rich_logging: false - save_location: /Users/i530566/.config/aprsd/ - trace: false - units: imperial - watch_list: - alert_callsign: NOCALL - alert_time_seconds: 43200 - callsigns: [] - enabled: false - enabled_plugins: - - aprsd.plugins.notify.NotifySeenPlugin - packet_keep_count: 10 - web: - enabled: true - host: 0.0.0.0 - logging_enabled: true - port: 8001 - users: - admin: password-here - ham: - callsign: NOCALL - kiss: - serial: - baudrate: 9600 - device: /dev/ttyS0 - enabled: false - tcp: - enabled: false - host: direwolf.ip.address - port: '8001' - services: - aprs.fi: - # Get the apiKey from your aprs.fi account here: - # http://aprs.fi/account - apiKey: APIKEYVALUE - avwx: - # (Optional for AVWXWeatherPlugin) - # Use hosted avwx-api here: https://avwx.rest - # or deploy your own from here: - # https://github.com/avwx-rest/avwx-api - apiKey: APIKEYVALUE - base_url: http://host:port - opencagedata: - # (Optional for TimeOpenCageDataPlugin) - # Get the apiKey from your opencagedata account here: - # https://opencagedata.com/dashboard#api-keys - apiKey: APIKEYVALUE - openweathermap: - # (Optional for OWMWeatherPlugin) - # Get the apiKey from your - # openweathermap account here: - # https://home.openweathermap.org/api_keys - apiKey: APIKEYVALUE + ... server -^^^^^^ +====== This is the main server command that will listen to APRS-IS servers and look for incomming commands to the callsign configured in the config file @@ -277,7 +190,7 @@ look for incomming commands to the callsign configured in the config file send-message -^^^^^^^^^^^^ +============ This command is typically used for development to send another aprsd instance test messages @@ -310,7 +223,7 @@ test messages SEND EMAIL (radio to smtp server) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +================================= :: @@ -332,7 +245,7 @@ SEND EMAIL (radio to smtp server) RECEIVE EMAIL (imap server to radio) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +==================================== :: @@ -349,7 +262,7 @@ RECEIVE EMAIL (imap server to radio) LOCATION -^^^^^^^^ +======== :: @@ -384,7 +297,7 @@ Development * ``make`` Workflow --------- +======== While working aprsd, The workflow is as follows: @@ -413,7 +326,7 @@ While working aprsd, The workflow is as follows: Release -------- +======= To do release to pypi: @@ -435,10 +348,10 @@ To do release to pypi: Docker Container ----------------- +================ Building -^^^^^^^^ +======== There are 2 versions of the container Dockerfile that can be used. The main Dockerfile, which is for building the official release container @@ -447,18 +360,18 @@ which is used for building a container based off of a git branch of the repo. Official Build -^^^^^^^^^^^^^^ +============== ``docker build -t hemna6969/aprsd:latest .`` Development Build -^^^^^^^^^^^^^^^^^ +================= ``docker build -t hemna6969/aprsd:latest -f Dockerfile-dev .`` Running the container -^^^^^^^^^^^^^^^^^^^^^ +===================== There is a ``docker-compose.yml`` file in the ``docker/`` directory that can be used to run your container. To provide the container diff --git a/aprsd/cli_helper.py b/aprsd/cli_helper.py index ed8acce..4dbf727 100644 --- a/aprsd/cli_helper.py +++ b/aprsd/cli_helper.py @@ -1,4 +1,5 @@ from functools import update_wrapper +import logging from pathlib import Path import typing as t @@ -61,6 +62,7 @@ def process_standard_options(f: F) -> F: def new_func(*args, **kwargs): ctx = args[0] ctx.ensure_object(dict) + config_file_found = True if kwargs["config_file"]: default_config_files = [kwargs["config_file"]] else: @@ -71,7 +73,7 @@ def process_standard_options(f: F) -> F: default_config_files=default_config_files, ) except cfg.ConfigFilesNotFoundError: - pass + config_file_found = False ctx.obj["loglevel"] = kwargs["loglevel"] # ctx.obj["config_file"] = kwargs["config_file"] ctx.obj["quiet"] = kwargs["quiet"] @@ -82,6 +84,10 @@ def process_standard_options(f: F) -> F: if CONF.trace_enabled: trace.setup_tracing(["method", "api"]) + if not config_file_found: + LOG = logging.getLogger("APRSD") # noqa: N806 + LOG.error("No config file found!! run 'aprsd sample-config'") + del kwargs["loglevel"] del kwargs["config_file"] del kwargs["quiet"] diff --git a/docs/apidoc/aprsd.plugins.rst b/docs/apidoc/aprsd.plugins.rst index 5b7ace6..8454f24 100644 --- a/docs/apidoc/aprsd.plugins.rst +++ b/docs/apidoc/aprsd.plugins.rst @@ -52,14 +52,6 @@ aprsd.plugins.query module :undoc-members: :show-inheritance: -aprsd.plugins.stock module --------------------------- - -.. automodule:: aprsd.plugins.stock - :members: - :undoc-members: - :show-inheritance: - aprsd.plugins.time module ------------------------- diff --git a/docs/apidoc/aprsd.rst b/docs/apidoc/aprsd.rst index 7a12817..5a1eb5a 100644 --- a/docs/apidoc/aprsd.rst +++ b/docs/apidoc/aprsd.rst @@ -7,11 +7,35 @@ Subpackages .. toctree:: :maxdepth: 4 + aprsd.clients + aprsd.cmds + aprsd.conf + aprsd.logging + aprsd.packets aprsd.plugins + aprsd.threads + aprsd.utils + aprsd.web Submodules ---------- +aprsd.aprsd module +------------------ + +.. automodule:: aprsd.aprsd + :members: + :undoc-members: + :show-inheritance: + +aprsd.cli\_helper module +------------------------ + +.. automodule:: aprsd.cli_helper + :members: + :undoc-members: + :show-inheritance: + aprsd.client module ------------------- @@ -20,18 +44,10 @@ aprsd.client module :undoc-members: :show-inheritance: -aprsd.dev module ----------------- +aprsd.exception module +---------------------- -.. automodule:: aprsd.dev - :members: - :undoc-members: - :show-inheritance: - -aprsd.fake\_aprs module ------------------------ - -.. automodule:: aprsd.fake_aprs +.. automodule:: aprsd.exception :members: :undoc-members: :show-inheritance: @@ -44,46 +60,6 @@ aprsd.flask module :undoc-members: :show-inheritance: -aprsd.fuzzyclock module ------------------------ - -.. automodule:: aprsd.fuzzyclock - :members: - :undoc-members: - :show-inheritance: - -aprsd.healthcheck module ------------------------- - -.. automodule:: aprsd.healthcheck - :members: - :undoc-members: - :show-inheritance: - -aprsd.kissclient module ------------------------ - -.. automodule:: aprsd.kissclient - :members: - :undoc-members: - :show-inheritance: - -aprsd.listen module -------------------- - -.. automodule:: aprsd.listen - :members: - :undoc-members: - :show-inheritance: - -aprsd.main module ------------------ - -.. automodule:: aprsd.main - :members: - :undoc-members: - :show-inheritance: - aprsd.messaging module ---------------------- @@ -92,14 +68,6 @@ aprsd.messaging module :undoc-members: :show-inheritance: -aprsd.packets module --------------------- - -.. automodule:: aprsd.packets - :members: - :undoc-members: - :show-inheritance: - aprsd.plugin module ------------------- @@ -116,6 +84,14 @@ aprsd.plugin\_utils module :undoc-members: :show-inheritance: +aprsd.rpc\_server module +------------------------ + +.. automodule:: aprsd.rpc_server + :members: + :undoc-members: + :show-inheritance: + aprsd.stats module ------------------ @@ -124,30 +100,6 @@ aprsd.stats module :undoc-members: :show-inheritance: -aprsd.threads module --------------------- - -.. automodule:: aprsd.threads - :members: - :undoc-members: - :show-inheritance: - -aprsd.trace module ------------------- - -.. automodule:: aprsd.trace - :members: - :undoc-members: - :show-inheritance: - -aprsd.utils module ------------------- - -.. automodule:: aprsd.utils - :members: - :undoc-members: - :show-inheritance: - Module contents --------------- diff --git a/docs/changelog.rst b/docs/changelog.rst index a4673d7..93b31c3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,9 +1,339 @@ CHANGES ======= +* Decouple admin web interface from server command +* Dockerfile now produces aprsd.conf +* Fix some unit tests and loading of CONF w/o file +* Added missing conf +* Removed references to old custom config +* Convert config to oslo\_config +* Added rain formatting unit tests to WeatherPacket +* Fix Rain reporting in WeatherPacket send +* Removed Packet.send() +* Removed watchlist plugins +* Fix PluginManager.get\_plugins +* Cleaned up PluginManager +* Cleaned up PluginManager +* Update routing for weatherpacket +* Fix some WeatherPacket formatting +* Fix pep8 violation +* Add packet filtering for aprsd listen +* Added WeatherPacket encoding +* Updated webchat and listen for queue based RX +* reworked collecting and reporting stats +* Removed unused threading code +* Change RX packet processing to enqueu +* Make tracking objectstores work w/o initializing +* Cleaned up packet transmit class attributes +* Fix packets timestamp to int +* More messaging -> packets cleanup +* Cleaned out all references to messaging +* Added contructing a GPSPacket for sending +* cleanup webchat +* Reworked all packet processing +* Updated plugins and plugin interfaces for Packet +* Started using dataclasses to describe packets + +v2.6.1 +------ + +* v2.6.1 +* Fixed position report for webchat beacon +* Try and fix broken 32bit qemu builds on 64bit system +* Add unit tests for webchat +* remove armv7 build RUST sucks +* Fix for Collections change in 3.10 + +v2.6.0 +------ + +* Update workflow again +* Update Dockerfile to 22.04 +* Update Dockerfile and build.sh +* Update workflow +* Prep for 2.6.0 release +* Update requirements +* Removed Makefile comment +* Update Makefile for dev vs. run environments +* Added pyopenssl for https for webchat +* change from device-detector to user-agents +* Remove twine from dev-requirements +* Update to latest Makefile.venv +* Refactored threads a bit +* Mark packets as acked in MsgTracker +* remove dev setting for template +* Add GPS beacon to mobile page +* Allow werkzeug for admin interface +* Allow werkzeug for admin interface +* Add support for mobile browsers for webchat +* Ignore callsign case while processing packets +* remove linux/arm/v7 for official builds for now +* added workflow for building specific version +* Allow passing in version to the Dockerfile +* Send GPS Beacon from webchat interface +* specify Dockerfile-dev +* Fixed build.sh +* Build on the source not released aprsd +* Remove email validation +* Add support for building linux/arm/v7 +* Remove python 3.7 from docker build github +* Fixed failing unit tests +* change github workflow +* Removed TimeOpenCageDataPlugin +* Dump config with aprsd dev test-plugin +* Updated requirements +* Got webchat working with KISS tcp +* Added click auto\_envvar\_prefix +* Update aprsd thread base class to use queue +* Update packets to use wrapt +* Add remving existing requirements +* Try sending raw APRSFrames to aioax25 +* Use new aprsd.callsign as the main callsign +* Fixed access to threads refactor +* Added webchat command +* Moved log.py to logging +* Moved trace.py to utils +* Fixed pep8 errors +* Refactored threads.py +* Refactor utils to directory +* remove arm build for now +* Added rustc and cargo to Dockerfile +* remove linux/arm/v6 from docker platform build +* Only tag master build as master +* Remove docker build from test +* create master-build.yml +* Added container build action +* Update docs on using Docker +* Update dev-requirements pip-tools +* Fix typo in docker-compose.yml +* Fix PyPI scraping +* Allow web interface when running in Docker +* Fix typo on exception +* README formatting fixes +* Bump dependencies to fix python 3.10 +* Fixed up config option checking for KISS +* Fix logging issue with log messages +* for 2.5.9 + +v2.5.9 +------ + +* FIX: logging exceptions +* Updated build and run for rich lib +* update build for 2.5.8 + +v2.5.8 +------ + +* For 2.5.8 +* Removed debug code +* Updated list-plugins +* Renamed virtualenv dir to .aprsd-venv +* Added unit tests for dev test-plugin +* Send Message command defaults to config + +v2.5.7 +------ + +* Updated Changelog +* Fixed an KISS config disabled issue +* Fixed a bug with multiple notify plugins enabled +* Unify the logging to file and stdout +* Added new feature to list-plugins command +* more README.rst cleanup +* Updated README examples + +v2.5.6 +------ + +* Changelog +* Tightened up the packet logging +* Added unit tests for USWeatherPlugin, USMetarPlugin +* Added test\_location to test LocationPlugin +* Updated pytest output +* Added py39 to tox for tests +* Added NotifyPlugin unit tests and more +* Small cleanup on packet logging +* Reduced the APRSIS connection reset to 2 minutes +* Fixed the NotifyPlugin +* Fixed some pep8 errors +* Add tracing for dev command +* Added python rich library based logging +* Added LOG\_LEVEL env variable for the docker + +v2.5.5 +------ + +* Update requirements to use aprslib 0.7.0 +* fixed the failure during loading for objectstore +* updated docker build + +v2.5.4 +------ + +* Updated Changelog +* Fixed dev command missing initialization + +v2.5.3 +------ + +* Fix admin logging tab + +v2.5.2 +------ + +* Added new list-plugins command +* Don't require check-version command to have a config +* Healthcheck command doesn't need the aprsd.yml config +* Fix test failures +* Removed requirement for aprs.fi key +* Updated Changelog + +v2.5.1 +------ + +* Removed stock plugin +* Removed the stock plugin + +v2.5.0 +------ + +* Updated for v2.5.0 +* Updated Dockerfile's and build script for docker +* Cleaned up some verbose output & colorized output +* Reworked all the common arguments +* Fixed test-plugin +* Ensure common params are honored +* pep8 +* Added healthcheck to the cmds +* Removed the need for FROMCALL in dev test-plugin +* Pep8 failures +* Refactor the cli +* Updated Changelog for 4.2.3 +* Fixed a problem with send-message command + +v2.4.2 +------ + +* Updated Changelog +* Be more careful picking data to/from disk +* Updated Changelog + +v2.4.1 +------ + +* Ensure plugins are last to be loaded +* Fixed email connecting to smtp server + +v2.4.0 +------ + +* Updated Changelog for 2.4.0 release +* Converted MsgTrack to ObjectStoreMixin +* Fixed unit tests +* Make sure SeenList update has a from in packet +* Ensure PacketList is initialized +* Added SIGTERM to signal\_handler +* Enable configuring where to save the objectstore data +* PEP8 cleanup +* Added objectstore Mixin +* Added -num option to aprsd-dev test-plugin +* Only call stop\_threads if it exists +* Added new SeenList +* Added plugin version to stats reporting +* Added new HelpPlugin +* Updated aprsd-dev to use config for logfile format +* Updated build.sh +* removed usage of config.check\_config\_option +* Fixed send-message after config/client rework +* Fixed issue with flask config +* Added some server startup info logs +* Increase email delay to +10 +* Updated dev to use plugin manager +* Fixed notify plugins +* Added new Config object +* Fixed email plugin's use of globals +* Refactored client classes +* Refactor utils usage +* 2.3.1 Changelog + +v2.3.1 +------ + +* Fixed issue of aprs-is missing keepalive +* Fixed packet processing issue with aprsd send-message + +v2.3.0 +------ + +* Prep 2.3.0 +* Enable plugins to return message object +* Added enabled flag for every plugin object +* Ensure plugin threads are valid +* Updated Dockerfile to use v2.3.0 +* Removed fixed size on logging queue +* Added Logfile tab in Admin ui +* Updated Makefile clean target +* Added self creating Makefile help target +* Update dev.py +* Allow passing in aprsis\_client +* Fixed a problem with the AVWX plugin not working +* Remove some noisy trace in email plugin +* Fixed issue at startup with notify plugin +* Fixed email validation +* Removed values from forms +* Added send-message to the main admin UI +* Updated requirements +* Cleaned up some pep8 failures +* Upgraded the send-message POC to use websockets +* New Admin ui send message page working +* Send Message via admin Web interface +* Updated Admin UI to show KISS connections +* Got TX/RX working with aioax25+direwolf over TCP +* Rebased from master +* Added the ability to use direwolf KISS socket +* Update Dockerfile to use 2.2.1 + +v2.2.1 +------ + +* Update Changelog for 2.2.1 +* Silence some log noise + +v2.2.0 +------ + +* Updated Changelog for v2.2.0 +* Updated overview image +* Removed Black code style reference +* Removed TXThread +* Added days to uptime string formatting +* Updated select timeouts +* Rebase from master and run gray +* Added tracking plugin processing +* Added threads functions to APRSDPluginBase +* Refactor Message processing and MORE +* Use Gray instead of Black for code formatting +* Updated tox.ini +* Fixed LOG.debug issue in weather plugin +* Updated slack channel link +* Cleanup of the README.rst +* Fixed aprsd-dev + +v2.1.0 +------ + +* Prep for v2.1.0 +* Enable multiple replies for plugins +* Put in a fix for aprslib parse exceptions +* Fixed time plugin +* Updated the charts Added the packets chart +* Added showing symbol images to watch list + v2.0.0 ------ +* Updated docs for 2.0.0 * Reworked the notification threads and admin ui * Fixed small bug with packets get\_packet\_type * Updated overview images diff --git a/docs/conf.py b/docs/conf.py index 9e2a75c..1c0b547 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -64,7 +64,7 @@ master_doc = "index" # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. diff --git a/docs/configure.rst b/docs/configure.rst index dfb2a6c..d1ba824 100644 --- a/docs/configure.rst +++ b/docs/configure.rst @@ -15,10 +15,11 @@ a sample config file in the default location of ~/.config/aprsd/aprsd.yml .. code-block:: shell - โ””โ”€[$] -> aprsd server - Load config - /home/aprsd/.config/aprsd/aprsd.yml is missing, creating config file - Default config file created at /home/aprsd/.config/aprsd/aprsd.yml. Please edit with your settings. + โ””โ”€> aprsd server + 12/28/2022 04:26:31 PM MainThread ERROR No config file found!! run 'aprsd sample-config' cli_helper.py:90 + 12/28/2022 04:26:31 PM MainThread ERROR Config aprs_network.password not set. client.py:105 + 12/28/2022 04:26:31 PM MainThread ERROR Option 'aprs_network.password is not set.' was not in config file client.py:268 + 12/28/2022 04:26:31 PM MainThread ERROR APRS client is not properly configured in config file. server.py:58 You can see the sample config file output @@ -27,43 +28,310 @@ Sample config file .. code-block:: shell - โ””โ”€[$] -> cat ~/.config/aprsd/aprsd.yml - aprs: - host: rotate.aprs.net - logfile: /tmp/arsd.log - login: someusername - password: somepassword - port: 14580 - aprsd: - enabled_plugins: - - aprsd.plugins.email.EmailPlugin - - aprsd.plugins.fortune.FortunePlugin - - aprsd.plugins.location.LocationPlugin - - aprsd.plugins.ping.PingPlugin - - aprsd.plugins.query.QueryPlugin - - aprsd.plugins.time.TimePlugin - - aprsd.plugins.weather.WeatherPlugin - - aprsd.plugins.version.VersionPlugin - plugin_dir: ~/.config/aprsd/plugins - ham: - callsign: KFART - imap: - host: imap.gmail.com - login: imapuser - password: something here too - port: 993 - use_ssl: true - shortcuts: - aa: 5551239999@vtext.com - cl: craiglamparter@somedomain.org - wb: 555309@vtext.com - smtp: - host: imap.gmail.com - login: something - password: some lame password - port: 465 - use_ssl: false + โ””โ”€> aprsd sample-config + [DEFAULT] + # + # From aprsd.conf + # + + # Callsign to use for messages sent by APRSD (string value) + #callsign = + + # Enable saving of watch list, packet tracker between restarts. + # (boolean value) + #enable_save = true + + # Save location for packet tracking files. (string value) + #save_location = ~/.config/aprsd + + # Enable code tracing (boolean value) + #trace_enabled = false + + # Units for display, imperial or metric (string value) + #units = imperial + + # Comma separated list of enabled plugins for APRSD.To enable + # installed external plugins add them here.The full python path to the + # class name must be used (list value) + #enabled_plugins = aprsd.plugins.email.EmailPlugin,aprsd.plugins.fortune.FortunePlugin,aprsd.plugins.location.LocationPlugin,aprsd.plugins.ping.PingPlugin,aprsd.plugins.query.QueryPlugin,aprsd.plugins.time.TimePlugin,aprsd.plugins.weather.OWMWeatherPlugin,aprsd.plugins.version.VersionPlugin + + + [admin] + + # + # From aprsd.conf + # + + # Enable the Admin Web Interface (boolean value) + #web_enabled = false + + # The ip address to listen on (IP address value) + #web_ip = 0.0.0.0 + + # The port to listen on (port value) + # Minimum value: 0 + # Maximum value: 65535 + #web_port = 8001 + + # The admin user for the admin web interface (string value) + #user = admin + + # Admin interface password (string value) + #password = + + + [aprs_fi] + + # + # From aprsd.conf + # + + # Get the apiKey from your aprs.fi account here:http://aprs.fi/account + # (string value) + #apiKey = + + + [aprs_network] + + # + # From aprsd.conf + # + + # Set enabled to False if there is no internet connectivity.This is + # useful for a direwolf KISS aprs connection only. (boolean value) + #enabled = true + + # APRS Username (string value) + #login = NOCALL + + # APRS Password Get the passcode for your callsign here: + # https://apps.magicbug.co.uk/passcode (string value) + #password = + + # The APRS-IS hostname (hostname value) + #host = noam.aprs2.net + + # APRS-IS port (port value) + # Minimum value: 0 + # Maximum value: 65535 + #port = 14580 + + + [aprsd_weewx_plugin] + + # + # From aprsd_weewx_plugin.conf + # + + # Latitude of the station you want to report as (floating point value) + #latitude = + + # Longitude of the station you want to report as (floating point + # value) + #longitude = + + # How long (in seconds) in between weather reports (integer value) + #report_interval = 60 + + + [avwx_plugin] + + # + # From aprsd.conf + # + + # avwx-api is an opensource project that hasa hosted service here: + # https://avwx.rest/You can launch your own avwx-api in a containerby + # cloning the githug repo here:https://github.com/avwx-rest/AVWX-API + # (string value) + #apiKey = + + # The base url for the avwx API. If you are hosting your ownHere is + # where you change the url to point to yours. (string value) + #base_url = https://avwx.rest + + + [email_plugin] + + # + # From aprsd.conf + # + + # (Required) Callsign to validate for doing email commands.Only this + # callsign can check email. This is also where the email notifications + # for new emails will be sent. (string value) + #callsign = + + # Enable the Email plugin? (boolean value) + #enabled = false + + # Enable the Email plugin Debugging? (boolean value) + #debug = false + + # Login username/email for IMAP server (string value) + #imap_login = + + # Login password for IMAP server (string value) + #imap_password = + + # Hostname/IP of the IMAP server (hostname value) + #imap_host = + + # Port to use for IMAP server (port value) + # Minimum value: 0 + # Maximum value: 65535 + #imap_port = 993 + + # Use SSL for connection to IMAP Server (boolean value) + #imap_use_ssl = true + + # Login username/email for SMTP server (string value) + #smtp_login = + + # Login password for SMTP server (string value) + #smtp_password = + + # Hostname/IP of the SMTP server (hostname value) + #smtp_host = + + # Port to use for SMTP server (port value) + # Minimum value: 0 + # Maximum value: 65535 + #smtp_port = 465 + + # Use SSL for connection to SMTP Server (boolean value) + #smtp_use_ssl = true + + # List of email shortcuts for checking/sending email For Exmaple: + # wb=walt@walt.com,cl=cl@cl.com + # Means use 'wb' to send an email to walt@walt.com (list value) + #email_shortcuts = + + + [kiss_serial] + + # + # From aprsd.conf + # + + # Enable Serial KISS interface connection. (boolean value) + #enabled = false + + # Serial Device file to use. /dev/ttyS0 (string value) + #device = + + # The Serial device baud rate for communication (integer value) + #baudrate = 9600 + + + [kiss_tcp] + + # + # From aprsd.conf + # + + # Enable Serial KISS interface connection. (boolean value) + #enabled = false + + # The KISS TCP Host to connect to. (hostname value) + #host = + + # The KISS TCP/IP network port (port value) + # Minimum value: 0 + # Maximum value: 65535 + #port = 8001 + + + [logging] + + # + # From aprsd.conf + # + + # Date format for log entries (string value) + #date_format = %m/%d/%Y %I:%M:%S %p + + # Enable Rich logging (boolean value) + #rich_logging = true + + # File to log to (string value) + #logfile = + + # Log file format, unless rich_logging enabled. (string value) + #logformat = [%(asctime)s] [%(threadName)-20.20s] [%(levelname)-5.5s] %(message)s - [%(pathname)s:%(lineno)d] + + + [owm_weather_plugin] + + # + # From aprsd.conf + # + + # OWMWeatherPlugin api key to OpenWeatherMap's API.This plugin uses + # the openweathermap API to fetchlocation and weather information.To + # use this plugin you need to get an openweathermapaccount and + # apikey.https://home.openweathermap.org/api_keys (string value) + #apiKey = + + + [query_plugin] + + # + # From aprsd.conf + # + + # The Ham callsign to allow access to the query plugin from RF. + # (string value) + #callsign = + + + [rpc_settings] + + # + # From aprsd.conf + # + + # Enable RPC calls (boolean value) + #enabled = true + + # The ip address to listen on (string value) + #ip = localhost + + # The port to listen on (port value) + # Minimum value: 0 + # Maximum value: 65535 + #port = 18861 + + # Magic word to authenticate requests between client/server (string + # value) + #magic_word = CHANGEME!!! + + + [watch_list] + + # + # From aprsd.conf + # + + # Enable the watch list feature. Still have to enable the correct + # plugin. Built-in plugin to use is aprsd.plugins.notify.NotifyPlugin + # (boolean value) + #enabled = false + + # Callsigns to watch for messsages (list value) + #callsigns = + + # The Ham Callsign to send messages to for watch list alerts. (string + # value) + #alert_callsign = + + # The number of packets to store. (integer value) + #packet_keep_count = 10 + + # Time to wait before alert is sent on new message for users in + # callsigns. (integer value) + #alert_time_seconds = 3600 Note, You must edit the config file and change the ham callsign to your legal FCC HAM callsign, or aprsd server will not start. diff --git a/docs/plugin.rst b/docs/plugin.rst index 6f74ce3..534231e 100644 --- a/docs/plugin.rst +++ b/docs/plugin.rst @@ -41,7 +41,7 @@ aprsd/examples/plugins/example_plugin.py LOG = logging.getLogger("APRSD") - class HelloPlugin(plugin.APRSDPluginBase): + class HelloPlugin(plugin.APRSDRegexCommandPluginBase): """Hello World.""" version = "1.0" @@ -49,7 +49,7 @@ aprsd/examples/plugins/example_plugin.py command_regex = "^[hH]" command_name = "hello" - def command(self, fromcall, message, ack): + def process(self, packet): LOG.info("HelloPlugin") - reply = "Hello '{}'".format(fromcall) + reply = "Hello '{}'".format(packet.from_call) return reply diff --git a/docs/readme.rst b/docs/readme.rst index d14091d..caaf8b1 100644 --- a/docs/readme.rst +++ b/docs/readme.rst @@ -1,28 +1,12 @@ -===== -APRSD -===== -by KM6LYW and WB4BOR +=============================================== +APRSD - Ham radio APRS-IS Message plugin server +=============================================== -.. image:: https://badge.fury.io/py/aprsd.svg - :target: https://badge.fury.io/py/aprsd +KM6LYW and WB4BOR +____________________ -.. image:: https://github.com/craigerl/aprsd/workflows/python/badge.svg - :target: https://github.com/craigerl/aprsd/actions +|pypi| |pytest| |versions| |slack| |issues| |commit| |imports| |down| -.. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://black.readthedocs.io/en/stable/ - -.. image:: https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336 - :target: https://timothycrosley.github.io/isort/ - -.. image:: https://img.shields.io/github/issues/craigerl/aprsd - -.. image:: https://img.shields.io/github/last-commit/craigerl/aprsd - -.. image:: https://static.pepy.tech/personalized-badge/aprsd?period=month&units=international_system&left_color=black&right_color=orange&left_text=Downloads - :target: https://pepy.tech/project/aprsd - -.. contents:: :local: `APRSD `_ is a Ham radio `APRS `_ message command gateway built on python. @@ -37,11 +21,14 @@ provide responding to messages to check email, get location, ping, time of day, get weather, and fortune telling as well as version information of aprsd itself. -Documentation: https://aprsd.readthedocs.io +Please `read the docs`_ to learn more! + + +.. contents:: :local: APRSD Overview Diagram ----------------------- +====================== .. image:: https://raw.githubusercontent.com/craigerl/aprsd/master/docs/_static/aprsd_overview.svg?sanitize=true @@ -50,7 +37,7 @@ Typical use case ================ Ham radio operator using an APRS enabled HAM radio sends a message to check -the weather. an APRS message is sent, and then picked up by APRSD. The +the weather. An APRS message is sent, and then picked up by APRSD. The APRS packet is decoded, and the message is sent through the list of plugins for processing. For example, the WeatherPlugin picks up the message, fetches the weather for the area around the user who sent the request, and then responds with @@ -59,103 +46,91 @@ callsigns to look out for. The watch list can notify you when a HAM callsign in the list is seen and now available to message on the APRS network. -APRSD Capabilities -================== - -* server - The main aprsd server processor. Send/Rx APRS messages to HAM callsign -* send-message - use aprsd to send a command/message to aprsd server. Used for development testing -* sample-config - generate a sample aprsd.yml config file for use/editing -* bash completion generation. Uses python click bash completion to generate completion code for your .bashrc/.zshrc - - -List of core server plugins -=========================== - -Plugins function by specifying a regex that is searched for in the APRS message. -If it matches, the plugin runs. IF the regex doesn't match, the plugin is skipped. - -* EmailPlugin - Check email and reply with contents. Have to configure IMAP and SMTP settings in aprs.yml -* FortunePlugin - Replies with old unix fortune random fortune! -* LocationPlugin - Checks location of ham operator -* PingPlugin - Sends pong with timestamp -* QueryPlugin - Allows querying the list of delayed messages that were not ACK'd by radio -* TimePlugin - Current time of day -* WeatherPlugin - Get weather conditions for current location of HAM callsign -* VersionPlugin - Reports the version information for aprsd - - -List of core notification plugins -================================= - -These plugins see all APRS messages from ham callsigns in the config's watch -list. - -* NotifySeenPlugin - Send a message when a message is seen from a callsign in - the watch list. This is helpful when you want to know - when a friend is online in the ARPS network, but haven't - been seen in a while. - - -Current messages this will respond to: +Current list of built-in plugins ====================================== :: - APRS messages: - l(ocation) [callsign] = descriptive current location of your radio - 8 Miles E Auburn CA 1673' 39.92150,-120.93950 0.1h ago - w(eather) = weather forecast for your radio's current position - 58F(58F/46F) Partly Cloudy. Tonight, Heavy Rain. - t(ime) = respond with the current time - f(ortune) = respond with a short fortune - -email_addr email text = send an email, say "mapme" to send a current position/map - -2 = resend the last 2 emails from your imap inbox to this radio - p(ing) = respond with Pong!/time - v(ersion) = Respond with current APRSD Version string - anything else = respond with usage + โ””โ”€> aprsd list-plugins + ๐Ÿ APRSD Built-in Plugins ๐Ÿ + โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“ + โ”ƒ Plugin Name โ”ƒ Info โ”ƒ Type โ”ƒ Plugin Path โ”ƒ + โ”กโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ฉ + โ”‚ AVWXWeatherPlugin โ”‚ AVWX weather of GPS Beacon location โ”‚ RegexCommand โ”‚ aprsd.plugins.weather.AVWXWeatherPlugin โ”‚ + โ”‚ EmailPlugin โ”‚ Send and Receive email โ”‚ RegexCommand โ”‚ aprsd.plugins.email.EmailPlugin โ”‚ + โ”‚ FortunePlugin โ”‚ Give me a fortune โ”‚ RegexCommand โ”‚ aprsd.plugins.fortune.FortunePlugin โ”‚ + โ”‚ LocationPlugin โ”‚ Where in the world is a CALLSIGN's last GPS beacon? โ”‚ RegexCommand โ”‚ aprsd.plugins.location.LocationPlugin โ”‚ + โ”‚ NotifySeenPlugin โ”‚ Notify me when a CALLSIGN is recently seen on APRS-IS โ”‚ WatchList โ”‚ aprsd.plugins.notify.NotifySeenPlugin โ”‚ + โ”‚ OWMWeatherPlugin โ”‚ OpenWeatherMap weather of GPS Beacon location โ”‚ RegexCommand โ”‚ aprsd.plugins.weather.OWMWeatherPlugin โ”‚ + โ”‚ PingPlugin โ”‚ reply with a Pong! โ”‚ RegexCommand โ”‚ aprsd.plugins.ping.PingPlugin โ”‚ + โ”‚ QueryPlugin โ”‚ APRSD Owner command to query messages in the MsgTrack โ”‚ RegexCommand โ”‚ aprsd.plugins.query.QueryPlugin โ”‚ + โ”‚ TimeOWMPlugin โ”‚ Current time of GPS beacon's timezone. Uses OpenWeatherMap โ”‚ RegexCommand โ”‚ aprsd.plugins.time.TimeOWMPlugin โ”‚ + โ”‚ TimePlugin โ”‚ What is the current local time. โ”‚ RegexCommand โ”‚ aprsd.plugins.time.TimePlugin โ”‚ + โ”‚ USMetarPlugin โ”‚ USA only METAR of GPS Beacon location โ”‚ RegexCommand โ”‚ aprsd.plugins.weather.USMetarPlugin โ”‚ + โ”‚ USWeatherPlugin โ”‚ Provide USA only weather of GPS Beacon location โ”‚ RegexCommand โ”‚ aprsd.plugins.weather.USWeatherPlugin โ”‚ + โ”‚ VersionPlugin โ”‚ What is the APRSD Version โ”‚ RegexCommand โ”‚ aprsd.plugins.version.VersionPlugin โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -Meanwhile this code will monitor a single imap mailbox and forward email -to your BASECALLSIGN over the air. Only radios using the BASECALLSIGN are allowed -to send email, so consider this security risk before using this (or Amatuer radio in -general). Email is single user at this time. + Pypi.org APRSD Installable Plugin Packages -There are additional parameters in the code (sorry), so be sure to set your -email server, and associated logins, passwords. search for "yourdomain", -"password". Search for "shortcuts" to setup email aliases as well. + Install any of the following plugins with 'pip install ' + โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“ + โ”ƒ Plugin Package Name โ”ƒ Description โ”ƒ Version โ”ƒ Released โ”ƒ Installed? โ”ƒ + โ”กโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ฉ + โ”‚ ๐Ÿ“‚ aprsd-stock-plugin โ”‚ Ham Radio APRSD Plugin for fetching stock quotes โ”‚ 0.1.3 โ”‚ Dec 2, 2022 โ”‚ No โ”‚ + โ”‚ ๐Ÿ“‚ aprsd-sentry-plugin โ”‚ Ham radio APRSD plugin that does.... โ”‚ 0.1.2 โ”‚ Dec 2, 2022 โ”‚ No โ”‚ + โ”‚ ๐Ÿ“‚ aprsd-timeopencage-plugin โ”‚ APRSD plugin for fetching time based on GPS location โ”‚ 0.1.0 โ”‚ Dec 2, 2022 โ”‚ No โ”‚ + โ”‚ ๐Ÿ“‚ aprsd-weewx-plugin โ”‚ HAM Radio APRSD that reports weather from a weewx weather station. โ”‚ 0.1.4 โ”‚ Dec 7, 2021 โ”‚ Yes โ”‚ + โ”‚ ๐Ÿ“‚ aprsd-repeat-plugins โ”‚ APRSD Plugins for the REPEAT service โ”‚ 1.0.12 โ”‚ Dec 2, 2022 โ”‚ No โ”‚ + โ”‚ ๐Ÿ“‚ aprsd-telegram-plugin โ”‚ Ham Radio APRS APRSD plugin for Telegram IM service โ”‚ 0.1.3 โ”‚ Dec 2, 2022 โ”‚ No โ”‚ + โ”‚ ๐Ÿ“‚ aprsd-twitter-plugin โ”‚ Python APRSD plugin to send tweets โ”‚ 0.3.0 โ”‚ Dec 7, 2021 โ”‚ No โ”‚ + โ”‚ ๐Ÿ“‚ aprsd-slack-plugin โ”‚ Amateur radio APRS daemon which listens for messages and responds โ”‚ 1.0.5 โ”‚ Dec 18, 2022 โ”‚ No โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -Installation: + ๐Ÿ APRSD Installed 3rd party Plugins ๐Ÿ + โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“ + โ”ƒ Package Name โ”ƒ Plugin Name โ”ƒ Version โ”ƒ Type โ”ƒ Plugin Path โ”ƒ + โ”กโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ฉ + โ”‚ aprsd-weewx-plugin โ”‚ WeewxMQTTPlugin โ”‚ 1.0 โ”‚ RegexCommand โ”‚ aprsd_weewx_plugin.weewx.WeewxMQTTPlugin โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +Installation ============= - pip install aprsd +To install ``aprsd``, use Pip: -Example usage: +``pip install aprsd`` + +Example usage ============== - aprsd -h +``aprsd -h`` Help ==== :: - โ””โ”€[$] > aprsd -h + โ””โ”€> aprsd -h Usage: aprsd [OPTIONS] COMMAND [ARGS]... - Shell completion for click-completion-command Available shell types: - bash Bourne again shell fish Friendly interactive shell - powershell Windows PowerShell zsh Z shell Default type: auto - Options: --version Show the version and exit. -h, --help Show this message and exit. Commands: - install Install the click-completion-command completion - sample-config This dumps the config to stdout. + check-version Check this version against the latest in pypi.org. + completion Click Completion subcommands + dev Development type subcommands + healthcheck Check the health of the running aprsd server. + list-plugins List the built in plugins available to APRSD. + listen Listen to packets on the APRS-IS Network based on FILTER. + sample-config Generate a sample Config file from aprsd and all... send-message Send a message to a callsign via APRS_IS. - server Start the aprsd server process. - show Show the click-completion-command completion code + server Start the aprsd server gateway process. + version Show the APRSD version. + webchat Web based HAM Radio chat program! @@ -165,90 +140,14 @@ Commands Configuration ============= This command outputs a sample config yml formatted block that you can edit -and use to pass in to aprsd with -c. By default aprsd looks in ~/.config/aprsd/aprsd.yml +and use to pass in to ``aprsd`` with ``-c``. By default aprsd looks in ``~/.config/aprsd/aprsd.yml`` - aprsd sample-config +``aprsd sample-config`` -Output -====== :: โ””โ”€> aprsd sample-config - aprs: - # Get the passcode for your callsign here: - # https://apps.magicbug.co.uk/passcode - host: rotate.aprs2.net - login: CALLSIGN - password: '00000' - port: 14580 - aprsd: - dateformat: '%m/%d/%Y %I:%M:%S %p' - email: - enabled: true - imap: - debug: false - host: imap.gmail.com - login: IMAP_USERNAME - password: IMAP_PASSWORD - port: 993 - use_ssl: true - shortcuts: - aa: 5551239999@vtext.com - cl: craiglamparter@somedomain.org - wb: 555309@vtext.com - smtp: - debug: false - host: smtp.gmail.com - login: SMTP_USERNAME - password: SMTP_PASSWORD - port: 465 - use_ssl: false - enabled_plugins: - - aprsd.plugins.email.EmailPlugin - - aprsd.plugins.fortune.FortunePlugin - - aprsd.plugins.location.LocationPlugin - - aprsd.plugins.ping.PingPlugin - - aprsd.plugins.query.QueryPlugin - - aprsd.plugins.stock.StockPlugin - - aprsd.plugins.time.TimePlugin - - aprsd.plugins.weather.USWeatherPlugin - - aprsd.plugins.version.VersionPlugin - logfile: /tmp/aprsd.log - logformat: '[%(asctime)s] [%(threadName)-12s] [%(levelname)-5.5s] %(message)s - [%(pathname)s:%(lineno)d]' - trace: false - units: imperial - web: - enabled: true - host: 0.0.0.0 - logging_enabled: true - port: 8001 - users: - admin: aprsd - ham: - callsign: CALLSIGN - services: - aprs.fi: - # Get the apiKey from your aprs.fi account here: - # http://aprs.fi/account - apiKey: APIKEYVALUE - avwx: - # (Optional for AVWXWeatherPlugin) - # Use hosted avwx-api here: https://avwx.rest - # or deploy your own from here: - # https://github.com/avwx-rest/avwx-api - apiKey: APIKEYVALUE - base_url: http://host:port - opencagedata: - # (Optional for TimeOpenCageDataPlugin) - # Get the apiKey from your opencagedata account here: - # https://opencagedata.com/dashboard#api-keys - apiKey: APIKEYVALUE - openweathermap: - # (Optional for OWMWeatherPlugin) - # Get the apiKey from your - # openweathermap account here: - # https://home.openweathermap.org/api_keys - apiKey: APIKEYVALUE + ... server ====== @@ -259,35 +158,35 @@ look for incomming commands to the callsign configured in the config file :: โ””โ”€[$] > aprsd server --help - Usage: aprsd server [OPTIONS] + Usage: aprsd server [OPTIONS] - Start the aprsd server process. + Start the aprsd server gateway process. - Options: - --loglevel [CRITICAL|ERROR|WARNING|INFO|DEBUG] - The log level to use for aprsd.log - [default: INFO] + Options: + --loglevel [CRITICAL|ERROR|WARNING|INFO|DEBUG] + The log level to use for aprsd.log + [default: INFO] + -c, --config TEXT The aprsd config file to use for options. + [default: + /Users/i530566/.config/aprsd/aprsd.yml] + --quiet Don't log to stdout + -f, --flush Flush out all old aged messages on disk. + [default: False] + -h, --help Show this message and exit. - --quiet Don't log to stdout - --disable-validation Disable email shortcut validation. Bad - email addresses can result in broken email - responses!! - - -c, --config TEXT The aprsd config file to use for options. - [default: - /home/waboring/.config/aprsd/aprsd.yml] - - -f, --flush Flush out all old aged messages on disk. - [default: False] - - -h, --help Show this message and exit. - - $ aprsd server + โ””โ”€> aprsd server Load config - [02/13/2021 09:22:09 AM] [MainThread ] [INFO ] APRSD Started version: 1.6.0 - [02/13/2021 09:22:09 AM] [MainThread ] [INFO ] Checking IMAP configuration - [02/13/2021 09:22:09 AM] [MainThread ] [INFO ] Checking SMTP configuration - [02/13/2021 09:22:10 AM] [MainThread ] [INFO ] Validating 2 Email shortcuts. This can take up to 10 seconds per shortcut + 12/07/2021 03:16:17 PM MainThread INFO APRSD is up to date server.py:51 + 12/07/2021 03:16:17 PM MainThread INFO APRSD Started version: 2.5.6 server.py:52 + 12/07/2021 03:16:17 PM MainThread INFO Using CONFIG values: server.py:55 + 12/07/2021 03:16:17 PM MainThread INFO ham.callsign = WB4BOR server.py:60 + 12/07/2021 03:16:17 PM MainThread INFO aprs.login = WB4BOR-12 server.py:60 + 12/07/2021 03:16:17 PM MainThread INFO aprs.password = XXXXXXXXXXXXXXXXXXX server.py:58 + 12/07/2021 03:16:17 PM MainThread INFO aprs.host = noam.aprs2.net server.py:60 + 12/07/2021 03:16:17 PM MainThread INFO aprs.port = 14580 server.py:60 + 12/07/2021 03:16:17 PM MainThread INFO aprs.logfile = /tmp/aprsd.log server.py:60 + + send-message @@ -299,32 +198,30 @@ test messages :: โ””โ”€[$] > aprsd send-message -h - Usage: aprsd send-message [OPTIONS] TOCALLSIGN [COMMAND]... + Usage: aprsd send-message [OPTIONS] TOCALLSIGN COMMAND... Send a message to a callsign via APRS_IS. Options: --loglevel [CRITICAL|ERROR|WARNING|INFO|DEBUG] The log level to use for aprsd.log - [default: DEBUG] - - --quiet Don't log to stdout + [default: INFO] -c, --config TEXT The aprsd config file to use for options. - [default: ~/.config/aprsd/aprsd.yml] - + [default: + /Users/i530566/.config/aprsd/aprsd.yml] + --quiet Don't log to stdout --aprs-login TEXT What callsign to send the message from. [env var: APRS_LOGIN] - --aprs-password TEXT the APRS-IS password for APRS_LOGIN [env var: APRS_PASSWORD] - + -n, --no-ack Don't wait for an ack, just sent it to APRS- + IS and bail. [default: False] + -w, --wait-response Wait for a response to the message? + [default: False] + --raw TEXT Send a raw message. Implies --no-ack -h, --help Show this message and exit. -Example output: -=============== - - SEND EMAIL (radio to smtp server) ================================= @@ -395,25 +292,35 @@ AND... ping, fortune, time..... Development =========== -* git clone git@github.com:craigerl/aprsd.git -* cd aprsd -* make +* ``git clone git@github.com:craigerl/aprsd.git`` +* ``cd aprsd`` +* ``make`` Workflow ======== -While working aprsd, The workflow is as follows +While working aprsd, The workflow is as follows: + +* Checkout a new branch to work on by running + + ``git checkout -b mybranch`` + +* Make your changes to the code +* Run Tox with the following options: + + - ``tox -epep8`` + - ``tox -efmt`` + - ``tox -p`` + +* Commit your changes. This will run the pre-commit hooks which does checks too + + ``git commit`` -* checkout a new branch to work on -* git checkout -b mybranch -* Edit code -* run tox -epep8 -* run tox -efmt -* run tox -p -* git commit ( This will run the pre-commit hooks which does checks too ) * Once you are done with all of your commits, then push up the branch to - github -* git push -u origin mybranch + github with: + + ``git push -u origin mybranch`` + * Create a pull request from your branch so github tests can run and we can do a code review. @@ -423,21 +330,21 @@ Release To do release to pypi: -* Tag release with +* Tag release with: - git tag -v1.XX -m "New release" + ``git tag -v1.XX -m "New release"`` -* push release tag up +* Push release tag: - git push origin master --tags + ``git push origin master --tags`` -* Do a test build and verify build is valid +* Do a test build and verify build is valid by running: - make build + ``make build`` -* Once twine is happy, upload release to pypi +* Once twine is happy, upload release to pypi: - make upload + ``make upload`` Docker Container @@ -455,24 +362,62 @@ the repo. Official Build ============== - docker build -t hemna6969/aprsd:latest . +``docker build -t hemna6969/aprsd:latest .`` Development Build ================= - docker build -t hemna6969/aprsd:latest -f Dockerfile-dev . +``docker build -t hemna6969/aprsd:latest -f Dockerfile-dev .`` Running the container ===================== -There is a docker-compose.yml file that can be used to run your container. -There are 2 volumes defined that can be used to store your configuration -and the plugins directory: /config and /plugins +There is a ``docker-compose.yml`` file in the ``docker/`` directory +that can be used to run your container. To provide the container +an ``aprsd.conf`` configuration file, change your +``docker-compose.yml`` as shown below: -If you want to install plugins at container start time, then use the -environment var in docker-compose.yml specified as APRS_PLUGINS -Provide a csv list of pypi installable plugins. Then make sure the plugin -python file is in your /plugins volume and the plugin will be installed at -container startup. The plugin may have dependencies that are required. -The plugin file should be copied to /plugins for loading by aprsd +:: + + volumes: + - $HOME/.config/aprsd:/config + +To install plugins at container start time, pass in a list of +comma-separated list of plugins on PyPI using the ``APRSD_PLUGINS`` +environment variable in the ``docker-compose.yml`` file. Note that +version constraints may also be provided. For example: + +:: + + environment: + - APRSD_PLUGINS=aprsd-slack-plugin>=1.0.2,aprsd-twitter-plugin + + +.. badges + +.. |pypi| image:: https://badge.fury.io/py/aprsd.svg + :target: https://badge.fury.io/py/aprsd + +.. |pytest| image:: https://github.com/craigerl/aprsd/workflows/python/badge.svg + :target: https://github.com/craigerl/aprsd/actions + +.. |versions| image:: https://img.shields.io/pypi/pyversions/aprsd.svg + :target: https://pypi.org/pypi/aprsd + +.. |slack| image:: https://img.shields.io/badge/slack-@hemna/aprsd-blue.svg?logo=slack + :target: https://hemna.slack.com/app_redirect?channel=C01KQSCP5RP + +.. |imports| image:: https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336 + :target: https://timothycrosley.github.io/isort/ + +.. |issues| image:: https://img.shields.io/github/issues/craigerl/aprsd + +.. |commit| image:: https://img.shields.io/github/last-commit/craigerl/aprsd + +.. |down| image:: https://static.pepy.tech/personalized-badge/aprsd?period=month&units=international_system&left_color=black&right_color=orange&left_text=Downloads + :target: https://pepy.tech/project/aprsd + +.. links +.. _read the docs: + https://aprsd.readthedocs.io