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
This commit is contained in:
Hemna 2022-12-24 13:53:06 -05:00
parent ce3b29f990
commit e13ca0061a
29 changed files with 496 additions and 430 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}'"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = {}

View File

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

View File

@ -29,3 +29,4 @@ user-agents
pyopenssl
dataclasses
dacite2
oslo.config

View File

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

View File

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

View File

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

View File

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