1
0
mirror of https://github.com/craigerl/aprsd.git synced 2025-08-01 13:12:26 -04:00

Merge pull request #70 from craigerl/utils_refactor

Refactoring/Cleanup
This commit is contained in:
Walter A. Boring IV 2021-10-08 08:41:14 -04:00 committed by GitHub
commit 14f77876f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1012 additions and 978 deletions

View File

@ -1,26 +1,30 @@
import abc
import logging import logging
import select
import time import time
import aprslib import aprslib
from aprslib import is_py3 from aprslib.exceptions import LoginError
from aprslib.exceptions import (
ConnectionDrop, ConnectionError, GenericError, LoginError, ParseError,
UnknownFormat,
)
import aprsd from aprsd import trace
from aprsd import stats from aprsd.clients import aprsis, kiss
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
TRANSPORT_APRSIS = "aprsis"
TRANSPORT_TCPKISS = "tcpkiss"
TRANSPORT_SERIALKISS = "serialkiss"
# Main must create this from the ClientFactory
# object such that it's populated with the
# Correct config
factory = None
class Client: class Client:
"""Singleton client class that constructs the aprslib connection.""" """Singleton client class that constructs the aprslib connection."""
_instance = None _instance = None
aprs_client = None _client = None
config = None config = None
connected = False connected = False
@ -38,21 +42,51 @@ class Client:
if config: if config:
self.config = config self.config = config
def new(self):
obj = super().__new__(Client)
obj.config = self.config
return obj
@property @property
def client(self): def client(self):
if not self.aprs_client: if not self._client:
self.aprs_client = self.setup_connection() self._client = self.setup_connection()
return self.aprs_client return self._client
def reset(self): def reset(self):
"""Call this to force a rebuild/reconnect.""" """Call this to force a rebuild/reconnect."""
del self.aprs_client del self._client
@abc.abstractmethod
def setup_connection(self):
pass
@staticmethod
@abc.abstractmethod
def is_enabled(config):
pass
@staticmethod
@abc.abstractmethod
def transport(config):
pass
@abc.abstractmethod
def decode_packet(self, *args, **kwargs):
pass
class APRSISClient(Client):
@staticmethod
def is_enabled(config):
# Defaults to True if the enabled flag is non existent
return config["aprs"].get("enabled", True)
@staticmethod
def transport(config):
return TRANSPORT_APRSIS
def decode_packet(self, *args, **kwargs):
"""APRS lib already decodes this."""
return args[0]
@trace.trace
def setup_connection(self): def setup_connection(self):
user = self.config["aprs"]["login"] user = self.config["aprs"]["login"]
password = self.config["aprs"]["password"] password = self.config["aprs"]["password"]
@ -60,10 +94,11 @@ class Client:
port = self.config["aprs"].get("port", 14580) port = self.config["aprs"].get("port", 14580)
connected = False connected = False
backoff = 1 backoff = 1
aprs_client = None
while not connected: while not connected:
try: try:
LOG.info("Creating aprslib client") LOG.info("Creating aprslib client")
aprs_client = Aprsdis(user, passwd=password, host=host, port=port) aprs_client = aprsis.Aprsdis(user, passwd=password, host=host, port=port)
# Force the logging to be the same # Force the logging to be the same
aprs_client.logger = LOG aprs_client.logger = LOG
aprs_client.connect() aprs_client.connect()
@ -82,200 +117,92 @@ class Client:
return aprs_client return aprs_client
class Aprsdis(aprslib.IS): class KISSClient(Client):
"""Extend the aprslib class so we can exit properly."""
# flag to tell us to stop @staticmethod
thread_stop = False def is_enabled(config):
"""Return if tcp or serial KISS is enabled."""
if "kiss" not in config:
return False
# timeout in seconds if config.get("kiss.serial.enabled", default=False):
select_timeout = 1 return True
def stop(self): if config.get("kiss.tcp.enabled", default=False):
self.thread_stop = True return True
LOG.info("Shutdown Aprsdis client.")
def send(self, msg): @staticmethod
"""Send an APRS Message object.""" def transport(config):
line = str(msg) if config.get("kiss.serial.enabled", default=False):
self.sendall(line) return TRANSPORT_SERIALKISS
def _socket_readlines(self, blocking=False): if config.get("kiss.tcp.enabled", default=False):
""" return TRANSPORT_TCPKISS
Generator for complete lines, received from the server
"""
try:
self.sock.setblocking(0)
except OSError as e:
self.logger.error(f"socket error when setblocking(0): {str(e)}")
raise aprslib.ConnectionDrop("connection dropped")
while not self.thread_stop: def decode_packet(self, *args, **kwargs):
short_buf = b"" """We get a frame, which has to be decoded."""
newline = b"\r\n" frame = kwargs["frame"]
LOG.debug(f"Got an APRS Frame '{frame}'")
# try and nuke the * from the fromcall sign.
frame.header._source._ch = False
payload = str(frame.payload.decode())
msg = f"{str(frame.header)}:{payload}"
# msg = frame.tnc2
LOG.debug(f"Decoding {msg}")
# set a select timeout, so we get a chance to exit packet = aprslib.parse(msg)
# when user hits CTRL-C return packet
readable, writable, exceptional = select.select(
[self.sock],
[],
[],
self.select_timeout,
)
if not readable:
if not blocking:
break
else:
continue
try: @trace.trace
short_buf = self.sock.recv(4096) def setup_connection(self):
ax25client = kiss.Aioax25Client(self.config)
# sock.recv returns empty if the connection drops return ax25client
if not short_buf:
if not blocking:
# We could just not be blocking, so empty is expected
continue
else:
self.logger.error("socket.recv(): returned empty")
raise aprslib.ConnectionDrop("connection dropped")
except OSError as e:
# self.logger.error("socket error on recv(): %s" % str(e))
if "Resource temporarily unavailable" in str(e):
if not blocking:
if len(self.buf) == 0:
break
self.buf += short_buf
while newline in self.buf:
line, self.buf = self.buf.split(newline, 1)
yield line
def _send_login(self):
"""
Sends login string to server
"""
login_str = "user {0} pass {1} vers github.com/craigerl/aprsd {3}{2}\r\n"
login_str = login_str.format(
self.callsign,
self.passwd,
(" filter " + self.filter) if self.filter != "" else "",
aprsd.__version__,
)
self.logger.info("Sending login information")
try:
self._sendall(login_str)
self.sock.settimeout(5)
test = self.sock.recv(len(login_str) + 100)
if is_py3:
test = test.decode("latin-1")
test = test.rstrip()
self.logger.debug("Server: %s", test)
a, b, callsign, status, e = test.split(" ", 4)
s = e.split(",")
if len(s):
server_string = s[0].replace("server ", "")
else:
server_string = e.replace("server ", "")
self.logger.info(f"Connected to {server_string}")
self.server_string = server_string
stats.APRSDStats().set_aprsis_server(server_string)
if callsign == "":
raise LoginError("Server responded with empty callsign???")
if callsign != self.callsign:
raise LoginError(f"Server: {test}")
if status != "verified," and self.passwd != "-1":
raise LoginError("Password is incorrect")
if self.passwd == "-1":
self.logger.info("Login successful (receive only)")
else:
self.logger.info("Login successful")
except LoginError as e:
self.logger.error(str(e))
self.close()
raise
except Exception as e:
self.close()
self.logger.error(f"Failed to login '{e}'")
raise LoginError("Failed to login")
def consumer(self, callback, blocking=True, immortal=False, raw=False):
"""
When a position sentence is received, it will be passed to the callback function
blocking: if true (default), runs forever, otherwise will return after one sentence
You can still exit the loop, by raising StopIteration in the callback function
immortal: When true, consumer will try to reconnect and stop propagation of Parse exceptions
if false (default), consumer will return
raw: when true, raw packet is passed to callback, otherwise the result from aprs.parse()
"""
if not self._connected:
raise ConnectionError("not connected to a server")
line = b""
while True and not self.thread_stop:
try:
for line in self._socket_readlines(blocking):
if line[0:1] != b"#":
if raw:
callback(line)
else:
callback(self._parse(line))
else:
self.logger.debug("Server: %s", line.decode("utf8"))
stats.APRSDStats().set_aprsis_keepalive()
except ParseError as exp:
self.logger.log(
11,
"%s\n Packet: %s",
exp,
exp.packet,
)
except UnknownFormat as exp:
self.logger.log(
9,
"%s\n Packet: %s",
exp,
exp.packet,
)
except LoginError as exp:
self.logger.error("%s: %s", exp.__class__.__name__, exp)
except (KeyboardInterrupt, SystemExit):
raise
except (ConnectionDrop, ConnectionError):
self.close()
if not immortal:
raise
else:
self.connect(blocking=blocking)
continue
except GenericError:
pass
except StopIteration:
break
except Exception:
self.logger.error("APRS Packet: %s", line)
raise
if not blocking:
break
def get_client(): class ClientFactory:
cl = Client() _instance = None
return cl.client
def __new__(cls, *args, **kwargs):
"""This magic turns this into a singleton."""
if cls._instance is None:
cls._instance = super().__new__(cls)
# Put any initialization here.
return cls._instance
def __init__(self, config):
self.config = config
self._builders = {}
def register(self, key, builder):
self._builders[key] = builder
def create(self, key=None):
if not key:
if APRSISClient.is_enabled(self.config):
key = TRANSPORT_APRSIS
elif KISSClient.is_enabled(self.config):
key = KISSClient.transport(self.config)
LOG.debug(f"GET client {key}")
builder = self._builders.get(key)
if not builder:
raise ValueError(key)
return builder(self.config)
def is_client_enabled(self):
"""Make sure at least one client is enabled."""
enabled = False
for key in self._builders.keys():
enabled |= self._builders[key].is_enabled(self.config)
return enabled
@staticmethod
def setup(config):
"""Create and register all possible client objects."""
global factory
factory = ClientFactory(config)
factory.register(TRANSPORT_APRSIS, APRSISClient)
factory.register(TRANSPORT_TCPKISS, KISSClient)
factory.register(TRANSPORT_SERIALKISS, KISSClient)

209
aprsd/clients/aprsis.py Normal file
View File

