From 23e3876e7b5d4a4b68101dd824afb2ec8ade5281 Mon Sep 17 00:00:00 2001 From: Hemna Date: Thu, 16 Sep 2021 17:08:30 -0400 Subject: [PATCH] Refactor utils usage This patch separates out the config from the utils.py utils.py has grown into a catchall for everything and this patch is the start of that cleanup. --- aprsd/client.py | 5 - aprsd/config.py | 367 ++++++++++++++++++++++++++++++++++++++ aprsd/dev.py | 8 +- aprsd/flask.py | 12 +- aprsd/healthcheck.py | 6 +- aprsd/listen.py | 14 +- aprsd/main.py | 47 ++--- aprsd/messaging.py | 16 +- aprsd/plugins/location.py | 4 +- aprsd/plugins/time.py | 10 +- aprsd/plugins/weather.py | 18 +- aprsd/utils.py | 348 ------------------------------------ docker/Dockerfile | 2 +- docker/build.sh | 2 +- tests/test_plugin.py | 4 +- 15 files changed, 435 insertions(+), 428 deletions(-) create mode 100644 aprsd/config.py diff --git a/aprsd/client.py b/aprsd/client.py index dd1e248..3e8e198 100644 --- a/aprsd/client.py +++ b/aprsd/client.py @@ -38,11 +38,6 @@ class Client: if config: self.config = config - def new(self): - obj = super().__new__(Client) - obj.config = self.config - return obj - @property def client(self): if not self.aprs_client: diff --git a/aprsd/config.py b/aprsd/config.py new file mode 100644 index 0000000..1461c2f --- /dev/null +++ b/aprsd/config.py @@ -0,0 +1,367 @@ +import logging +import os +from pathlib import Path +import sys + +import click +import yaml + +from aprsd import utils + + +LOG_LEVELS = { + "CRITICAL": logging.CRITICAL, + "ERROR": logging.ERROR, + "WARNING": logging.WARNING, + "INFO": logging.INFO, + "DEBUG": logging.DEBUG, +} + +DEFAULT_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p" +DEFAULT_LOG_FORMAT = ( + "[%(asctime)s] [%(threadName)-20.20s] [%(levelname)-5.5s]" + " %(message)s - [%(pathname)s:%(lineno)d]" +) + +QUEUE_DATE_FORMAT = "[%m/%d/%Y] [%I:%M:%S %p]" +QUEUE_LOG_FORMAT = ( + "%(asctime)s [%(threadName)-20.20s] [%(levelname)-5.5s]" + " %(message)s - [%(pathname)s:%(lineno)d]" +) + +CORE_MESSAGE_PLUGINS = [ + "aprsd.plugins.email.EmailPlugin", + "aprsd.plugins.fortune.FortunePlugin", + "aprsd.plugins.location.LocationPlugin", + "aprsd.plugins.ping.PingPlugin", + "aprsd.plugins.query.QueryPlugin", + "aprsd.plugins.stock.StockPlugin", + "aprsd.plugins.time.TimePlugin", + "aprsd.plugins.weather.USWeatherPlugin", + "aprsd.plugins.version.VersionPlugin", +] + +CORE_NOTIFY_PLUGINS = [ + "aprsd.plugins.notify.NotifySeenPlugin", +] + +# an example of what should be in the ~/.aprsd/config.yml +DEFAULT_CONFIG_DICT = { + "ham": {"callsign": "NOCALL"}, + "aprs": { + "enabled": True, + "login": "CALLSIGN", + "password": "00000", + "host": "rotate.aprs2.net", + "port": 14580, + }, + "kiss": { + "tcp": { + "enabled": False, + "host": "direwolf.ip.address", + "port": "8001", + }, + "serial": { + "enabled": False, + "device": "/dev/ttyS0", + "baudrate": 9600, + }, + }, + "aprsd": { + "logfile": "/tmp/aprsd.log", + "logformat": DEFAULT_LOG_FORMAT, + "dateformat": DEFAULT_DATE_FORMAT, + "trace": False, + "enabled_plugins": CORE_MESSAGE_PLUGINS, + "units": "imperial", + "watch_list": { + "enabled": False, + # Who gets the alert? + "alert_callsign": "NOCALL", + # 43200 is 12 hours + "alert_time_seconds": 43200, + # How many packets to save in a ring Buffer + # for a particular callsign + "packet_keep_count": 10, + "callsigns": [], + "enabled_plugins": CORE_NOTIFY_PLUGINS, + }, + "web": { + "enabled": True, + "logging_enabled": True, + "host": "0.0.0.0", + "port": 8001, + "users": { + "admin": "password-here", + }, + }, + "email": { + "enabled": True, + "shortcuts": { + "aa": "5551239999@vtext.com", + "cl": "craiglamparter@somedomain.org", + "wb": "555309@vtext.com", + }, + "smtp": { + "login": "SMTP_USERNAME", + "password": "SMTP_PASSWORD", + "host": "smtp.gmail.com", + "port": 465, + "use_ssl": False, + "debug": False, + }, + "imap": { + "login": "IMAP_USERNAME", + "password": "IMAP_PASSWORD", + "host": "imap.gmail.com", + "port": 993, + "use_ssl": True, + "debug": False, + }, + }, + }, + "services": { + "aprs.fi": {"apiKey": "APIKEYVALUE"}, + "openweathermap": {"apiKey": "APIKEYVALUE"}, + "opencagedata": {"apiKey": "APIKEYVALUE"}, + "avwx": {"base_url": "http://host:port", "apiKey": "APIKEYVALUE"}, + }, +} + +home = str(Path.home()) +DEFAULT_CONFIG_DIR = f"{home}/.config/aprsd/" +DEFAULT_SAVE_FILE = f"{home}/.config/aprsd/aprsd.p" +DEFAULT_CONFIG_FILE = f"{home}/.config/aprsd/aprsd.yml" + + +def add_config_comments(raw_yaml): + end_idx = utils.end_substr(raw_yaml, "aprs:") + if end_idx != -1: + # lets insert a comment + raw_yaml = utils.insert_str( + raw_yaml, + "\n # Set enabled to False if there is no internet connectivity." + "\n # This is useful for a direwolf KISS aprs connection only. " + "\n" + "\n # Get the passcode for your callsign here: " + "\n # https://apps.magicbug.co.uk/passcode", + end_idx, + ) + + end_idx = utils.end_substr(raw_yaml, "aprs.fi:") + if end_idx != -1: + # lets insert a comment + raw_yaml = utils.insert_str( + raw_yaml, + "\n # Get the apiKey from your aprs.fi account here: " + "\n # http://aprs.fi/account", + end_idx, + ) + + end_idx = utils.end_substr(raw_yaml, "opencagedata:") + if end_idx != -1: + # lets insert a comment + raw_yaml = utils.insert_str( + raw_yaml, + "\n # (Optional for TimeOpenCageDataPlugin) " + "\n # Get the apiKey from your opencagedata account here: " + "\n # https://opencagedata.com/dashboard#api-keys", + end_idx, + ) + + end_idx = utils.end_substr(raw_yaml, "openweathermap:") + if end_idx != -1: + # lets insert a comment + raw_yaml = utils.insert_str( + raw_yaml, + "\n # (Optional for OWMWeatherPlugin) " + "\n # Get the apiKey from your " + "\n # openweathermap account here: " + "\n # https://home.openweathermap.org/api_keys", + end_idx, + ) + + end_idx = utils.end_substr(raw_yaml, "avwx:") + if end_idx != -1: + # lets insert a comment + raw_yaml = utils.insert_str( + raw_yaml, + "\n # (Optional for AVWXWeatherPlugin) " + "\n # Use hosted avwx-api here: https://avwx.rest " + "\n # or deploy your own from here: " + "\n # https://github.com/avwx-rest/avwx-api", + end_idx, + ) + + return raw_yaml + + +def dump_default_cfg(): + return add_config_comments( + yaml.dump( + DEFAULT_CONFIG_DICT, + indent=4, + ), + ) + + +def create_default_config(): + """Create a default config file.""" + # make sure the directory location exists + config_file_expanded = os.path.expanduser(DEFAULT_CONFIG_FILE) + config_dir = os.path.dirname(config_file_expanded) + if not os.path.exists(config_dir): + click.echo(f"Config dir '{config_dir}' doesn't exist, creating.") + utils.mkdir_p(config_dir) + with open(config_file_expanded, "w+") as cf: + cf.write(dump_default_cfg()) + + +def get_config(config_file): + """This tries to read the yaml config from .""" + config_file_expanded = os.path.expanduser(config_file) + if os.path.exists(config_file_expanded): + with open(config_file_expanded) as stream: + config = yaml.load(stream, Loader=yaml.FullLoader) + return config + else: + if config_file == DEFAULT_CONFIG_FILE: + click.echo( + f"{config_file_expanded} is missing, creating config file", + ) + create_default_config() + msg = ( + "Default config file created at {}. Please edit with your " + "settings.".format(config_file) + ) + click.echo(msg) + else: + # The user provided a config file path different from the + # Default, so we won't try and create it, just bitch and bail. + msg = f"Custom config file '{config_file}' is missing." + click.echo(msg) + + sys.exit(-1) + + +# This method tries to parse the config yaml file +# and consume the settings. +# If the required params don't exist, +# it will look in the environment +def parse_config(config_file): + # for now we still use globals....ugh + global CONFIG + + def fail(msg): + click.echo(msg) + sys.exit(-1) + + def check_option(config, chain, default_fail=None): + try: + config = check_config_option(config, chain, default_fail=default_fail) + except Exception as ex: + fail(repr(ex)) + else: + return config + + config = get_config(config_file) + + # special check here to make sure user has edited the config file + # and changed the ham callsign + check_option( + config, + [ + "ham", + "callsign", + ], + default_fail=DEFAULT_CONFIG_DICT["ham"]["callsign"], + ) + check_option( + config, + ["services", "aprs.fi", "apiKey"], + default_fail=DEFAULT_CONFIG_DICT["services"]["aprs.fi"]["apiKey"], + ) + check_option( + config, + ["aprs", "login"], + default_fail=DEFAULT_CONFIG_DICT["aprs"]["login"], + ) + check_option( + config, + ["aprs", "password"], + default_fail=DEFAULT_CONFIG_DICT["aprs"]["password"], + ) + + # Ensure they change the admin password + if config["aprsd"]["web"]["enabled"] is True: + check_option( + config, + ["aprsd", "web", "users", "admin"], + default_fail=DEFAULT_CONFIG_DICT["aprsd"]["web"]["users"]["admin"], + ) + + if config["aprsd"]["watch_list"]["enabled"] is True: + check_option( + config, + ["aprsd", "watch_list", "alert_callsign"], + default_fail=DEFAULT_CONFIG_DICT["aprsd"]["watch_list"]["alert_callsign"], + ) + + if config["aprsd"]["email"]["enabled"] is True: + # Check IMAP server settings + check_option(config, ["aprsd", "email", "imap", "host"]) + check_option(config, ["aprsd", "email", "imap", "port"]) + check_option( + config, + ["aprsd", "email", "imap", "login"], + default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["login"], + ) + check_option( + config, + ["aprsd", "email", "imap", "password"], + default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["password"], + ) + + # Check SMTP server settings + check_option(config, ["aprsd", "email", "smtp", "host"]) + check_option(config, ["aprsd", "email", "smtp", "port"]) + check_option( + config, + ["aprsd", "email", "smtp", "login"], + default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["login"], + ) + check_option( + config, + ["aprsd", "email", "smtp", "password"], + default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["password"], + ) + + return config + + +def conf_option_exists(conf, chain): + _key = chain.pop(0) + if _key in conf: + return conf_option_exists(conf[_key], chain) if chain else conf[_key] + + +def check_config_option(config, chain, default_fail=None): + result = conf_option_exists(config, chain.copy()) + if result is None: + raise Exception( + "'{}' was not in config file".format( + chain, + ), + ) + else: + if default_fail: + if result == default_fail: + # We have to fail and bail if the user hasn't edited + # this config option. + raise Exception( + "Config file needs to be edited from provided defaults for {}.".format( + chain, + ), + ) + else: + return config diff --git a/aprsd/dev.py b/aprsd/dev.py index e93f2f0..01eecc1 100644 --- a/aprsd/dev.py +++ b/aprsd/dev.py @@ -14,7 +14,9 @@ import click_completion # local imports here import aprsd -from aprsd import client, plugin, utils +from aprsd import client +from aprsd import config as aprsd_config +from aprsd import plugin # setup the global logger @@ -156,7 +158,7 @@ def setup_logging(config, loglevel, quiet): "--config", "config_file", show_default=True, - default=utils.DEFAULT_CONFIG_FILE, + default=aprsd_config.DEFAULT_CONFIG_FILE, help="The aprsd config file to use for options.", ) @click.option( @@ -178,7 +180,7 @@ def test_plugin( ): """APRSD Plugin test app.""" - config = utils.parse_config(config_file) + config = aprsd_config.parse_config(config_file) setup_logging(config, loglevel, False) LOG.info(f"Test APRSD PLugin version: {aprsd.__version__}") diff --git a/aprsd/flask.py b/aprsd/flask.py index e8fc0aa..ede31db 100644 --- a/aprsd/flask.py +++ b/aprsd/flask.py @@ -17,9 +17,9 @@ from flask_socketio import Namespace, SocketIO from werkzeug.security import check_password_hash, generate_password_hash import aprsd -from aprsd import ( - client, kissclient, messaging, packets, plugin, stats, threads, utils, -) +from aprsd import client +from aprsd import config as aprsd_config +from aprsd import kissclient, messaging, packets, plugin, stats, threads, utils LOG = logging.getLogger("APRSD") @@ -553,10 +553,10 @@ def setup_logging(config, flask_app, loglevel, quiet): flask_app.logger.disabled = True return - log_level = utils.LOG_LEVELS[loglevel] + log_level = aprsd_config.LOG_LEVELS[loglevel] LOG.setLevel(log_level) - log_format = config["aprsd"].get("logformat", utils.DEFAULT_LOG_FORMAT) - date_format = config["aprsd"].get("dateformat", utils.DEFAULT_DATE_FORMAT) + log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT) + date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT) log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) log_file = config["aprsd"].get("logfile", None) if log_file: diff --git a/aprsd/healthcheck.py b/aprsd/healthcheck.py index 4e4b212..462661c 100644 --- a/aprsd/healthcheck.py +++ b/aprsd/healthcheck.py @@ -19,7 +19,7 @@ import requests # local imports here import aprsd -from aprsd import utils +from aprsd import config as aprsd_config # setup the global logger @@ -172,7 +172,7 @@ def parse_delta_str(s): "--config", "config_file", show_default=True, - default=utils.DEFAULT_CONFIG_FILE, + default=aprsd_config.DEFAULT_CONFIG_FILE, help="The aprsd config file to use for options.", ) @click.option( @@ -191,7 +191,7 @@ def parse_delta_str(s): def check(loglevel, config_file, health_url, timeout): """APRSD Plugin test app.""" - config = utils.parse_config(config_file) + config = aprsd_config.parse_config(config_file) setup_logging(config, loglevel, False) LOG.debug(f"APRSD HealthCheck version: {aprsd.__version__}") diff --git a/aprsd/listen.py b/aprsd/listen.py index 4342447..40d90b7 100644 --- a/aprsd/listen.py +++ b/aprsd/listen.py @@ -36,7 +36,9 @@ import click_completion # local imports here import aprsd -from aprsd import client, messaging, stats, threads, trace, utils +from aprsd import client +from aprsd import config as aprsd_config +from aprsd import messaging, stats, threads, trace, utils # setup the global logger @@ -169,10 +171,10 @@ def signal_handler(sig, frame): # to disable logging to stdout, but still log to file # use the --quiet option on the cmdln def setup_logging(config, loglevel, quiet): - log_level = utils.LOG_LEVELS[loglevel] + log_level = aprsd_config.LOG_LEVELS[loglevel] LOG.setLevel(log_level) - log_format = config["aprsd"].get("logformat", utils.DEFAULT_LOG_FORMAT) - date_format = config["aprsd"].get("dateformat", utils.DEFAULT_DATE_FORMAT) + log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT) + date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT) log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) log_file = config["aprsd"].get("logfile", None) if log_file: @@ -218,7 +220,7 @@ def setup_logging(config, loglevel, quiet): "--config", "config_file", show_default=True, - default=utils.DEFAULT_CONFIG_FILE, + default=aprsd_config.DEFAULT_CONFIG_FILE, help="The aprsd config file to use for options.", ) @click.option( @@ -258,7 +260,7 @@ def listen( """Send a message to a callsign via APRS_IS.""" global got_ack, got_response - config = utils.parse_config(config_file) + config = aprsd_config.parse_config(config_file) if not aprs_login: click.echo("Must set --aprs_login or APRS_LOGIN") return diff --git a/aprsd/main.py b/aprsd/main.py index 6de7358..91b4816 100644 --- a/aprsd/main.py +++ b/aprsd/main.py @@ -37,9 +37,10 @@ import click_completion # local imports here import aprsd from aprsd import ( - client, flask, kissclient, messaging, packets, plugin, stats, threads, - trace, utils, + flask, kissclient, messaging, packets, plugin, stats, threads, trace, utils, ) +from aprsd import client +from aprsd import config as aprsd_config # setup the global logger @@ -48,22 +49,8 @@ LOG = logging.getLogger("APRSD") CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) - flask_enabled = False -# server_event = threading.Event() - -# localization, please edit: -# HOST = "noam.aprs2.net" # north america tier2 servers round robin -# USER = "KM6XXX-9" # callsign of this aprs client with SSID -# PASS = "99999" # google how to generate this -# BASECALLSIGN = "KM6XXX" # callsign of radio in the field to send email -# shortcuts = { -# "aa" : "5551239999@vtext.com", -# "cl" : "craiglamparter@somedomain.org", -# "wb" : "5553909472@vtext.com" -# } - def custom_startswith(string, incomplete): """A custom completion match that supports case insensitive matching.""" @@ -172,10 +159,10 @@ def signal_handler(sig, frame): # to disable logging to stdout, but still log to file # use the --quiet option on the cmdln def setup_logging(config, loglevel, quiet): - log_level = utils.LOG_LEVELS[loglevel] + log_level = aprsd_config.LOG_LEVELS[loglevel] LOG.setLevel(log_level) - log_format = config["aprsd"].get("logformat", utils.DEFAULT_LOG_FORMAT) - date_format = config["aprsd"].get("dateformat", utils.DEFAULT_DATE_FORMAT) + log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT) + date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT) log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) log_file = config["aprsd"].get("logfile", None) if log_file: @@ -196,15 +183,15 @@ def setup_logging(config, loglevel, quiet): imap_logger.addHandler(fh) if ( - utils.check_config_option( + aprsd_config.check_config_option( config, ["aprsd", "web", "enabled"], default_fail=False, ) ): qh = logging.handlers.QueueHandler(threads.logging_queue) q_log_formatter = logging.Formatter( - fmt=utils.QUEUE_LOG_FORMAT, - datefmt=utils.QUEUE_DATE_FORMAT, + fmt=aprsd_config.QUEUE_LOG_FORMAT, + datefmt=aprsd_config.QUEUE_DATE_FORMAT, ) qh.setFormatter(q_log_formatter) LOG.addHandler(qh) @@ -234,11 +221,11 @@ def setup_logging(config, loglevel, quiet): "--config", "config_file", show_default=True, - default=utils.DEFAULT_CONFIG_FILE, + default=aprsd_config.DEFAULT_CONFIG_FILE, help="The aprsd config file to use for options.", ) def check_version(loglevel, config_file): - config = utils.parse_config(config_file) + config = aprsd_config.parse_config(config_file) setup_logging(config, loglevel, False) level, msg = utils._check_version() @@ -251,7 +238,7 @@ def check_version(loglevel, config_file): @main.command() def sample_config(): """This dumps the config to stdout.""" - click.echo(utils.dump_default_cfg()) + click.echo(aprsd_config.dump_default_cfg()) @main.command() @@ -272,7 +259,7 @@ def sample_config(): "--config", "config_file", show_default=True, - default=utils.DEFAULT_CONFIG_FILE, + default=aprsd_config.DEFAULT_CONFIG_FILE, help="The aprsd config file to use for options.", ) @click.option( @@ -312,7 +299,7 @@ def send_message( """Send a message to a callsign via APRS_IS.""" global got_ack, got_response - config = utils.parse_config(config_file) + config = aprsd_config.parse_config(config_file) if not aprs_login: click.echo("Must set --aprs_login or APRS_LOGIN") return @@ -429,7 +416,7 @@ def send_message( "--config", "config_file", show_default=True, - default=utils.DEFAULT_CONFIG_FILE, + default=aprsd_config.DEFAULT_CONFIG_FILE, help="The aprsd config file to use for options.", ) @click.option( @@ -454,7 +441,7 @@ def server( if not quiet: click.echo("Load config") - config = utils.parse_config(config_file) + config = aprsd_config.parse_config(config_file) setup_logging(config, loglevel, quiet) level, msg = utils._check_version() @@ -523,7 +510,7 @@ def server( keepalive = threads.KeepAliveThread(config=config) keepalive.start() - web_enabled = utils.check_config_option(config, ["aprsd", "web", "enabled"], default_fail=False) + web_enabled = aprsd_config.check_config_option(config, ["aprsd", "web", "enabled"], default_fail=False) if web_enabled: flask_enabled = True diff --git a/aprsd/messaging.py b/aprsd/messaging.py index 751272f..1a983a1 100644 --- a/aprsd/messaging.py +++ b/aprsd/messaging.py @@ -9,7 +9,9 @@ import re import threading import time -from aprsd import client, kissclient, packets, stats, threads, trace, utils +from aprsd import client +from aprsd import config as aprsd_config +from aprsd import kissclient, packets, stats, threads, trace LOG = logging.getLogger("APRSD") @@ -113,11 +115,11 @@ class MsgTrack: LOG.debug(f"Save tracker to disk? {len(self)}") if len(self) > 0: LOG.info(f"Saving {len(self)} tracking messages to disk") - pickle.dump(self.dump(), open(utils.DEFAULT_SAVE_FILE, "wb+")) + pickle.dump(self.dump(), open(aprsd_config.DEFAULT_SAVE_FILE, "wb+")) else: LOG.debug( "Nothing to save, flushing old save file '{}'".format( - utils.DEFAULT_SAVE_FILE, + aprsd_config.DEFAULT_SAVE_FILE, ), ) self.flush() @@ -131,8 +133,8 @@ class MsgTrack: return dump def load(self): - if os.path.exists(utils.DEFAULT_SAVE_FILE): - raw = pickle.load(open(utils.DEFAULT_SAVE_FILE, "rb")) + if os.path.exists(aprsd_config.DEFAULT_SAVE_FILE): + raw = pickle.load(open(aprsd_config.DEFAULT_SAVE_FILE, "rb")) if raw: self.track = raw LOG.debug("Loaded MsgTrack dict from disk.") @@ -171,8 +173,8 @@ class MsgTrack: def flush(self): """Nuke the old pickle file that stored the old results from last aprsd run.""" - if os.path.exists(utils.DEFAULT_SAVE_FILE): - pathlib.Path(utils.DEFAULT_SAVE_FILE).unlink() + if os.path.exists(aprsd_config.DEFAULT_SAVE_FILE): + pathlib.Path(aprsd_config.DEFAULT_SAVE_FILE).unlink() with self.lock: self.track = {} diff --git a/aprsd/plugins/location.py b/aprsd/plugins/location.py index 59b3847..80a44e6 100644 --- a/aprsd/plugins/location.py +++ b/aprsd/plugins/location.py @@ -2,7 +2,7 @@ import logging import re import time -from aprsd import plugin, plugin_utils, trace, utils +from aprsd import config, plugin, plugin_utils, trace LOG = logging.getLogger("APRSD") @@ -24,7 +24,7 @@ class LocationPlugin(plugin.APRSDRegexCommandPluginBase): # get last location of a callsign, get descriptive name from weather service try: - utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"]) + config.check_config_option(self.config, ["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" diff --git a/aprsd/plugins/time.py b/aprsd/plugins/time.py index 81df83a..516643a 100644 --- a/aprsd/plugins/time.py +++ b/aprsd/plugins/time.py @@ -5,7 +5,7 @@ import time from opencage.geocoder import OpenCageGeocode import pytz -from aprsd import fuzzyclock, plugin, plugin_utils, trace, utils +from aprsd import config, fuzzyclock, plugin, plugin_utils, trace LOG = logging.getLogger("APRSD") @@ -64,7 +64,7 @@ class TimeOpenCageDataPlugin(TimePlugin): # get last location of a callsign, get descriptive name from weather service try: - utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"]) + config.check_config_option(self.config, ["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" @@ -95,7 +95,7 @@ class TimeOpenCageDataPlugin(TimePlugin): lon = aprs_data["entries"][0]["lng"] try: - utils.check_config_option(self.config, "opencagedata", "apiKey") + config.check_config_option(self.config, "opencagedata", "apiKey") except Exception as ex: LOG.error(f"Failed to find config opencage:apiKey {ex}") return "No opencage apiKey found" @@ -130,7 +130,7 @@ class TimeOWMPlugin(TimePlugin): # get last location of a callsign, get descriptive name from weather service try: - utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"]) + config.check_config_option(self.config, ["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" @@ -160,7 +160,7 @@ class TimeOWMPlugin(TimePlugin): lon = aprs_data["entries"][0]["lng"] try: - utils.check_config_option( + config.check_config_option( self.config, ["services", "openweathermap", "apiKey"], ) diff --git a/aprsd/plugins/weather.py b/aprsd/plugins/weather.py index 8eea849..a1dc2cf 100644 --- a/aprsd/plugins/weather.py +++ b/aprsd/plugins/weather.py @@ -4,7 +4,7 @@ import re import requests -from aprsd import plugin, plugin_utils, trace, utils +from aprsd import config, plugin, plugin_utils, trace LOG = logging.getLogger("APRSD") @@ -34,7 +34,7 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase): # message = packet.get("message_text", None) # ack = packet.get("msgNo", "0") try: - utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"]) + config.check_config_option(self.config, ["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" @@ -115,7 +115,7 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase): fromcall = fromcall try: - utils.check_config_option( + config.check_config_option( self.config, ["services", "aprs.fi", "apiKey"], ) @@ -199,7 +199,7 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase): searchcall = fromcall try: - utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"]) + config.check_config_option(self.config, ["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" @@ -220,7 +220,7 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase): lon = aprs_data["entries"][0]["lng"] try: - utils.check_config_option( + config.check_config_option( self.config, ["services", "openweathermap", "apiKey"], ) @@ -229,7 +229,7 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase): return "No openweathermap apiKey found" try: - utils.check_config_option(self.config, ["aprsd", "units"]) + config.check_config_option(self.config, ["aprsd", "units"]) except Exception: LOG.debug("Couldn't find untis in aprsd:services:units") units = "metric" @@ -323,7 +323,7 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase): searchcall = fromcall try: - utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"]) + config.check_config_option(self.config, ["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" @@ -344,13 +344,13 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase): lon = aprs_data["entries"][0]["lng"] try: - utils.check_config_option(self.config, ["services", "avwx", "apiKey"]) + config.check_config_option(self.config, ["services", "avwx", "apiKey"]) except Exception as ex: LOG.error(f"Failed to find config avwx:apiKey {ex}") return "No avwx apiKey found" try: - utils.check_config_option(self.config, ["services", "avwx", "base_url"]) + config.check_config_option(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" diff --git a/aprsd/utils.py b/aprsd/utils.py index 1f55e06..d639582 100644 --- a/aprsd/utils.py +++ b/aprsd/utils.py @@ -3,128 +3,13 @@ import collections import errno import functools -import logging import os -from pathlib import Path import re -import sys import threading -import click import update_checker -import yaml import aprsd -from aprsd import plugin - - -LOG_LEVELS = { - "CRITICAL": logging.CRITICAL, - "ERROR": logging.ERROR, - "WARNING": logging.WARNING, - "INFO": logging.INFO, - "DEBUG": logging.DEBUG, -} - -DEFAULT_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p" -DEFAULT_LOG_FORMAT = ( - "[%(asctime)s] [%(threadName)-20.20s] [%(levelname)-5.5s]" - " %(message)s - [%(pathname)s:%(lineno)d]" -) - -QUEUE_DATE_FORMAT = "[%m/%d/%Y] [%I:%M:%S %p]" -QUEUE_LOG_FORMAT = ( - "%(asctime)s [%(threadName)-20.20s] [%(levelname)-5.5s]" - " %(message)s - [%(pathname)s:%(lineno)d]" -) - -# an example of what should be in the ~/.aprsd/config.yml -DEFAULT_CONFIG_DICT = { - "ham": {"callsign": "NOCALL"}, - "aprs": { - "enabled": True, - "login": "CALLSIGN", - "password": "00000", - "host": "rotate.aprs2.net", - "port": 14580, - }, - "kiss": { - "tcp": { - "enabled": False, - "host": "direwolf.ip.address", - "port": "8001", - }, - "serial": { - "enabled": False, - "device": "/dev/ttyS0", - "baudrate": 9600, - }, - }, - "aprsd": { - "logfile": "/tmp/aprsd.log", - "logformat": DEFAULT_LOG_FORMAT, - "dateformat": DEFAULT_DATE_FORMAT, - "trace": False, - "enabled_plugins": plugin.CORE_MESSAGE_PLUGINS, - "units": "imperial", - "watch_list": { - "enabled": False, - # Who gets the alert? - "alert_callsign": "NOCALL", - # 43200 is 12 hours - "alert_time_seconds": 43200, - # How many packets to save in a ring Buffer - # for a particular callsign - "packet_keep_count": 10, - "callsigns": [], - "enabled_plugins": plugin.CORE_NOTIFY_PLUGINS, - }, - "web": { - "enabled": True, - "logging_enabled": True, - "host": "0.0.0.0", - "port": 8001, - "users": { - "admin": "password-here", - }, - }, - "email": { - "enabled": True, - "shortcuts": { - "aa": "5551239999@vtext.com", - "cl": "craiglamparter@somedomain.org", - "wb": "555309@vtext.com", - }, - "smtp": { - "login": "SMTP_USERNAME", - "password": "SMTP_PASSWORD", - "host": "smtp.gmail.com", - "port": 465, - "use_ssl": False, - "debug": False, - }, - "imap": { - "login": "IMAP_USERNAME", - "password": "IMAP_PASSWORD", - "host": "imap.gmail.com", - "port": 993, - "use_ssl": True, - "debug": False, - }, - }, - }, - "services": { - "aprs.fi": {"apiKey": "APIKEYVALUE"}, - "openweathermap": {"apiKey": "APIKEYVALUE"}, - "opencagedata": {"apiKey": "APIKEYVALUE"}, - "avwx": {"base_url": "http://host:port", "apiKey": "APIKEYVALUE"}, - }, -} - -home = str(Path.home()) -DEFAULT_CONFIG_DIR = f"{home}/.config/aprsd/" -DEFAULT_SAVE_FILE = f"{home}/.config/aprsd/aprsd.p" -DEFAULT_CONFIG_FILE = f"{home}/.config/aprsd/aprsd.yml" def synchronized(wrapped): @@ -175,239 +60,6 @@ def end_substr(original, substr): return idx -def dump_default_cfg(): - return add_config_comments( - yaml.dump( - DEFAULT_CONFIG_DICT, - indent=4, - ), - ) - - -def add_config_comments(raw_yaml): - end_idx = end_substr(raw_yaml, "aprs:") - if end_idx != -1: - # lets insert a comment - raw_yaml = insert_str( - raw_yaml, - "\n # Set enabled to False if there is no internet connectivity." - "\n # This is useful for a direwolf KISS aprs connection only. " - "\n" - "\n # Get the passcode for your callsign here: " - "\n # https://apps.magicbug.co.uk/passcode", - end_idx, - ) - - end_idx = end_substr(raw_yaml, "aprs.fi:") - if end_idx != -1: - # lets insert a comment - raw_yaml = insert_str( - raw_yaml, - "\n # Get the apiKey from your aprs.fi account here: " - "\n # http://aprs.fi/account", - end_idx, - ) - - end_idx = end_substr(raw_yaml, "opencagedata:") - if end_idx != -1: - # lets insert a comment - raw_yaml = insert_str( - raw_yaml, - "\n # (Optional for TimeOpenCageDataPlugin) " - "\n # Get the apiKey from your opencagedata account here: " - "\n # https://opencagedata.com/dashboard#api-keys", - end_idx, - ) - - end_idx = end_substr(raw_yaml, "openweathermap:") - if end_idx != -1: - # lets insert a comment - raw_yaml = insert_str( - raw_yaml, - "\n # (Optional for OWMWeatherPlugin) " - "\n # Get the apiKey from your " - "\n # openweathermap account here: " - "\n # https://home.openweathermap.org/api_keys", - end_idx, - ) - - end_idx = end_substr(raw_yaml, "avwx:") - if end_idx != -1: - # lets insert a comment - raw_yaml = insert_str( - raw_yaml, - "\n # (Optional for AVWXWeatherPlugin) " - "\n # Use hosted avwx-api here: https://avwx.rest " - "\n # or deploy your own from here: " - "\n # https://github.com/avwx-rest/avwx-api", - end_idx, - ) - - return raw_yaml - - -def create_default_config(): - """Create a default config file.""" - # make sure the directory location exists - config_file_expanded = os.path.expanduser(DEFAULT_CONFIG_FILE) - config_dir = os.path.dirname(config_file_expanded) - if not os.path.exists(config_dir): - click.echo(f"Config dir '{config_dir}' doesn't exist, creating.") - mkdir_p(config_dir) - with open(config_file_expanded, "w+") as cf: - cf.write(dump_default_cfg()) - - -def get_config(config_file): - """This tries to read the yaml config from .""" - config_file_expanded = os.path.expanduser(config_file) - if os.path.exists(config_file_expanded): - with open(config_file_expanded) as stream: - config = yaml.load(stream, Loader=yaml.FullLoader) - return config - else: - if config_file == DEFAULT_CONFIG_FILE: - click.echo( - f"{config_file_expanded} is missing, creating config file", - ) - create_default_config() - msg = ( - "Default config file created at {}. Please edit with your " - "settings.".format(config_file) - ) - click.echo(msg) - else: - # The user provided a config file path different from the - # Default, so we won't try and create it, just bitch and bail. - msg = f"Custom config file '{config_file}' is missing." - click.echo(msg) - - sys.exit(-1) - - -def conf_option_exists(conf, chain): - _key = chain.pop(0) - if _key in conf: - return conf_option_exists(conf[_key], chain) if chain else conf[_key] - - -def check_config_option(config, chain, default_fail=None): - result = conf_option_exists(config, chain.copy()) - if result is None: - raise Exception( - "'{}' was not in config file".format( - chain, - ), - ) - else: - if default_fail: - if result == default_fail: - # We have to fail and bail if the user hasn't edited - # this config option. - raise Exception( - "Config file needs to be edited from provided defaults for {}.".format( - chain, - ), - ) - else: - return config - - -# This method tries to parse the config yaml file -# and consume the settings. -# If the required params don't exist, -# it will look in the environment -def parse_config(config_file): - # for now we still use globals....ugh - global CONFIG - - def fail(msg): - click.echo(msg) - sys.exit(-1) - - def check_option(config, chain, default_fail=None): - try: - config = check_config_option(config, chain, default_fail=default_fail) - except Exception as ex: - fail(repr(ex)) - else: - return config - - config = get_config(config_file) - - # special check here to make sure user has edited the config file - # and changed the ham callsign - check_option( - config, - [ - "ham", - "callsign", - ], - default_fail=DEFAULT_CONFIG_DICT["ham"]["callsign"], - ) - check_option( - config, - ["services", "aprs.fi", "apiKey"], - default_fail=DEFAULT_CONFIG_DICT["services"]["aprs.fi"]["apiKey"], - ) - check_option( - config, - ["aprs", "login"], - default_fail=DEFAULT_CONFIG_DICT["aprs"]["login"], - ) - check_option( - config, - ["aprs", "password"], - default_fail=DEFAULT_CONFIG_DICT["aprs"]["password"], - ) - - # Ensure they change the admin password - if config["aprsd"]["web"]["enabled"] is True: - check_option( - config, - ["aprsd", "web", "users", "admin"], - default_fail=DEFAULT_CONFIG_DICT["aprsd"]["web"]["users"]["admin"], - ) - - if config["aprsd"]["watch_list"]["enabled"] is True: - check_option( - config, - ["aprsd", "watch_list", "alert_callsign"], - default_fail=DEFAULT_CONFIG_DICT["aprsd"]["watch_list"]["alert_callsign"], - ) - - if config["aprsd"]["email"]["enabled"] is True: - # Check IMAP server settings - check_option(config, ["aprsd", "email", "imap", "host"]) - check_option(config, ["aprsd", "email", "imap", "port"]) - check_option( - config, - ["aprsd", "email", "imap", "login"], - default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["login"], - ) - check_option( - config, - ["aprsd", "email", "imap", "password"], - default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["password"], - ) - - # Check SMTP server settings - check_option(config, ["aprsd", "email", "smtp", "host"]) - check_option(config, ["aprsd", "email", "smtp", "port"]) - check_option( - config, - ["aprsd", "email", "smtp", "login"], - default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["login"], - ) - check_option( - config, - ["aprsd", "email", "smtp", "password"], - default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["password"], - ) - - return config - - def human_size(bytes, units=None): """Returns a human readable string representation of bytes""" if not units: diff --git a/docker/Dockerfile b/docker/Dockerfile index a928482..a07b82f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -28,7 +28,7 @@ RUN addgroup --gid $GID $APRS_USER RUN useradd -m -u $UID -g $APRS_USER $APRS_USER # Install aprsd -RUN /usr/local/bin/pip3 install aprsd==2.3.0 +RUN /usr/local/bin/pip3 install aprsd==2.3.1 # Ensure /config is there with a default config file USER root diff --git a/docker/build.sh b/docker/build.sh index 064b1cd..6ab4edf 100755 --- a/docker/build.sh +++ b/docker/build.sh @@ -36,7 +36,7 @@ do esac done -VERSION="2.2.1" +VERSION="2.3.1" if [ $ALL_PLATFORMS -eq 1 ] then diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 7a5d575..df4337e 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -4,7 +4,7 @@ from unittest import mock import pytz import aprsd -from aprsd import messaging, packets, stats, utils +from aprsd import config, messaging, packets, stats from aprsd.fuzzyclock import fuzzy from aprsd.plugins import fortune as fortune_plugin from aprsd.plugins import ping as ping_plugin @@ -19,7 +19,7 @@ class TestPlugin(unittest.TestCase): def setUp(self): self.fromcall = fake.FAKE_FROM_CALLSIGN self.ack = 1 - self.config = utils.DEFAULT_CONFIG_DICT + self.config = config.DEFAULT_CONFIG_DICT self.config["ham"]["callsign"] = self.fromcall self.config["aprs"]["login"] = fake.FAKE_TO_CALLSIGN # Inintialize the stats object with the config