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)