@ -0,0 +1,209 @@
import logging
import select
import aprslib
from aprslib import is_py3
from aprslib.exceptions import (
ConnectionDrop, ConnectionError, GenericError, LoginError, ParseError,
UnknownFormat,
)
import aprsd
from aprsd import stats
LOG = logging.getLogger("APRSD")
class Aprsdis(aprslib.IS):
"""Extend the aprslib class so we can exit properly."""
# flag to tell us to stop
thread_stop = False
# timeout in seconds
select_timeout = 1
def stop(self):
self.thread_stop = True
LOG.info("Shutdown Aprsdis client.")
def send(self, msg):
"""Send an APRS Message object."""
line = str(msg)
self.sendall(line)
def _socket_readlines(self, blocking=False):
"""
Generator for complete lines, received from the server
"""
try:
self.sock.setblocking(0)
except OSError as e:
self.logger.error(f"socket error when setblocking(0): {str(e)}")
raise aprslib.ConnectionDrop("connection dropped")
while not self.thread_stop:
short_buf = b""
newline = b"\r\n"
# set a select timeout, so we get a chance to exit
# when user hits CTRL-C
readable, writable, exceptional = select.select(
[self.sock],
[],
[],
self.select_timeout,
)
if not readable:
if not blocking:
break
else:
continue
try:
short_buf = self.sock.recv(4096)
# sock.recv returns empty if the connection drops
if not short_buf:
if not blocking:
# We could just not be blocking, so empty is expected
continue
else:
self.logger.error("socket.recv(): returned empty")
raise aprslib.ConnectionDrop("connection dropped")
except OSError as e:
# self.logger.error("socket error on recv(): %s" % str(e))
if "Resource temporarily unavailable" in str(e):
if not blocking:
if len(self.buf) == 0:
break
self.buf += short_buf
while newline in self.buf:
line, self.buf = self.buf.split(newline, 1)
yield line
def _send_login(self):
"""
Sends login string to server
"""
login_str = "user {0} pass {1} vers github.com/craigerl/aprsd {3}{2}\r\n"
login_str = login_str.format(
self.callsign,
self.passwd,
(" filter " + self.filter) if self.filter != "" else "",
aprsd.__version__,
)
self.logger.info("Sending login information")
try:
self._sendall(login_str)
self.sock.settimeout(5)
test = self.sock.recv(len(login_str) + 100)
if is_py3:
test = test.decode("latin-1")
test = test.rstrip()
self.logger.debug("Server: %s", test)
a, b, callsign, status, e = test.split(" ", 4)
s = e.split(",")
if len(s):
server_string = s[0].replace("server ", "")
else:
server_string = e.replace("server ", "")
self.logger.info(f"Connected to {server_string}")
self.server_string = server_string
stats.APRSDStats().set_aprsis_server(server_string)
if callsign == "":
raise LoginError("Server responded with empty callsign???")
if callsign != self.callsign:
raise LoginError(f"Server: {test}")
if status != "verified," and self.passwd != "-1":
raise LoginError("Password is incorrect")
if self.passwd == "-1":
self.logger.info("Login successful (receive only)")
else:
self.logger.info("Login successful")
except LoginError as e:
self.logger.error(str(e))
self.close()
raise
except Exception as e:
self.close()
self.logger.error(f"Failed to login '{e}'")
raise LoginError("Failed to login")
def consumer(self, callback, blocking=True, immortal=False, raw=False):
"""
When a position sentence is received, it will be passed to the callback function
blocking: if true (default), runs forever, otherwise will return after one sentence
You can still exit the loop, by raising StopIteration in the callback function
immortal: When true, consumer will try to reconnect and stop propagation of Parse exceptions
if false (default), consumer will return
raw: when true, raw packet is passed to callback, otherwise the result from aprs.parse()
"""
if not self._connected:
raise ConnectionError("not connected to a server")
line = b""
while True and not self.thread_stop:
try:
for line in self._socket_readlines(blocking):
if line[0:1] != b"#":
if raw:
callback(line)
else:
callback(self._parse(line))
else:
self.logger.debug("Server: %s", line.decode("utf8"))
stats.APRSDStats().set_aprsis_keepalive()
except ParseError as exp:
self.logger.log(
11,
"%s\n Packet: %s",
exp,
exp.packet,
)
except UnknownFormat as exp:
self.logger.log(
9,
"%s\n Packet: %s",
exp,
exp.packet,
)
except LoginError as exp:
self.logger.error("%s: %s", exp.__class__.__name__, exp)
except (KeyboardInterrupt, SystemExit):
raise
except (ConnectionDrop, ConnectionError):
self.close()
if not immortal:
raise
else:
self.connect(blocking=blocking)
continue
except GenericError:
pass
except StopIteration:
break
except Exception:
self.logger.error("APRS Packet: %s", line)
raise
if not blocking:
break

View File

@ -5,83 +5,20 @@ from aioax25 import interface
from aioax25 import kiss as kiss from aioax25 import kiss as kiss
from aioax25.aprs import APRSInterface from aioax25.aprs import APRSInterface
from aprsd import trace
TRANSPORT_TCPKISS = "tcpkiss"
TRANSPORT_SERIALKISS = "serialkiss"
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
class KISSClient:
_instance = None
config = None
ax25client = None
loop = None
def __new__(cls, *args, **kwargs):
"""Singleton for this class."""
if cls._instance is None:
cls._instance = super().__new__(cls)
# initialize shit here
return cls._instance
def __init__(self, config=None):
if config:
self.config = config
@staticmethod
def kiss_enabled(config):
"""Return if tcp or serial KISS is enabled."""
if "kiss" not in config:
return False
if "serial" in config["kiss"]:
if config["kiss"]["serial"].get("enabled", False):
return True
if "tcp" in config["kiss"]:
if config["kiss"]["tcp"].get("enabled", False):
return True
@staticmethod
def transport(config):
if "serial" in config["kiss"]:
if config["kiss"]["serial"].get("enabled", False):
return TRANSPORT_SERIALKISS
if "tcp" in config["kiss"]:
if config["kiss"]["tcp"].get("enabled", False):
return TRANSPORT_TCPKISS
@property
def client(self):
if not self.ax25client:
self.ax25client = self.setup_connection()
return self.ax25client
def reset(self):
"""Call this to fore a rebuild/reconnect."""
self.ax25client.stop()
del self.ax25client
@trace.trace
def setup_connection(self):
ax25client = Aioax25Client(self.config)
LOG.debug("Complete")
return ax25client
class Aioax25Client: class Aioax25Client:
def __init__(self, config): def __init__(self, config):
self.config = config self.config = config
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
self.loop = asyncio.get_event_loop()
self.setup() self.setup()
def setup(self): def setup(self):
# we can be TCP kiss or Serial kiss # we can be TCP kiss or Serial kiss
self.loop = asyncio.get_event_loop()
if "serial" in self.config["kiss"] and self.config["kiss"]["serial"].get( if "serial" in self.config["kiss"] and self.config["kiss"]["serial"].get(
"enabled", "enabled",
False, False,
@ -131,10 +68,20 @@ class Aioax25Client:
self.kissdev._close() self.kissdev._close()
self.loop.stop() self.loop.stop()
def consumer(self, callback, callsign=None): def set_filter(self, filter):
if not callsign: # This does nothing right now.
callsign = self.config["ham"]["callsign"] pass
self.aprsint.bind(callback=callback, callsign="WB4BOR", ssid=12, regex=False)
def consumer(self, callback, blocking=True, immortal=False, raw=False):
callsign = self.config["kiss"]["callsign"]
call = callsign.split("-")
if len(call) > 1:
callsign = call[0]
ssid = int(call[1])
else:
ssid = 0
self.aprsint.bind(callback=callback, callsign=callsign, ssid=ssid, regex=False)
self.loop.run_forever()
def send(self, msg): def send(self, msg):
"""Send an APRS Message object.""" """Send an APRS Message object."""
@ -145,8 +92,3 @@ class Aioax25Client:
path=["WIDE1-1", "WIDE2-1"], path=["WIDE1-1", "WIDE2-1"],
oneshot=True, oneshot=True,
) )
def get_client():
cl = KISSClient()
return cl.client

389
aprsd/config.py Normal file
View File

@ -0,0 +1,389 @@
import collections
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"
class Config(collections.UserDict):
def _get(self, d, keys, default=None):
"""
Example:
d = {'meta': {'status': 'OK', 'status_code': 200}}
deep_get(d, ['meta', 'status_code']) # => 200
deep_get(d, ['garbage', 'status_code']) # => None
deep_get(d, ['meta', 'garbage'], default='-') # => '-'
"""
if type(keys) is str and "." in keys:
keys = keys.split(".")
assert type(keys) is list
if d is None:
return default
if not keys:
return d
if type(d) is str:
return default
return self._get(d.get(keys[0]), keys[1:], default)
def get(self, path, default=None):
return self._get(self.data, path, default=default)
def exists(self, path):
"""See if a conf value exists."""
test = "-3.14TEST41.3-"
return (self.get(path, default=test) != test)
def check_option(self, path, default_fail=None):
"""Make sure the config option doesn't have default value."""
if not self.exists(path):
raise Exception(
"Option '{}' was not in config file".format(
path,
),
)
val = self.get(path)
if val == default_fail:
# We have to fail and bail if the user hasn't edited
# this config option.
raise Exception(
"Config file needs to be changed from provided"
" defaults for '{}'".format(
path,
),
)
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>."""
config_file_expanded = os.path.expanduser(config_file)
if os.path.exists(config_file_expanded):
with open(config_file_expanded) as stream:
config = yaml.load(stream, Loader=yaml.FullLoader)
return Config(config)
else:
if config_file == DEFAULT_CONFIG_FILE:
click.echo(
f"{config_file_expanded} is missing, creating config file",
)
create_default_config()
msg = (
"Default config file created at {}. Please edit with your "
"settings.".format(config_file)
)
click.echo(msg)
else:
# The user provided a config file path different from the
# Default, so we won't try and create it, just bitch and bail.
msg = f"Custom config file '{config_file}' is missing."
click.echo(msg)
sys.exit(-1)
# This method tries to parse the config yaml file
# and consume the settings.
# If the required params don't exist,
# it will look in the environment
def parse_config(config_file):
config = get_config(config_file)
def fail(msg):
click.echo(msg)
sys.exit(-1)
def check_option(config, path, default_fail=None):
try:
config.check_option(path, default_fail=default_fail)
except Exception as ex:
fail(repr(ex))
else:
return config
# special check here to make sure user has edited the config file
# and changed the ham callsign
check_option(
config,
"ham.callsign",
default_fail=DEFAULT_CONFIG_DICT["ham"]["callsign"],
)
check_option(
config,
["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.get("aprsd.web.enabled") is True:
check_option(
config,
["aprsd", "web", "users", "admin"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["web"]["users"]["admin"],
)
if config.get("aprsd.watch_list.enabled") is True:
check_option(
config,
["aprsd", "watch_list", "alert_callsign"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["watch_list"]["alert_callsign"],
)
if config.get("aprsd.email.enabled") is True:
# Check IMAP server settings
check_option(config, ["aprsd", "email", "imap", "host"])
check_option(config, ["aprsd", "email", "imap", "port"])
check_option(
config,
["aprsd", "email", "imap", "login"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["login"],
)
check_option(
config,
["aprsd", "email", "imap", "password"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["password"],
)
# Check SMTP server settings
check_option(config, ["aprsd", "email", "smtp", "host"])
check_option(config, ["aprsd", "email", "smtp", "port"])
check_option(
config,
["aprsd", "email", "smtp", "login"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["login"],
)
check_option(
config,
["aprsd", "email", "smtp", "password"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["password"],
)
return config

View File

@ -14,7 +14,9 @@ import click_completion
# local imports here # local imports here
import aprsd 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 # setup the global logger
@ -156,7 +158,7 @@ def setup_logging(config, loglevel, quiet):
"--config", "--config",
"config_file", "config_file",
show_default=True, show_default=True,
default=utils.DEFAULT_CONFIG_FILE, default=aprsd_config.DEFAULT_CONFIG_FILE,
help="The aprsd config file to use for options.", help="The aprsd config file to use for options.",
) )
@click.option( @click.option(
@ -178,7 +180,7 @@ def test_plugin(
): ):
"""APRSD Plugin test app.""" """APRSD Plugin test app."""
config = utils.parse_config(config_file) config = aprsd_config.parse_config(config_file)
setup_logging(config, loglevel, False) setup_logging(config, loglevel, False)
LOG.info(f"Test APRSD PLugin version: {aprsd.__version__}") LOG.info(f"Test APRSD PLugin version: {aprsd.__version__}")
@ -188,7 +190,9 @@ def test_plugin(
client.Client(config) client.Client(config)
pm = plugin.PluginManager(config) pm = plugin.PluginManager(config)
pm._init()
obj = pm._create_class(plugin_path, plugin.APRSDPluginBase, config=config) obj = pm._create_class(plugin_path, plugin.APRSDPluginBase, config=config)
pm._pluggy_pm.register(obj)
login = config["aprs"]["login"] login = config["aprs"]["login"]
packet = { packet = {
@ -198,7 +202,7 @@ def test_plugin(
"msgNo": 1, "msgNo": 1,
} }
reply = obj.filter(packet) reply = pm.run(packet)
# Plugin might have threads, so lets stop them so we can exit. # Plugin might have threads, so lets stop them so we can exit.
obj.stop_threads() obj.stop_threads()
LOG.info(f"Result = '{reply}'") LOG.info(f"Result = '{reply}'")

View File

@ -17,9 +17,10 @@ from flask_socketio import Namespace, SocketIO
from werkzeug.security import check_password_hash, generate_password_hash from werkzeug.security import check_password_hash, generate_password_hash
import aprsd import aprsd
from aprsd import ( from aprsd import client
client, kissclient, messaging, packets, plugin, stats, threads, utils, from aprsd import config as aprsd_config
) from aprsd import messaging, packets, plugin, stats, threads, utils
from aprsd.clients import aprsis
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -136,7 +137,8 @@ class SendMessageThread(threads.APRSDThread):
while not connected: while not connected:
try: try:
LOG.info("Creating aprslib client") LOG.info("Creating aprslib client")
aprs_client = client.Aprsdis(
aprs_client = aprsis.Aprsdis(
user, user,
passwd=password, passwd=password,
host=host, host=host,
@ -312,16 +314,16 @@ class APRSDFlask(flask_classful.FlaskView):
) )
else: else:
# We might be connected to a KISS socket? # We might be connected to a KISS socket?
if kissclient.KISSClient.kiss_enabled(self.config): if client.KISSClient.kiss_enabled(self.config):
transport = kissclient.KISSClient.transport(self.config) transport = client.KISSClient.transport(self.config)
if transport == kissclient.TRANSPORT_TCPKISS: if transport == client.TRANSPORT_TCPKISS:
aprs_connection = ( aprs_connection = (
"TCPKISS://{}:{}".format( "TCPKISS://{}:{}".format(
self.config["kiss"]["tcp"]["host"], self.config["kiss"]["tcp"]["host"],
self.config["kiss"]["tcp"]["port"], self.config["kiss"]["tcp"]["port"],
) )
) )
elif transport == kissclient.TRANSPORT_SERIALKISS: elif transport == client.TRANSPORT_SERIALKISS:
aprs_connection = ( aprs_connection = (
"SerialKISS://{}@{} baud".format( "SerialKISS://{}@{} baud".format(
self.config["kiss"]["serial"]["device"], self.config["kiss"]["serial"]["device"],
@ -338,7 +340,7 @@ class APRSDFlask(flask_classful.FlaskView):
aprs_connection=aprs_connection, aprs_connection=aprs_connection,
callsign=self.config["aprs"]["login"], callsign=self.config["aprs"]["login"],
version=aprsd.__version__, version=aprsd.__version__,
config_json=json.dumps(self.config), config_json=json.dumps(self.config.data),
watch_count=watch_count, watch_count=watch_count,
watch_age=watch_age, watch_age=watch_age,
plugin_count=plugin_count, plugin_count=plugin_count,
@ -553,10 +555,10 @@ def setup_logging(config, flask_app, loglevel, quiet):
flask_app.logger.disabled = True flask_app.logger.disabled = True
return return
log_level = utils.LOG_LEVELS[loglevel] log_level = aprsd_config.LOG_LEVELS[loglevel]
LOG.setLevel(log_level) LOG.setLevel(log_level)
log_format = config["aprsd"].get("logformat", utils.DEFAULT_LOG_FORMAT) log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT)
date_format = config["aprsd"].get("dateformat", utils.DEFAULT_DATE_FORMAT) date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT)
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
log_file = config["aprsd"].get("logfile", None) log_file = config["aprsd"].get("logfile", None)
if log_file: if log_file:

View File

@ -19,7 +19,7 @@ import requests
# local imports here # local imports here
import aprsd import aprsd
from aprsd import utils from aprsd import config as aprsd_config
# setup the global logger # setup the global logger
@ -172,7 +172,7 @@ def parse_delta_str(s):
"--config", "--config",
"config_file", "config_file",
show_default=True, show_default=True,
default=utils.DEFAULT_CONFIG_FILE, default=aprsd_config.DEFAULT_CONFIG_FILE,
help="The aprsd config file to use for options.", help="The aprsd config file to use for options.",
) )
@click.option( @click.option(
@ -191,7 +191,7 @@ def parse_delta_str(s):
def check(loglevel, config_file, health_url, timeout): def check(loglevel, config_file, health_url, timeout):
"""APRSD Plugin test app.""" """APRSD Plugin test app."""
config = utils.parse_config(config_file) config = aprsd_config.parse_config(config_file)
setup_logging(config, loglevel, False) setup_logging(config, loglevel, False)
LOG.debug(f"APRSD HealthCheck version: {aprsd.__version__}") LOG.debug(f"APRSD HealthCheck version: {aprsd.__version__}")

View File

@ -36,7 +36,9 @@ import click_completion
# local imports here # local imports here
import aprsd 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 # setup the global logger
@ -169,10 +171,10 @@ def signal_handler(sig, frame):
# to disable logging to stdout, but still log to file # to disable logging to stdout, but still log to file
# use the --quiet option on the cmdln # use the --quiet option on the cmdln
def setup_logging(config, loglevel, quiet): def setup_logging(config, loglevel, quiet):
log_level = utils.LOG_LEVELS[loglevel] log_level = aprsd_config.LOG_LEVELS[loglevel]
LOG.setLevel(log_level) LOG.setLevel(log_level)
log_format = config["aprsd"].get("logformat", utils.DEFAULT_LOG_FORMAT) log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT)
date_format = config["aprsd"].get("dateformat", utils.DEFAULT_DATE_FORMAT) date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT)
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
log_file = config["aprsd"].get("logfile", None) log_file = config["aprsd"].get("logfile", None)
if log_file: if log_file:
@ -218,7 +220,7 @@ def setup_logging(config, loglevel, quiet):
"--config", "--config",
"config_file", "config_file",
show_default=True, show_default=True,
default=utils.DEFAULT_CONFIG_FILE, default=aprsd_config.DEFAULT_CONFIG_FILE,
help="The aprsd config file to use for options.", help="The aprsd config file to use for options.",
) )
@click.option( @click.option(
@ -258,7 +260,7 @@ def listen(
"""Send a message to a callsign via APRS_IS.""" """Send a message to a callsign via APRS_IS."""
global got_ack, got_response global got_ack, got_response
config = utils.parse_config(config_file) config = aprsd_config.parse_config(config_file)
if not aprs_login: if not aprs_login:
click.echo("Must set --aprs_login or APRS_LOGIN") click.echo("Must set --aprs_login or APRS_LOGIN")
return return

View File

@ -37,9 +37,10 @@ import click_completion
# local imports here # local imports here
import aprsd import aprsd
from aprsd import ( from aprsd import (
client, flask, kissclient, messaging, packets, plugin, stats, threads, flask, messaging, packets, plugin, stats, threads, trace, utils,
trace, utils,
) )
from aprsd import client
from aprsd import config as aprsd_config
# setup the global logger # setup the global logger
@ -48,22 +49,8 @@ LOG = logging.getLogger("APRSD")
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
flask_enabled = False 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): def custom_startswith(string, incomplete):
"""A custom completion match that supports case insensitive matching.""" """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 # to disable logging to stdout, but still log to file
# use the --quiet option on the cmdln # use the --quiet option on the cmdln
def setup_logging(config, loglevel, quiet): def setup_logging(config, loglevel, quiet):
log_level = utils.LOG_LEVELS[loglevel] log_level = aprsd_config.LOG_LEVELS[loglevel]
LOG.setLevel(log_level) LOG.setLevel(log_level)
log_format = config["aprsd"].get("logformat", utils.DEFAULT_LOG_FORMAT) log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT)
date_format = config["aprsd"].get("dateformat", utils.DEFAULT_DATE_FORMAT) date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT)
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
log_file = config["aprsd"].get("logfile", None) log_file = config["aprsd"].get("logfile", None)
if log_file: if log_file:
@ -187,24 +174,17 @@ def setup_logging(config, loglevel, quiet):
LOG.addHandler(fh) LOG.addHandler(fh)
imap_logger = None imap_logger = None
if config["aprsd"]["email"].get("enabled", False) and config["aprsd"]["email"][ if config.get("aprsd.email.enabled", default=False) and config.get("aprsd.email.imap.debug", default=False):
"imap"
].get("debug", False):
imap_logger = logging.getLogger("imapclient.imaplib") imap_logger = logging.getLogger("imapclient.imaplib")
imap_logger.setLevel(log_level) imap_logger.setLevel(log_level)
imap_logger.addHandler(fh) imap_logger.addHandler(fh)
if ( if config.get("aprsd.web.enabled", default=False):
utils.check_config_option(
config, ["aprsd", "web", "enabled"],
default_fail=False,
)
):
qh = logging.handlers.QueueHandler(threads.logging_queue) qh = logging.handlers.QueueHandler(threads.logging_queue)
q_log_formatter = logging.Formatter( q_log_formatter = logging.Formatter(
fmt=utils.QUEUE_LOG_FORMAT, fmt=aprsd_config.QUEUE_LOG_FORMAT,
datefmt=utils.QUEUE_DATE_FORMAT, datefmt=aprsd_config.QUEUE_DATE_FORMAT,
) )
qh.setFormatter(q_log_formatter) qh.setFormatter(q_log_formatter)
LOG.addHandler(qh) LOG.addHandler(qh)
@ -234,11 +214,11 @@ def setup_logging(config, loglevel, quiet):
"--config", "--config",
"config_file", "config_file",
show_default=True, show_default=True,
default=utils.DEFAULT_CONFIG_FILE, default=aprsd_config.DEFAULT_CONFIG_FILE,
help="The aprsd config file to use for options.", help="The aprsd config file to use for options.",
) )
def check_version(loglevel, config_file): def check_version(loglevel, config_file):
config = utils.parse_config(config_file) config = aprsd_config.parse_config(config_file)
setup_logging(config, loglevel, False) setup_logging(config, loglevel, False)
level, msg = utils._check_version() level, msg = utils._check_version()
@ -251,7 +231,7 @@ def check_version(loglevel, config_file):
@main.command() @main.command()
def sample_config(): def sample_config():
"""This dumps the config to stdout.""" """This dumps the config to stdout."""
click.echo(utils.dump_default_cfg()) click.echo(aprsd_config.dump_default_cfg())
@main.command() @main.command()
@ -272,7 +252,7 @@ def sample_config():
"--config", "--config",
"config_file", "config_file",
show_default=True, show_default=True,
default=utils.DEFAULT_CONFIG_FILE, default=aprsd_config.DEFAULT_CONFIG_FILE,
help="The aprsd config file to use for options.", help="The aprsd config file to use for options.",
) )
@click.option( @click.option(
@ -312,7 +292,7 @@ def send_message(
"""Send a message to a callsign via APRS_IS.""" """Send a message to a callsign via APRS_IS."""
global got_ack, got_response global got_ack, got_response
config = utils.parse_config(config_file) config = aprsd_config.parse_config(config_file)
if not aprs_login: if not aprs_login:
click.echo("Must set --aprs_login or APRS_LOGIN") click.echo("Must set --aprs_login or APRS_LOGIN")
return return
@ -371,8 +351,8 @@ def send_message(
sys.exit(0) sys.exit(0)
try: try:
cl = client.Client(config) client.ClientFactory.setup(config)
cl.setup_connection() client.factory.create().client
except LoginError: except LoginError:
sys.exit(-1) sys.exit(-1)
@ -399,7 +379,7 @@ def send_message(
# This will register a packet consumer with aprslib # This will register a packet consumer with aprslib
# When new packets come in the consumer will process # When new packets come in the consumer will process
# the packet # the packet
aprs_client = client.get_client() aprs_client = client.factory.create().client
aprs_client.consumer(rx_packet, raw=False) aprs_client.consumer(rx_packet, raw=False)
except aprslib.exceptions.ConnectionDrop: except aprslib.exceptions.ConnectionDrop:
LOG.error("Connection dropped, reconnecting") LOG.error("Connection dropped, reconnecting")
@ -407,7 +387,7 @@ def send_message(
# Force the deletion of the client object connected to aprs # Force the deletion of the client object connected to aprs
# This will cause a reconnect, next time client.get_client() # This will cause a reconnect, next time client.get_client()
# is called # is called
cl.reset() aprs_client.reset()
# main() ### # main() ###
@ -429,7 +409,7 @@ def send_message(
"--config", "--config",
"config_file", "config_file",
show_default=True, show_default=True,
default=utils.DEFAULT_CONFIG_FILE, default=aprsd_config.DEFAULT_CONFIG_FILE,
help="The aprsd config file to use for options.", help="The aprsd config file to use for options.",
) )
@click.option( @click.option(
@ -454,7 +434,7 @@ def server(
if not quiet: if not quiet:
click.echo("Load config") click.echo("Load config")
config = utils.parse_config(config_file) config = aprsd_config.parse_config(config_file)
setup_logging(config, loglevel, quiet) setup_logging(config, loglevel, quiet)
level, msg = utils._check_version() level, msg = utils._check_version()
@ -476,25 +456,20 @@ def server(
trace.setup_tracing(["method", "api"]) trace.setup_tracing(["method", "api"])
stats.APRSDStats(config) stats.APRSDStats(config)
if config["aprs"].get("enabled", True): # Initialize the client factory and create
try: # The correct client object ready for use
cl = client.Client(config) client.ClientFactory.setup(config)
cl.client # Make sure we have 1 client transport enabled
except LoginError: if not client.factory.is_client_enabled():
sys.exit(-1) LOG.error("No Clients are enabled in config.")
sys.exit(-1)
rx_thread = threads.APRSDRXThread( # Creates the client object
msg_queues=threads.msg_queues, LOG.info("Creating client connection")
config=config, client.factory.create().client
)
rx_thread.start()
else:
LOG.info(
"APRS network connection Not Enabled in config. This is"
" for setups without internet connectivity.",
)
# Create the initial PM singleton and Register plugins # 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(config)
plugin_manager.setup_plugins() plugin_manager.setup_plugins()
@ -510,20 +485,18 @@ def server(
packets.PacketList(config=config) packets.PacketList(config=config)
packets.WatchList(config=config) packets.WatchList(config=config)
if kissclient.KISSClient.kiss_enabled(config): rx_thread = threads.APRSDRXThread(
kcl = kissclient.KISSClient(config=config) msg_queues=threads.msg_queues,
# This initializes the client object. config=config,
kcl.client )
rx_thread.start()
kissrx_thread = threads.KISSRXThread(msg_queues=threads.msg_queues, config=config)
kissrx_thread.start()
messaging.MsgTrack().restart() messaging.MsgTrack().restart()
keepalive = threads.KeepAliveThread(config=config) keepalive = threads.KeepAliveThread(config=config)
keepalive.start() keepalive.start()
web_enabled = utils.check_config_option(config, ["aprsd", "web", "enabled"], default_fail=False) web_enabled = config.get("aprsd.web.enabled", default=False)
if web_enabled: if web_enabled:
flask_enabled = True flask_enabled = True

View File

@ -9,7 +9,9 @@ import re
import threading import threading
import time 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 packets, stats, threads
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -18,10 +20,6 @@ LOG = logging.getLogger("APRSD")
# and it's ok, but don't send a usage string back # and it's ok, but don't send a usage string back
NULL_MESSAGE = -1 NULL_MESSAGE = -1
MESSAGE_TRANSPORT_TCPKISS = "tcpkiss"
MESSAGE_TRANSPORT_SERIALKISS = "serialkiss"
MESSAGE_TRANSPORT_APRSIS = "aprsis"
class MsgTrack: class MsgTrack:
"""Class to keep track of outstanding text messages. """Class to keep track of outstanding text messages.
@ -34,13 +32,6 @@ class MsgTrack:
automatically adds itself to this class. When the ack is automatically adds itself to this class. When the ack is
recieved from the radio, the message object is removed from recieved from the radio, the message object is removed from
this class. this class.
# TODO(hemna)
When aprsd is asked to quit this class should be serialized and
saved to disk/db to keep track of the state of outstanding messages.
When aprsd is started, it should try and fetch the saved state,
and reloaded to a live state.
""" """
_instance = None _instance = None
@ -113,11 +104,11 @@ class MsgTrack:
LOG.debug(f"Save tracker to disk? {len(self)}") LOG.debug(f"Save tracker to disk? {len(self)}")
if len(self) > 0: if len(self) > 0:
LOG.info(f"Saving {len(self)} tracking messages to disk") 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: else:
LOG.debug( LOG.debug(
"Nothing to save, flushing old save file '{}'".format( "Nothing to save, flushing old save file '{}'".format(
utils.DEFAULT_SAVE_FILE, aprsd_config.DEFAULT_SAVE_FILE,
), ),
) )
self.flush() self.flush()
@ -131,8 +122,8 @@ class MsgTrack:
return dump return dump
def load(self): def load(self):
if os.path.exists(utils.DEFAULT_SAVE_FILE): if os.path.exists(aprsd_config.DEFAULT_SAVE_FILE):
raw = pickle.load(open(utils.DEFAULT_SAVE_FILE, "rb")) raw = pickle.load(open(aprsd_config.DEFAULT_SAVE_FILE, "rb"))
if raw: if raw:
self.track = raw self.track = raw
LOG.debug("Loaded MsgTrack dict from disk.") LOG.debug("Loaded MsgTrack dict from disk.")
@ -171,8 +162,8 @@ class MsgTrack:
def flush(self): def flush(self):
"""Nuke the old pickle file that stored the old results from last aprsd run.""" """Nuke the old pickle file that stored the old results from last aprsd run."""
if os.path.exists(utils.DEFAULT_SAVE_FILE): if os.path.exists(aprsd_config.DEFAULT_SAVE_FILE):
pathlib.Path(utils.DEFAULT_SAVE_FILE).unlink() pathlib.Path(aprsd_config.DEFAULT_SAVE_FILE).unlink()
with self.lock: with self.lock:
self.track = {} self.track = {}
@ -239,7 +230,6 @@ class Message(metaclass=abc.ABCMeta):
fromcall, fromcall,
tocall, tocall,
msg_id=None, msg_id=None,
transport=MESSAGE_TRANSPORT_APRSIS,
): ):
self.fromcall = fromcall self.fromcall = fromcall
self.tocall = tocall self.tocall = tocall
@ -248,18 +238,11 @@ class Message(metaclass=abc.ABCMeta):
c.increment() c.increment()
msg_id = c.value msg_id = c.value
self.id = msg_id self.id = msg_id
self.transport = transport
@abc.abstractmethod @abc.abstractmethod
def send(self): def send(self):
"""Child class must declare.""" """Child class must declare."""
def get_transport(self):
if self.transport == MESSAGE_TRANSPORT_APRSIS:
return client.get_client()
elif self.transport == MESSAGE_TRANSPORT_TCPKISS:
return kissclient.get_client()
class RawMessage(Message): class RawMessage(Message):
"""Send a raw message. """Send a raw message.
@ -271,8 +254,8 @@ class RawMessage(Message):
message = None message = None
def __init__(self, message, transport=MESSAGE_TRANSPORT_APRSIS): def __init__(self, message):
super().__init__(None, None, msg_id=None, transport=transport) super().__init__(None, None, msg_id=None)
self.message = message self.message = message
def dict(self): def dict(self):
@ -301,7 +284,7 @@ class RawMessage(Message):
def send_direct(self, aprsis_client=None): def send_direct(self, aprsis_client=None):
"""Send a message without a separate thread.""" """Send a message without a separate thread."""
cl = self.get_transport() cl = client.factory.create().client
log_message( log_message(
"Sending Message Direct", "Sending Message Direct",
str(self).rstrip("\n"), str(self).rstrip("\n"),
@ -310,7 +293,7 @@ class RawMessage(Message):
fromcall=self.fromcall, fromcall=self.fromcall,
) )
cl.send(self) cl.send(self)
stats.APRSDStats().msgs_sent_inc() stats.APRSDStats().msgs_tx_inc()
class TextMessage(Message): class TextMessage(Message):
@ -325,9 +308,8 @@ class TextMessage(Message):
message, message,
msg_id=None, msg_id=None,
allow_delay=True, allow_delay=True,
transport=MESSAGE_TRANSPORT_APRSIS,
): ):
super().__init__(fromcall, tocall, msg_id, transport=transport) super().__init__(fromcall, tocall, msg_id)
self.message = message self.message = message
# do we try and save this message for later if we don't get # do we try and save this message for later if we don't get
# an ack? Some messages we don't want to do this ever. # an ack? Some messages we don't want to do this ever.
@ -384,7 +366,7 @@ class TextMessage(Message):
if aprsis_client: if aprsis_client:
cl = aprsis_client cl = aprsis_client
else: else:
cl = self.get_transport() cl = client.factory.create().client
log_message( log_message(
"Sending Message Direct", "Sending Message Direct",
str(self).rstrip("\n"), str(self).rstrip("\n"),
@ -422,7 +404,6 @@ class SendMessageThread(threads.APRSDThread):
LOG.info("Message Send Complete via Ack.") LOG.info("Message Send Complete via Ack.")
return False return False
else: else:
cl = msg.get_transport()
send_now = False send_now = False
if msg.last_send_attempt == msg.retry_count: if msg.last_send_attempt == msg.retry_count:
# we reached the send limit, don't send again # we reached the send limit, don't send again
@ -453,6 +434,7 @@ class SendMessageThread(threads.APRSDThread):
retry_number=msg.last_send_attempt, retry_number=msg.last_send_attempt,
msg_num=msg.id, msg_num=msg.id,
) )
cl = client.factory.create().client
cl.send(msg) cl.send(msg)
stats.APRSDStats().msgs_tx_inc() stats.APRSDStats().msgs_tx_inc()
packets.PacketList().add(msg.dict()) packets.PacketList().add(msg.dict())
@ -467,8 +449,8 @@ class SendMessageThread(threads.APRSDThread):
class AckMessage(Message): class AckMessage(Message):
"""Class for building Acks and sending them.""" """Class for building Acks and sending them."""
def __init__(self, fromcall, tocall, msg_id, transport=MESSAGE_TRANSPORT_APRSIS): def __init__(self, fromcall, tocall, msg_id):
super().__init__(fromcall, tocall, msg_id=msg_id, transport=transport) super().__init__(fromcall, tocall, msg_id=msg_id)
def dict(self): def dict(self):
now = datetime.datetime.now() now = datetime.datetime.now()
@ -507,7 +489,7 @@ class AckMessage(Message):
if aprsis_client: if aprsis_client:
cl = aprsis_client cl = aprsis_client
else: else:
cl = self.get_transport() cl = client.factory.create().client
log_message( log_message(
"Sending ack", "Sending ack",
str(self).rstrip("\n"), str(self).rstrip("\n"),
@ -524,10 +506,8 @@ class SendAckThread(threads.APRSDThread):
self.ack = ack self.ack = ack
super().__init__(f"SendAck-{self.ack.id}") super().__init__(f"SendAck-{self.ack.id}")
@trace.trace
def loop(self): def loop(self):
"""Separate thread to send acks with retries.""" """Separate thread to send acks with retries."""
LOG.debug("SendAckThread loop start")
send_now = False send_now = False
if self.ack.last_send_attempt == self.ack.retry_count: if self.ack.last_send_attempt == self.ack.retry_count:
# we reached the send limit, don't send again # we reached the send limit, don't send again
@ -552,7 +532,7 @@ class SendAckThread(threads.APRSDThread):
send_now = True send_now = True
if send_now: if send_now:
cl = self.ack.get_transport() cl = client.factory.create().client
log_message( log_message(
"Sending ack", "Sending ack",
str(self.ack).rstrip("\n"), str(self.ack).rstrip("\n"),

View File

@ -17,9 +17,6 @@ from aprsd import client, messaging, packets, threads
# setup the global logger # setup the global logger
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
hookspec = pluggy.HookspecMarker("aprsd")
hookimpl = pluggy.HookimplMarker("aprsd")
CORE_MESSAGE_PLUGINS = [ CORE_MESSAGE_PLUGINS = [
"aprsd.plugins.email.EmailPlugin", "aprsd.plugins.email.EmailPlugin",
"aprsd.plugins.fortune.FortunePlugin", "aprsd.plugins.fortune.FortunePlugin",
@ -36,8 +33,11 @@ CORE_NOTIFY_PLUGINS = [
"aprsd.plugins.notify.NotifySeenPlugin", "aprsd.plugins.notify.NotifySeenPlugin",
] ]
hookspec = pluggy.HookspecMarker("aprsd")
hookimpl = pluggy.HookimplMarker("aprsd")
class APRSDCommandSpec:
class APRSDPluginSpec:
"""A hook specification namespace.""" """A hook specification namespace."""
@hookspec @hookspec
@ -62,11 +62,8 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
self.config = config self.config = config
self.message_counter = 0 self.message_counter = 0
self.setup() self.setup()
threads = self.create_threads() self.threads = self.create_threads()
if threads: self.start_threads()
self.threads = threads
if self.threads:
self.start_threads()
def start_threads(self): def start_threads(self):
if self.enabled and self.threads: if self.enabled and self.threads:
@ -96,11 +93,6 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
def message_count(self): def message_count(self):
return self.message_counter return self.message_counter
@property
def version(self):
"""Version"""
raise NotImplementedError
@abc.abstractmethod @abc.abstractmethod
def setup(self): def setup(self):
"""Do any plugin setup here.""" """Do any plugin setup here."""
@ -122,7 +114,6 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
if isinstance(thread, threads.APRSDThread): if isinstance(thread, threads.APRSDThread):
thread.stop() thread.stop()
@hookimpl
@abc.abstractmethod @abc.abstractmethod
def filter(self, packet): def filter(self, packet):
pass pass
@ -158,20 +149,28 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
) )
# make sure the timeout is set or this doesn't work # make sure the timeout is set or this doesn't work
if watch_list: if watch_list:
aprs_client = client.get_client() aprs_client = client.factory.create().client
filter_str = "b/{}".format("/".join(watch_list)) filter_str = "b/{}".format("/".join(watch_list))
aprs_client.set_filter(filter_str) aprs_client.set_filter(filter_str)
else: else:
LOG.warning("Watch list enabled, but no callsigns set.") LOG.warning("Watch list enabled, but no callsigns set.")
@hookimpl
def filter(self, packet): def filter(self, packet):
result = messaging.NULL_MESSAGE
if self.enabled: if self.enabled:
wl = packets.WatchList() wl = packets.WatchList()
result = messaging.NULL_MESSAGE
if wl.callsign_in_watchlist(packet["from"]): if wl.callsign_in_watchlist(packet["from"]):
# packet is from a callsign in the watch list # packet is from a callsign in the watch list
self.rx_inc() self.rx_inc()
result = self.process() try:
result = self.process(packet)
except Exception as ex:
LOG.error(
"Plugin {} failed to process packet {}".format(
self.__class__, ex,
),
)
if result: if result:
self.tx_inc() self.tx_inc()
wl.update_seen(packet) wl.update_seen(packet)
@ -221,7 +220,14 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
if re.search(self.command_regex, message): if re.search(self.command_regex, message):
self.rx_inc() self.rx_inc()
if self.enabled: if self.enabled:
result = self.process(packet) try:
result = self.process(packet)
except Exception as ex:
LOG.error(
"Plugin {} failed to process packet {}".format(
self.__class__, ex,
),
)
if result: if result:
self.tx_inc() self.tx_inc()
else: else:
@ -255,6 +261,10 @@ class PluginManager:
if config: if config:
self.config = config self.config = config
def _init(self):
self._pluggy_pm = pluggy.PluginManager("aprsd")
self._pluggy_pm.add_hookspecs(APRSDPluginSpec)
def load_plugins_from_path(self, module_path): def load_plugins_from_path(self, module_path):
if not os.path.exists(module_path): if not os.path.exists(module_path):
LOG.error(f"plugin path '{module_path}' doesn't exist.") LOG.error(f"plugin path '{module_path}' doesn't exist.")
@ -356,8 +366,7 @@ class PluginManager:
LOG.info("Loading APRSD Plugins") LOG.info("Loading APRSD Plugins")
enabled_plugins = self.config["aprsd"].get("enabled_plugins", None) enabled_plugins = self.config["aprsd"].get("enabled_plugins", None)
self._pluggy_pm = pluggy.PluginManager("aprsd") self._init()
self._pluggy_pm.add_hookspecs(APRSDCommandSpec)
if enabled_plugins: if enabled_plugins:
for p_name in enabled_plugins: for p_name in enabled_plugins:
self._load_plugin(p_name) self._load_plugin(p_name)

View File

@ -5,6 +5,7 @@ import imaplib
import logging import logging
import re import re
import smtplib import smtplib
import threading
import time import time
import imapclient import imapclient
@ -15,9 +16,44 @@ from aprsd import messaging, plugin, stats, threads, trace
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
# This gets forced set from main.py prior to being used internally
CONFIG = {} class EmailInfo:
check_email_delay = 60 """A singleton thread safe mechanism for the global check_email_delay.
This has to be done because we have 2 separate threads that access
the delay value.
1) when EmailPlugin runs from a user message and
2) when the background EmailThread runs to check email.
Access the check email delay with
EmailInfo().delay
Set it with
EmailInfo().delay = 100
or
EmailInfo().delay += 10
"""
_instance = None
def __new__(cls, *args, **kwargs):
"""This magic turns this into a singleton."""
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.lock = threading.Lock()
cls._instance._delay = 60
return cls._instance
@property
def delay(self):
with self.lock:
return self._delay
@delay.setter
def delay(self, val):
with self.lock:
self._delay = val
class EmailPlugin(plugin.APRSDRegexCommandPluginBase): class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
@ -34,8 +70,6 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
def setup(self): def setup(self):
"""Ensure that email is enabled and start the thread.""" """Ensure that email is enabled and start the thread."""
global CONFIG
CONFIG = self.config
email_enabled = self.config["aprsd"]["email"].get("enabled", False) email_enabled = self.config["aprsd"]["email"].get("enabled", False)
validation = self.config["aprsd"]["email"].get("validate", False) validation = self.config["aprsd"]["email"].get("validate", False)
@ -81,7 +115,7 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
r = re.search("^-([0-9])[0-9]*$", message) r = re.search("^-([0-9])[0-9]*$", message)
if r is not None: if r is not None:
LOG.debug("RESEND EMAIL") LOG.debug("RESEND EMAIL")
resend_email(r.group(1), fromcall) resend_email(self.config, r.group(1), fromcall)
reply = messaging.NULL_MESSAGE reply = messaging.NULL_MESSAGE
# -user@address.com body of email # -user@address.com body of email
elif re.search(r"^-([A-Za-z0-9_\-\.@]+) (.*)", message): elif re.search(r"^-([A-Za-z0-9_\-\.@]+) (.*)", message):
@ -91,7 +125,7 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
to_addr = a.group(1) to_addr = a.group(1)
content = a.group(2) content = a.group(2)
email_address = get_email_from_shortcut(to_addr) email_address = get_email_from_shortcut(self.config, to_addr)
if not email_address: if not email_address:
reply = "Bad email address" reply = "Bad email address"
return reply return reply
@ -114,7 +148,7 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
too_soon = 1 too_soon = 1
if not too_soon or ack == 0: if not too_soon or ack == 0:
LOG.info(f"Send email '{content}'") LOG.info(f"Send email '{content}'")
send_result = email.send_email(to_addr, content) send_result = send_email(self.config, to_addr, content)
reply = messaging.NULL_MESSAGE reply = messaging.NULL_MESSAGE
if send_result != 0: if send_result != 0:
reply = f"-{to_addr} failed" reply = f"-{to_addr} failed"
@ -143,10 +177,9 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
return reply return reply
def _imap_connect(): def _imap_connect(config):
global CONFIG imap_port = config["aprsd"]["email"]["imap"].get("port", 143)
imap_port = CONFIG["aprsd"]["email"]["imap"].get("port", 143) use_ssl = config["aprsd"]["email"]["imap"].get("use_ssl", False)
use_ssl = CONFIG["aprsd"]["email"]["imap"].get("use_ssl", False)
# host = CONFIG["aprsd"]["email"]["imap"]["host"] # host = CONFIG["aprsd"]["email"]["imap"]["host"]
# msg = "{}{}:{}".format("TLS " if use_ssl else "", host, imap_port) # msg = "{}{}:{}".format("TLS " if use_ssl else "", host, imap_port)
# LOG.debug("Connect to IMAP host {} with user '{}'". # LOG.debug("Connect to IMAP host {} with user '{}'".
@ -154,7 +187,7 @@ def _imap_connect():
try: try:
server = imapclient.IMAPClient( server = imapclient.IMAPClient(
CONFIG["aprsd"]["email"]["imap"]["host"], config["aprsd"]["email"]["imap"]["host"],
port=imap_port, port=imap_port,
use_uid=True, use_uid=True,
ssl=use_ssl, ssl=use_ssl,
@ -166,8 +199,8 @@ def _imap_connect():
try: try:
server.login( server.login(
CONFIG["aprsd"]["email"]["imap"]["login"], config["aprsd"]["email"]["imap"]["login"],
CONFIG["aprsd"]["email"]["imap"]["password"], config["aprsd"]["email"]["imap"]["password"],
) )
except (imaplib.IMAP4.error, Exception) as e: except (imaplib.IMAP4.error, Exception) as e:
msg = getattr(e, "message", repr(e)) msg = getattr(e, "message", repr(e))
@ -183,15 +216,15 @@ def _imap_connect():
return server return server
def _smtp_connect(): def _smtp_connect(config):
host = CONFIG["aprsd"]["email"]["smtp"]["host"] host = config["aprsd"]["email"]["smtp"]["host"]
smtp_port = CONFIG["aprsd"]["email"]["smtp"]["port"] smtp_port = config["aprsd"]["email"]["smtp"]["port"]
use_ssl = CONFIG["aprsd"]["email"]["smtp"].get("use_ssl", False) use_ssl = config["aprsd"]["email"]["smtp"].get("use_ssl", False)
msg = "{}{}:{}".format("SSL " if use_ssl else "", host, smtp_port) msg = "{}{}:{}".format("SSL " if use_ssl else "", host, smtp_port)
LOG.debug( LOG.debug(
"Connect to SMTP host {} with user '{}'".format( "Connect to SMTP host {} with user '{}'".format(
msg, msg,
CONFIG["aprsd"]["email"]["imap"]["login"], config["aprsd"]["email"]["imap"]["login"],
), ),
) )
@ -214,15 +247,15 @@ def _smtp_connect():
LOG.debug(f"Connected to smtp host {msg}") LOG.debug(f"Connected to smtp host {msg}")
debug = CONFIG["aprsd"]["email"]["smtp"].get("debug", False) debug = config["aprsd"]["email"]["smtp"].get("debug", False)
if debug: if debug:
server.set_debuglevel(5) server.set_debuglevel(5)
server.sendmail = trace.trace(server.sendmail) server.sendmail = trace.trace(server.sendmail)
try: try:
server.login( server.login(
CONFIG["aprsd"]["email"]["smtp"]["login"], config["aprsd"]["email"]["smtp"]["login"],
CONFIG["aprsd"]["email"]["smtp"]["password"], config["aprsd"]["email"]["smtp"]["password"],
) )
except Exception: except Exception:
LOG.error("Couldn't connect to SMTP Server") LOG.error("Couldn't connect to SMTP Server")
@ -273,9 +306,9 @@ def validate_shortcuts(config):
) )
def get_email_from_shortcut(addr): def get_email_from_shortcut(config, addr):
if CONFIG["aprsd"]["email"].get("shortcuts", False): if config["aprsd"]["email"].get("shortcuts", False):
return CONFIG["aprsd"]["email"]["shortcuts"].get(addr, addr) return config["aprsd"]["email"]["shortcuts"].get(addr, addr)
else: else:
return addr return addr
@ -286,9 +319,9 @@ def validate_email_config(config, disable_validation=False):
This helps with failing early during startup. This helps with failing early during startup.
""" """
LOG.info("Checking IMAP configuration") LOG.info("Checking IMAP configuration")
imap_server = _imap_connect() imap_server = _imap_connect(config)
LOG.info("Checking SMTP configuration") LOG.info("Checking SMTP configuration")
smtp_server = _smtp_connect() smtp_server = _smtp_connect(config)
# Now validate and flag any shortcuts as invalid # Now validate and flag any shortcuts as invalid
if not disable_validation: if not disable_validation:
@ -398,34 +431,32 @@ def parse_email(msgid, data, server):
@trace.trace @trace.trace
def send_email(to_addr, content): def send_email(config, to_addr, content):
global check_email_delay shortcuts = config["aprsd"]["email"]["shortcuts"]
email_address = get_email_from_shortcut(config, to_addr)
shortcuts = CONFIG["aprsd"]["email"]["shortcuts"]
email_address = get_email_from_shortcut(to_addr)
LOG.info("Sending Email_________________") LOG.info("Sending Email_________________")
if to_addr in shortcuts: if to_addr in shortcuts:
LOG.info("To : " + to_addr) LOG.info("To : " + to_addr)
to_addr = email_address to_addr = email_address
LOG.info(" (" + to_addr + ")") LOG.info(" (" + to_addr + ")")
subject = CONFIG["ham"]["callsign"] subject = config["ham"]["callsign"]
# content = content + "\n\n(NOTE: reply with one line)" # content = content + "\n\n(NOTE: reply with one line)"
LOG.info("Subject : " + subject) LOG.info("Subject : " + subject)
LOG.info("Body : " + content) LOG.info("Body : " + content)
# check email more often since there's activity right now # check email more often since there's activity right now
check_email_delay = 60 EmailInfo().delay = 60
msg = MIMEText(content) msg = MIMEText(content)
msg["Subject"] = subject msg["Subject"] = subject
msg["From"] = CONFIG["aprsd"]["email"]["smtp"]["login"] msg["From"] = config["aprsd"]["email"]["smtp"]["login"]
msg["To"] = to_addr msg["To"] = to_addr
server = _smtp_connect() server = _smtp_connect()
if server: if server:
try: try:
server.sendmail( server.sendmail(
CONFIG["aprsd"]["email"]["smtp"]["login"], config["aprsd"]["email"]["smtp"]["login"],
[to_addr], [to_addr],
msg.as_string(), msg.as_string(),
) )
@ -440,20 +471,19 @@ def send_email(to_addr, content):
@trace.trace @trace.trace
def resend_email(count, fromcall): def resend_email(config, count, fromcall):
global check_email_delay
date = datetime.datetime.now() date = datetime.datetime.now()
month = date.strftime("%B")[:3] # Nov, Mar, Apr month = date.strftime("%B")[:3] # Nov, Mar, Apr
day = date.day day = date.day
year = date.year year = date.year
today = f"{day}-{month}-{year}" today = f"{day}-{month}-{year}"
shortcuts = CONFIG["aprsd"]["email"]["shortcuts"] shortcuts = config["aprsd"]["email"]["shortcuts"]
# swap key/value # swap key/value
shortcuts_inverted = {v: k for k, v in shortcuts.items()} shortcuts_inverted = {v: k for k, v in shortcuts.items()}
try: try:
server = _imap_connect() server = _imap_connect(config)
except Exception as e: except Exception as e:
LOG.exception("Failed to Connect to IMAP. Cannot resend email ", e) LOG.exception("Failed to Connect to IMAP. Cannot resend email ", e)
return return
@ -493,7 +523,7 @@ def resend_email(count, fromcall):
reply = "-" + from_addr + " * " + body.decode(errors="ignore") reply = "-" + from_addr + " * " + body.decode(errors="ignore")
# messaging.send_message(fromcall, reply) # messaging.send_message(fromcall, reply)
msg = messaging.TextMessage( msg = messaging.TextMessage(
CONFIG["aprs"]["login"], config["aprs"]["login"],
fromcall, fromcall,
reply, reply,
) )
@ -515,11 +545,11 @@ def resend_email(count, fromcall):
str(s).zfill(2), str(s).zfill(2),
) )
# messaging.send_message(fromcall, reply) # messaging.send_message(fromcall, reply)
msg = messaging.TextMessage(CONFIG["aprs"]["login"], fromcall, reply) msg = messaging.TextMessage(config["aprs"]["login"], fromcall, reply)
msg.send() msg.send()
# check email more often since we're resending one now # check email more often since we're resending one now
check_email_delay = 60 EmailInfo().delay = 60
server.logout() server.logout()
# end resend_email() # end resend_email()
@ -533,27 +563,24 @@ class APRSDEmailThread(threads.APRSDThread):
self.past = datetime.datetime.now() self.past = datetime.datetime.now()
def loop(self): def loop(self):
global check_email_delay
check_email_delay = 60
time.sleep(5) time.sleep(5)
stats.APRSDStats().email_thread_update() stats.APRSDStats().email_thread_update()
# always sleep for 5 seconds and see if we need to check email # always sleep for 5 seconds and see if we need to check email
# This allows CTRL-C to stop the execution of this loop sooner # This allows CTRL-C to stop the execution of this loop sooner
# than check_email_delay time # than check_email_delay time
now = datetime.datetime.now() now = datetime.datetime.now()
if now - self.past > datetime.timedelta(seconds=check_email_delay): if now - self.past > datetime.timedelta(seconds=EmailInfo().delay):
# It's time to check email # It's time to check email
# slowly increase delay every iteration, max out at 300 seconds # slowly increase delay every iteration, max out at 300 seconds
# any send/receive/resend activity will reset this to 60 seconds # any send/receive/resend activity will reset this to 60 seconds
if check_email_delay < 300: if EmailInfo().delay < 300:
check_email_delay += 1 EmailInfo().delay += 10
LOG.debug( LOG.debug(
"check_email_delay is " + str(check_email_delay) + " seconds", f"check_email_delay is {EmailInfo().delay} seconds ",
) )
shortcuts = CONFIG["aprsd"]["email"]["shortcuts"] shortcuts = self.config["aprsd"]["email"]["shortcuts"]
# swap key/value # swap key/value
shortcuts_inverted = {v: k for k, v in shortcuts.items()} shortcuts_inverted = {v: k for k, v in shortcuts.items()}
@ -564,7 +591,7 @@ class APRSDEmailThread(threads.APRSDThread):
today = f"{day}-{month}-{year}" today = f"{day}-{month}-{year}"
try: try:
server = _imap_connect() server = _imap_connect(self.config)
except Exception as e: except Exception as e:
LOG.exception("IMAP failed to connect.", e) LOG.exception("IMAP failed to connect.", e)
return True return True
@ -658,7 +685,7 @@ class APRSDEmailThread(threads.APRSDThread):
LOG.exception("Couldn't remove seen flag from email", e) LOG.exception("Couldn't remove seen flag from email", e)
# check email more often since we just received an email # check email more often since we just received an email
check_email_delay = 60 EmailInfo().delay = 60
# reset clock # reset clock
LOG.debug("Done looping over Server.fetch, logging out.") LOG.debug("Done looping over Server.fetch, logging out.")

View File

@ -2,7 +2,7 @@ import logging
import re import re
import time import time
from aprsd import plugin, plugin_utils, trace, utils from aprsd import plugin, plugin_utils, trace
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -24,7 +24,7 @@ class LocationPlugin(plugin.APRSDRegexCommandPluginBase):
# get last location of a callsign, get descriptive name from weather service # get last location of a callsign, get descriptive name from weather service
try: try:
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"]) self.config.check_option(["services", "aprs.fi", "apiKey"])
except Exception as ex: except Exception as ex:
LOG.error(f"Failed to find config aprs.fi:apikey {ex}") LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
return "No aprs.fi apikey found" return "No aprs.fi apikey found"

View File

@ -17,12 +17,8 @@ class NotifySeenPlugin(plugin.APRSDWatchListPluginBase):
version = "1.0" version = "1.0"
def __init__(self, config):
"""The aprsd config object is stored."""
super().__init__(config)
def process(self, packet): def process(self, packet):
LOG.info("BaseNotifyPlugin") LOG.info("NotifySeenPlugin")
notify_callsign = self.config["aprsd"]["watch_list"]["alert_callsign"] notify_callsign = self.config["aprsd"]["watch_list"]["alert_callsign"]
fromcall = packet.get("from") fromcall = packet.get("from")

View File

@ -5,7 +5,7 @@ import time
from opencage.geocoder import OpenCageGeocode from opencage.geocoder import OpenCageGeocode
import pytz import pytz
from aprsd import fuzzyclock, plugin, plugin_utils, trace, utils from aprsd import fuzzyclock, plugin, plugin_utils, trace
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -64,7 +64,7 @@ class TimeOpenCageDataPlugin(TimePlugin):
# get last location of a callsign, get descriptive name from weather service # get last location of a callsign, get descriptive name from weather service
try: try:
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"]) self.config.exists(["services", "aprs.fi", "apiKey"])
except Exception as ex: except Exception as ex:
LOG.error(f"Failed to find config aprs.fi:apikey {ex}") LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
return "No aprs.fi apikey found" return "No aprs.fi apikey found"
@ -95,7 +95,7 @@ class TimeOpenCageDataPlugin(TimePlugin):
lon = aprs_data["entries"][0]["lng"] lon = aprs_data["entries"][0]["lng"]
try: try:
utils.check_config_option(self.config, "opencagedata", "apiKey") self.config.exists("opencagedata.apiKey")
except Exception as ex: except Exception as ex:
LOG.error(f"Failed to find config opencage:apiKey {ex}") LOG.error(f"Failed to find config opencage:apiKey {ex}")
return "No opencage apiKey found" return "No opencage apiKey found"
@ -130,7 +130,7 @@ class TimeOWMPlugin(TimePlugin):
# get last location of a callsign, get descriptive name from weather service # get last location of a callsign, get descriptive name from weather service
try: try:
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"]) self.config.exists(["services", "aprs.fi", "apiKey"])
except Exception as ex: except Exception as ex:
LOG.error(f"Failed to find config aprs.fi:apikey {ex}") LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
return "No aprs.fi apikey found" return "No aprs.fi apikey found"
@ -160,8 +160,7 @@ class TimeOWMPlugin(TimePlugin):
lon = aprs_data["entries"][0]["lng"] lon = aprs_data["entries"][0]["lng"]
try: try:
utils.check_config_option( self.config.exists(
self.config,
["services", "openweathermap", "apiKey"], ["services", "openweathermap", "apiKey"],
) )
except Exception as ex: except Exception as ex:

View File

@ -4,7 +4,7 @@ import re
import requests import requests
from aprsd import plugin, plugin_utils, trace, utils from aprsd import plugin, plugin_utils, trace
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -34,7 +34,7 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
# message = packet.get("message_text", None) # message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0") # ack = packet.get("msgNo", "0")
try: try:
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"]) self.config.exists(["services", "aprs.fi", "apiKey"])
except Exception as ex: except Exception as ex:
LOG.error(f"Failed to find config aprs.fi:apikey {ex}") LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
return "No aprs.fi apikey found" return "No aprs.fi apikey found"
@ -115,10 +115,7 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase):
fromcall = fromcall fromcall = fromcall
try: try:
utils.check_config_option( self.config.exists(["services", "aprs.fi", "apiKey"])
self.config,
["services", "aprs.fi", "apiKey"],
)
except Exception as ex: except Exception as ex:
LOG.error(f"Failed to find config aprs.fi:apikey {ex}") LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
return "No aprs.fi apikey found" return "No aprs.fi apikey found"
@ -199,7 +196,7 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
searchcall = fromcall searchcall = fromcall
try: try:
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"]) self.config.exists(["services", "aprs.fi", "apiKey"])
except Exception as ex: except Exception as ex:
LOG.error(f"Failed to find config aprs.fi:apikey {ex}") LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
return "No aprs.fi apikey found" return "No aprs.fi apikey found"
@ -220,16 +217,13 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
lon = aprs_data["entries"][0]["lng"] lon = aprs_data["entries"][0]["lng"]
try: try:
utils.check_config_option( self.config.exists(["services", "openweathermap", "apiKey"])
self.config,
["services", "openweathermap", "apiKey"],
)
except Exception as ex: except Exception as ex:
LOG.error(f"Failed to find config openweathermap:apiKey {ex}") LOG.error(f"Failed to find config openweathermap:apiKey {ex}")
return "No openweathermap apiKey found" return "No openweathermap apiKey found"
try: try:
utils.check_config_option(self.config, ["aprsd", "units"]) self.config.exists(["aprsd", "units"])
except Exception: except Exception:
LOG.debug("Couldn't find untis in aprsd:services:units") LOG.debug("Couldn't find untis in aprsd:services:units")
units = "metric" units = "metric"
@ -323,7 +317,7 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
searchcall = fromcall searchcall = fromcall
try: try:
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"]) self.config.exists(["services", "aprs.fi", "apiKey"])
except Exception as ex: except Exception as ex:
LOG.error(f"Failed to find config aprs.fi:apikey {ex}") LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
return "No aprs.fi apikey found" return "No aprs.fi apikey found"
@ -344,13 +338,13 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
lon = aprs_data["entries"][0]["lng"] lon = aprs_data["entries"][0]["lng"]
try: try:
utils.check_config_option(self.config, ["services", "avwx", "apiKey"]) self.config.exists(["services", "avwx", "apiKey"])
except Exception as ex: except Exception as ex:
LOG.error(f"Failed to find config avwx:apiKey {ex}") LOG.error(f"Failed to find config avwx:apiKey {ex}")
return "No avwx apiKey found" return "No avwx apiKey found"
try: try:
utils.check_config_option(self.config, ["services", "avwx", "base_url"]) self.config.exists(self.config, ["services", "avwx", "base_url"])
except Exception as ex: except Exception as ex:
LOG.debug(f"Didn't find avwx:base_url {ex}") LOG.debug(f"Didn't find avwx:base_url {ex}")
base_url = "https://avwx.rest" base_url = "https://avwx.rest"

View File

@ -8,7 +8,7 @@ import tracemalloc
import aprslib import aprslib
from aprsd import client, kissclient, messaging, packets, plugin, stats, utils from aprsd import client, messaging, packets, plugin, stats, utils
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -137,9 +137,9 @@ class KeepAliveThread(APRSDThread):
if delta > self.max_delta: if delta > self.max_delta:
# We haven't gotten a keepalive from aprs-is in a while # We haven't gotten a keepalive from aprs-is in a while
# reset the connection.a # reset the connection.a
if not kissclient.KISSClient.kiss_enabled(self.config): if not client.KISSClient.is_enabled(self.config):
LOG.warning("Resetting connection to APRS-IS.") LOG.warning("Resetting connection to APRS-IS.")
client.Client().reset() client.factory.create().reset()
# Check version every hour # Check version every hour
delta = now - self.checker_time delta = now - self.checker_time
@ -158,13 +158,13 @@ class APRSDRXThread(APRSDThread):
super().__init__("RX_MSG") super().__init__("RX_MSG")
self.msg_queues = msg_queues self.msg_queues = msg_queues
self.config = config self.config = config
self._client = client.factory.create()
def stop(self): def stop(self):
self.thread_stop = True self.thread_stop = True
client.get_client().stop() client.factory.create().client.stop()
def loop(self): def loop(self):
aprs_client = client.get_client()
# setup the consumer of messages and block until a messages # setup the consumer of messages and block until a messages
try: try:
@ -177,7 +177,9 @@ class APRSDRXThread(APRSDThread):
# and the aprslib developer didn't want to allow a PR to add # and the aprslib developer didn't want to allow a PR to add
# kwargs. :( # kwargs. :(
# https://github.com/rossengeorgiev/aprs-python/pull/56 # https://github.com/rossengeorgiev/aprs-python/pull/56
aprs_client.consumer(self.process_packet, raw=False, blocking=False) self._client.client.consumer(
self.process_packet, raw=False, blocking=False,
)
except aprslib.exceptions.ConnectionDrop: except aprslib.exceptions.ConnectionDrop:
LOG.error("Connection dropped, reconnecting") LOG.error("Connection dropped, reconnecting")
@ -185,21 +187,21 @@ class APRSDRXThread(APRSDThread):
# Force the deletion of the client object connected to aprs # Force the deletion of the client object connected to aprs
# This will cause a reconnect, next time client.get_client() # This will cause a reconnect, next time client.get_client()
# is called # is called
client.Client().reset() self._client.reset()
# Continue to loop # Continue to loop
return True return True
def process_packet(self, packet): def process_packet(self, *args, **kwargs):
packet = self._client.decode_packet(*args, **kwargs)
thread = APRSDProcessPacketThread(packet=packet, config=self.config) thread = APRSDProcessPacketThread(packet=packet, config=self.config)
thread.start() thread.start()
class APRSDProcessPacketThread(APRSDThread): class APRSDProcessPacketThread(APRSDThread):
def __init__(self, packet, config, transport="aprsis"): def __init__(self, packet, config):
self.packet = packet self.packet = packet
self.config = config self.config = config
self.transport = transport
name = self.packet["raw"][:10] name = self.packet["raw"][:10]
super().__init__(f"RX_PACKET-{name}") super().__init__(f"RX_PACKET-{name}")
@ -254,7 +256,6 @@ class APRSDProcessPacketThread(APRSDThread):
self.config["aprs"]["login"], self.config["aprs"]["login"],
fromcall, fromcall,
msg_id=msg_id, msg_id=msg_id,
transport=self.transport,
) )
ack.send() ack.send()
@ -275,7 +276,6 @@ class APRSDProcessPacketThread(APRSDThread):
self.config["aprs"]["login"], self.config["aprs"]["login"],
fromcall, fromcall,
subreply, subreply,
transport=self.transport,
) )
msg.send() msg.send()
elif isinstance(reply, messaging.Message): elif isinstance(reply, messaging.Message):
@ -296,7 +296,6 @@ class APRSDProcessPacketThread(APRSDThread):
self.config["aprs"]["login"], self.config["aprs"]["login"],
fromcall, fromcall,
reply, reply,
transport=self.transport,
) )
msg.send() msg.send()
@ -309,7 +308,6 @@ class APRSDProcessPacketThread(APRSDThread):
self.config["aprs"]["login"], self.config["aprs"]["login"],
fromcall, fromcall,
reply, reply,
transport=self.transport,
) )
msg.send() msg.send()
except Exception as ex: except Exception as ex:
@ -321,88 +319,7 @@ class APRSDProcessPacketThread(APRSDThread):
self.config["aprs"]["login"], self.config["aprs"]["login"],
fromcall, fromcall,
reply, reply,
transport=self.transport,
) )
msg.send() msg.send()
LOG.debug("Packet processing complete") LOG.debug("Packet processing complete")
class APRSDTXThread(APRSDThread):
def __init__(self, msg_queues, config):
super().__init__("TX_MSG")
self.msg_queues = msg_queues
self.config = config
def loop(self):
try:
msg = self.msg_queues["tx"].get(timeout=1)
msg.send()
except queue.Empty:
pass
# Continue to loop
return True
class KISSRXThread(APRSDThread):
"""Thread that connects to direwolf's TCPKISS interface.
All Packets are processed and sent back out the direwolf
interface instead of the aprs-is server.
"""
def __init__(self, msg_queues, config):
super().__init__("KISSRX_MSG")
self.msg_queues = msg_queues
self.config = config
def stop(self):
self.thread_stop = True
kissclient.get_client().stop()
def loop(self):
kiss_client = kissclient.get_client()
# setup the consumer of messages and block until a messages
try:
# This will register a packet consumer with aprslib
# When new packets come in the consumer will process
# the packet
# Do a partial here because the consumer signature doesn't allow
# For kwargs to be passed in to the consumer func we declare
# and the aprslib developer didn't want to allow a PR to add
# kwargs. :(
# https://github.com/rossengeorgiev/aprs-python/pull/56
kiss_client.consumer(self.process_packet, callsign=self.config["kiss"]["callsign"])
kiss_client.loop.run_forever()
except aprslib.exceptions.ConnectionDrop:
LOG.error("Connection dropped, reconnecting")
time.sleep(5)
# Force the deletion of the client object connected to aprs
# This will cause a reconnect, next time client.get_client()
# is called
client.Client().reset()
# Continue to loop
def process_packet(self, interface, frame):
"""Process a packet recieved from aprs-is server."""
LOG.debug(f"Got an APRS Frame '{frame}'")
# try and nuke the * from the fromcall sign.
frame.header._source._ch = False
payload = str(frame.payload.decode())
msg = f"{str(frame.header)}:{payload}"
# msg = frame.tnc2
LOG.debug(f"Decoding {msg}")
packet = aprslib.parse(msg)
LOG.debug(packet)
thread = APRSDProcessPacketThread(
packet=packet, config=self.config,
transport=messaging.MESSAGE_TRANSPORT_TCPKISS,
)
thread.start()
return

View File

@ -3,128 +3,13 @@
import collections import collections
import errno import errno
import functools import functools
import logging
import os import os
from pathlib import Path
import re import re
import sys
import threading import threading
import click
import update_checker import update_checker
import yaml
import aprsd 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): def synchronized(wrapped):
@ -175,239 +60,6 @@ def end_substr(original, substr):
return idx 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>."""
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): def human_size(bytes, units=None):
"""Returns a human readable string representation of bytes""" """Returns a human readable string representation of bytes"""
if not units: if not units:

View File

@ -28,7 +28,7 @@ RUN addgroup --gid $GID $APRS_USER
RUN useradd -m -u $UID -g $APRS_USER $APRS_USER RUN useradd -m -u $UID -g $APRS_USER $APRS_USER
# Install aprsd # 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 # Ensure /config is there with a default config file
USER root USER root

View File

@ -1,14 +1,14 @@
FROM python:3.8-slim as aprsd FROM python:3.8-slim as aprsd
# Dockerfile for building a container during aprsd development. # Dockerfile for building a container during aprsd development.
ARG BRANCH ARG branch
ARG UID ARG UID
ARG GID ARG GID
ENV APRS_USER=aprs ENV APRS_USER=aprs
ENV HOME=/home/aprs ENV HOME=/home/aprs
ENV APRSD=http://github.com/craigerl/aprsd.git ENV APRSD=http://github.com/craigerl/aprsd.git
ENV APRSD_BRANCH=${BRANCH:-master} ENV APRSD_BRANCH=${branch:-master}
ENV VIRTUAL_ENV=$HOME/.venv3 ENV VIRTUAL_ENV=$HOME/.venv3
ENV UID=${UID:-1000} ENV UID=${UID:-1000}
ENV GID=${GID:-1000} ENV GID=${GID:-1000}

View File

@ -15,14 +15,18 @@ EOF
ALL_PLATFORMS=0 ALL_PLATFORMS=0
DEV=0 DEV=0
TAG="master" TAG="latest"
BRANCH="master"
while getopts “t:da” OPTION while getopts “t:dab:” OPTION
do do
case $OPTION in case $OPTION in
t) t)
TAG=$OPTARG TAG=$OPTARG
;; ;;
b)
BRANCH=$OPTARG
;;
a) a)
ALL_PLATFORMS=1 ALL_PLATFORMS=1
;; ;;
@ -36,7 +40,7 @@ do
esac esac
done done
VERSION="2.2.1" VERSION="2.3.1"
if [ $ALL_PLATFORMS -eq 1 ] if [ $ALL_PLATFORMS -eq 1 ]
then then
@ -45,20 +49,28 @@ else
PLATFORMS="linux/amd64" PLATFORMS="linux/amd64"
fi fi
echo "Build with tag=${TAG} BRANCH=${BRANCH} dev?=${DEV} platforms?=${PLATFORMS}"
echo "Destroying old multiarch build container"
docker buildx rm multiarch
echo "Creating new buildx container"
docker buildx create --name multiarch --platform linux/arm/v7,linux/arm/v6,linux/arm64,linux/amd64 --config ./buildkit.toml --use --driver-opt image=moby/buildkit:master
if [ $DEV -eq 1 ] if [ $DEV -eq 1 ]
then then
echo "Build -DEV- with tag=${TAG} BRANCH=${BRANCH} platforms?=${PLATFORMS}"
# Use this script to locally build the docker image # Use this script to locally build the docker image
docker buildx build --push --platform $PLATFORMS \ docker buildx build --push --platform $PLATFORMS \
-t harbor.hemna.com/hemna6969/aprsd:$TAG \ -t harbor.hemna.com/hemna6969/aprsd:$TAG \
-f Dockerfile-dev --no-cache . -f Dockerfile-dev --build-arg branch=$BRANCH --no-cache .
else else
# Use this script to locally build the docker image # Use this script to locally build the docker image
echo "Build with tag=${TAG} BRANCH=${BRANCH} platforms?=${PLATFORMS}"
docker buildx build --push --platform $PLATFORMS \ docker buildx build --push --platform $PLATFORMS \
-t hemna6969/aprsd:$VERSION \ -t hemna6969/aprsd:$VERSION \
-t hemna6969/aprsd:latest \ -t hemna6969/aprsd:$TAG \
-t harbor.hemna.com/hemna6969/aprsd:latest \ -t harbor.hemna.com/hemna6969/aprsd:$TAG \
-t harbor.hemna.com/hemna6969/aprsd:$VERSION \ -t harbor.hemna.com/hemna6969/aprsd:$VERSION \
-f Dockerfile . -f Dockerfile .
fi fi

View File

@ -5,21 +5,21 @@ from aprsd.plugins import email
class TestEmail(unittest.TestCase): class TestEmail(unittest.TestCase):
def test_get_email_from_shortcut(self): def test_get_email_from_shortcut(self):
email.CONFIG = {"aprsd": {"email": {"shortcuts": {}}}} config = {"aprsd": {"email": {"shortcuts": {}}}}
email_address = "something@something.com" email_address = "something@something.com"
addr = f"-{email_address}" addr = f"-{email_address}"
actual = email.get_email_from_shortcut(addr) actual = email.get_email_from_shortcut(config, addr)
self.assertEqual(addr, actual) self.assertEqual(addr, actual)
email.CONFIG = {"aprsd": {"email": {"nothing": "nothing"}}} config = {"aprsd": {"email": {"nothing": "nothing"}}}
actual = email.get_email_from_shortcut(addr) actual = email.get_email_from_shortcut(config, addr)
self.assertEqual(addr, actual) self.assertEqual(addr, actual)
email.CONFIG = {"aprsd": {"email": {"shortcuts": {"not_used": "empty"}}}} config = {"aprsd": {"email": {"shortcuts": {"not_used": "empty"}}}}
actual = email.get_email_from_shortcut(addr) actual = email.get_email_from_shortcut(config, addr)
self.assertEqual(addr, actual) self.assertEqual(addr, actual)
email.CONFIG = {"aprsd": {"email": {"shortcuts": {"-wb": email_address}}}} config = {"aprsd": {"email": {"shortcuts": {"-wb": email_address}}}}
short = "-wb" short = "-wb"
actual = email.get_email_from_shortcut(short) actual = email.get_email_from_shortcut(config, short)
self.assertEqual(email_address, actual) self.assertEqual(email_address, actual)

View File

@ -4,7 +4,7 @@ from unittest import mock
import pytz import pytz
import aprsd import aprsd
from aprsd import messaging, packets, stats, utils from aprsd import config, messaging, packets, stats
from aprsd.fuzzyclock import fuzzy from aprsd.fuzzyclock import fuzzy
from aprsd.plugins import fortune as fortune_plugin from aprsd.plugins import fortune as fortune_plugin
from aprsd.plugins import ping as ping_plugin from aprsd.plugins import ping as ping_plugin
@ -19,7 +19,7 @@ class TestPlugin(unittest.TestCase):
def setUp(self): def setUp(self):
self.fromcall = fake.FAKE_FROM_CALLSIGN self.fromcall = fake.FAKE_FROM_CALLSIGN
self.ack = 1 self.ack = 1
self.config = utils.DEFAULT_CONFIG_DICT self.config = config.DEFAULT_CONFIG_DICT
self.config["ham"]["callsign"] = self.fromcall self.config["ham"]["callsign"] = self.fromcall
self.config["aprs"]["login"] = fake.FAKE_TO_CALLSIGN self.config["aprs"]["login"] = fake.FAKE_TO_CALLSIGN
# Inintialize the stats object with the config # Inintialize the stats object with the config