Merge pull request #107 from craigerl/oslo-config

Convert config to oslo_config
This commit is contained in:
Walter A. Boring IV 2022-12-29 09:17:51 -05:00 committed by GitHub
commit 473f00973b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 2872 additions and 2402 deletions

View File

@ -1,9 +1,43 @@
CHANGES CHANGES
======= =======
* Decouple admin web interface from server command
* Dockerfile now produces aprsd.conf
* Fix some unit tests and loading of CONF w/o file
* Added missing conf
* Removed references to old custom config
* Convert config to oslo\_config
* Added rain formatting unit tests to WeatherPacket
* Fix Rain reporting in WeatherPacket send
* Removed Packet.send()
* Removed watchlist plugins
* Fix PluginManager.get\_plugins
* Cleaned up PluginManager
* Cleaned up PluginManager
* Update routing for weatherpacket
* Fix some WeatherPacket formatting
* Fix pep8 violation
* Add packet filtering for aprsd listen
* Added WeatherPacket encoding
* Updated webchat and listen for queue based RX
* reworked collecting and reporting stats
* Removed unused threading code
* Change RX packet processing to enqueu
* Make tracking objectstores work w/o initializing
* Cleaned up packet transmit class attributes
* Fix packets timestamp to int
* More messaging -> packets cleanup
* Cleaned out all references to messaging
* Added contructing a GPSPacket for sending
* cleanup webchat
* Reworked all packet processing
* Updated plugins and plugin interfaces for Packet
* Started using dataclasses to describe packets
v2.6.1 v2.6.1
------ ------
* v2.6.1
* Fixed position report for webchat beacon * Fixed position report for webchat beacon
* Try and fix broken 32bit qemu builds on 64bit system * Try and fix broken 32bit qemu builds on 64bit system
* Add unit tests for webchat * Add unit tests for webchat

View File

@ -22,7 +22,7 @@ dev: venv ## Create a python virtual environment for development of aprsd
run: venv ## Create a virtual environment for running aprsd commands run: venv ## Create a virtual environment for running aprsd commands
docs: build docs: dev
cp README.rst docs/readme.rst cp README.rst docs/readme.rst
cp Changelog docs/changelog.rst cp Changelog docs/changelog.rst
tox -edocs tox -edocs

View File

@ -52,39 +52,49 @@ Current list of built-in plugins
:: ::
└─> aprsd list-plugins └─> aprsd list-plugins
🐍 APRSD Built-in Plugins 🐍 🐍 APRSD Built-in Plugins 🐍
┏━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Plugin Name ┃ Info ┃ Type ┃ Plugin Path ┃ ┃ Plugin Name ┃ Info ┃ Type ┃ Plugin Path ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ ┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ AVWXWeatherPlugin │ AVWX weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.AVWXWeatherPlugin │ │ AVWXWeatherPlugin │ AVWX weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.AVWXWeatherPlugin │
│ EmailPlugin │ Send and Receive email │ RegexCommand │ aprsd.plugins.email.EmailPlugin │ │ EmailPlugin │ Send and Receive email │ RegexCommand │ aprsd.plugins.email.EmailPlugin │
│ FortunePlugin │ Give me a fortune │ RegexCommand │ aprsd.plugins.fortune.FortunePlugin │ │ FortunePlugin │ Give me a fortune │ RegexCommand │ aprsd.plugins.fortune.FortunePlugin │
│ LocationPlugin │ Where in the world is a CALLSIGN's last GPS beacon? │ RegexCommand │ aprsd.plugins.location.LocationPlugin │ │ LocationPlugin │ Where in the world is a CALLSIGN's last GPS beacon? │ RegexCommand │ aprsd.plugins.location.LocationPlugin │
│ NotifySeenPlugin │ Notify me when a CALLSIGN is recently seen on APRS-IS │ WatchList │ aprsd.plugins.notify.NotifySeenPlugin │ │ NotifySeenPlugin │ Notify me when a CALLSIGN is recently seen on APRS-IS │ WatchList │ aprsd.plugins.notify.NotifySeenPlugin │
│ OWMWeatherPlugin │ OpenWeatherMap weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.OWMWeatherPlugin │ │ OWMWeatherPlugin │ OpenWeatherMap weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.OWMWeatherPlugin │
│ PingPlugin │ reply with a Pong! │ RegexCommand │ aprsd.plugins.ping.PingPlugin │ │ PingPlugin │ reply with a Pong! │ RegexCommand │ aprsd.plugins.ping.PingPlugin │
│ QueryPlugin │ APRSD Owner command to query messages in the MsgTrack │ RegexCommand │ aprsd.plugins.query.QueryPlugin │ │ QueryPlugin │ APRSD Owner command to query messages in the MsgTrack │ RegexCommand │ aprsd.plugins.query.QueryPlugin │
│ TimeOWMPlugin │ Current time of GPS beacon's timezone. Uses OpenWeatherMap │ RegexCommand │ aprsd.plugins.time.TimeOWMPlugin │ │ TimeOWMPlugin │ Current time of GPS beacon's timezone. Uses OpenWeatherMap │ RegexCommand │ aprsd.plugins.time.TimeOWMPlugin │
│ TimeOpenCageDataPlugin │ Current time of GPS beacon timezone. Uses OpenCage │ RegexCommand │ aprsd.plugins.time.TimeOpenCageDataPlugin │ │ TimePlugin │ What is the current local time. │ RegexCommand │ aprsd.plugins.time.TimePlugin │
│ TimePlugin │ What is the current local time. │ RegexCommand │ aprsd.plugins.time.TimePlugin │ │ USMetarPlugin │ USA only METAR of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USMetarPlugin │
│ USMetarPlugin │ USA only METAR of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USMetarPlugin │ │ USWeatherPlugin │ Provide USA only weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USWeatherPlugin │
│ USWeatherPlugin │ Provide USA only weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USWeatherPlugin │ │ VersionPlugin │ What is the APRSD Version │ RegexCommand │ aprsd.plugins.version.VersionPlugin │
│ VersionPlugin │ What is the APRSD Version │ RegexCommand │ aprsd.plugins.version.VersionPlugin │ └───────────────────┴────────────────────────────────────────────────────────────┴──────────────┴─────────────────────────────────────────┘
└────────────────────────┴────────────────────────────────────────────────────────────┴──────────────┴───────────────────────────────────────────┘
Pypi.org APRSD Installable Plugin Packages Pypi.org APRSD Installable Plugin Packages
Install any of the following plugins with pip install <Plugin Package Name> Install any of the following plugins with 'pip install <Plugin Package Name>'
┏━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Plugin Package Name ┃ Description ┃ Version ┃ Released ┃ Installed? ┃ ┃ Plugin Package Name ┃ Description ┃ Version ┃ Released ┃ Installed? ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ 📂 aprsd-stock-plugin │ Ham Radio APRSD Plugin for fetching stock quotes │ 0.1.2 │ Nov 9, 2021 │ No │ │ 📂 aprsd-stock-plugin │ Ham Radio APRSD Plugin for fetching stock quotes │ 0.1.3 │ Dec 2, 2022 │ No │
│ 📂 aprsd-weewx-plugin │ HAM Radio APRSD that reports weather from a weewx weather station. │ 0.1.4 │ Dec 7, 2021 │ No │ │ 📂 aprsd-sentry-plugin │ Ham radio APRSD plugin that does.... │ 0.1.2 │ Dec 2, 2022 │ No │
│ 📂 aprsd-telegram-plugin │ Ham Radio APRS APRSD plugin for Telegram IM service │ 0.1.2 │ Nov 9, 2021 │ No │ │ 📂 aprsd-timeopencage-plugin │ APRSD plugin for fetching time based on GPS location │ 0.1.0 │ Dec 2, 2022 │ No │
│ 📂 aprsd-twitter-plugin │ Python APRSD plugin to send tweets │ 0.3.0 │ Dec 7, 2021 │ No │ │ 📂 aprsd-weewx-plugin │ HAM Radio APRSD that reports weather from a weewx weather station. │ 0.1.4 │ Dec 7, 2021 │ Yes │
│ 📂 aprsd-slack-plugin │ Amateur radio APRS daemon which listens for messages and responds │ 1.0.4 │ Jan 15, 2021 │ No │ │ 📂 aprsd-repeat-plugins │ APRSD Plugins for the REPEAT service │ 1.0.12 │ Dec 2, 2022 │ No │
└──────────────────────────┴────────────────────────────────────────────────────────────────────┴─────────┴──────────────┴────────────┘ │ 📂 aprsd-telegram-plugin │ Ham Radio APRS APRSD plugin for Telegram IM service │ 0.1.3 │ Dec 2, 2022 │ No │
│ 📂 aprsd-twitter-plugin │ Python APRSD plugin to send tweets │ 0.3.0 │ Dec 7, 2021 │ No │
│ 📂 aprsd-slack-plugin │ Amateur radio APRS daemon which listens for messages and responds │ 1.0.5 │ Dec 18, 2022 │ No │
└──────────────────────────────┴────────────────────────────────────────────────────────────────────┴─────────┴──────────────┴────────────┘
🐍 APRSD Installed 3rd party Plugins 🐍
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Package Name ┃ Plugin Name ┃ Version ┃ Type ┃ Plugin Path ┃
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ aprsd-weewx-plugin │ WeewxMQTTPlugin │ 1.0 │ RegexCommand │ aprsd_weewx_plugin.weewx.WeewxMQTTPlugin │
└────────────────────┴─────────────────┴─────────┴──────────────┴──────────────────────────────────────────┘
Installation Installation
============= =============
@ -99,10 +109,10 @@ Example usage
``aprsd -h`` ``aprsd -h``
Help Help
---- ====
:: ::
└─[$] > aprsd -h └─> aprsd -h
Usage: aprsd [OPTIONS] COMMAND [ARGS]... Usage: aprsd [OPTIONS] COMMAND [ARGS]...
Options: Options:
@ -116,18 +126,19 @@ Help
healthcheck Check the health of the running aprsd server. healthcheck Check the health of the running aprsd server.
list-plugins List the built in plugins available to APRSD. list-plugins List the built in plugins available to APRSD.
listen Listen to packets on the APRS-IS Network based on FILTER. listen Listen to packets on the APRS-IS Network based on FILTER.
sample-config This dumps the config to stdout. sample-config Generate a sample Config file from aprsd and all...
send-message Send a message to a callsign via APRS_IS. send-message Send a message to a callsign via APRS_IS.
server Start the aprsd server gateway process. server Start the aprsd server gateway process.
version Show the APRSD version. version Show the APRSD version.
webchat Web based HAM Radio chat program!
Commands Commands
-------- ========
Configuration Configuration
^^^^^^^^^^^^^ =============
This command outputs a sample config yml formatted block that you can edit This command outputs a sample config yml formatted block that you can edit
and use to pass in to ``aprsd`` with ``-c``. By default aprsd looks in ``~/.config/aprsd/aprsd.yml`` and use to pass in to ``aprsd`` with ``-c``. By default aprsd looks in ``~/.config/aprsd/aprsd.yml``
@ -136,108 +147,10 @@ and use to pass in to ``aprsd`` with ``-c``. By default aprsd looks in ``~/.con
:: ::
└─> aprsd sample-config └─> aprsd sample-config
aprs: ...
# Set enabled to False if there is no internet connectivity.
# This is useful for a direwolf KISS aprs connection only.
# Get the passcode for your callsign here:
# https://apps.magicbug.co.uk/passcode
enabled: true
host: rotate.aprs2.net
login: CALLSIGN
password: '00000'
port: 14580
aprsd:
dateformat: '%m/%d/%Y %I:%M:%S %p'
email:
enabled: true
imap:
debug: false
host: imap.gmail.com
login: IMAP_USERNAME
password: IMAP_PASSWORD
port: 993
use_ssl: true
shortcuts:
aa: 5551239999@vtext.com
cl: craiglamparter@somedomain.org
wb: 555309@vtext.com
smtp:
debug: false
host: smtp.gmail.com
login: SMTP_USERNAME
password: SMTP_PASSWORD
port: 465
use_ssl: false
enabled_plugins:
- aprsd.plugins.email.EmailPlugin
- aprsd.plugins.fortune.FortunePlugin
- aprsd.plugins.location.LocationPlugin
- aprsd.plugins.ping.PingPlugin
- aprsd.plugins.query.QueryPlugin
- aprsd.plugins.stock.StockPlugin
- aprsd.plugins.time.TimePlugin
- aprsd.plugins.weather.USWeatherPlugin
- aprsd.plugins.version.VersionPlugin
logfile: /tmp/aprsd.log
logformat: '[%(asctime)s] [%(threadName)-20.20s] [%(levelname)-5.5s] %(message)s
- [%(pathname)s:%(lineno)d]'
rich_logging: false
save_location: /Users/i530566/.config/aprsd/
trace: false
units: imperial
watch_list:
alert_callsign: NOCALL
alert_time_seconds: 43200
callsigns: []
enabled: false
enabled_plugins:
- aprsd.plugins.notify.NotifySeenPlugin
packet_keep_count: 10
web:
enabled: true
host: 0.0.0.0
logging_enabled: true
port: 8001
users:
admin: password-here
ham:
callsign: NOCALL
kiss:
serial:
baudrate: 9600
device: /dev/ttyS0
enabled: false
tcp:
enabled: false
host: direwolf.ip.address
port: '8001'
services:
aprs.fi:
# Get the apiKey from your aprs.fi account here:
# http://aprs.fi/account
apiKey: APIKEYVALUE
avwx:
# (Optional for AVWXWeatherPlugin)
# Use hosted avwx-api here: https://avwx.rest
# or deploy your own from here:
# https://github.com/avwx-rest/avwx-api
apiKey: APIKEYVALUE
base_url: http://host:port
opencagedata:
# (Optional for TimeOpenCageDataPlugin)
# Get the apiKey from your opencagedata account here:
# https://opencagedata.com/dashboard#api-keys
apiKey: APIKEYVALUE
openweathermap:
# (Optional for OWMWeatherPlugin)
# Get the apiKey from your
# openweathermap account here:
# https://home.openweathermap.org/api_keys
apiKey: APIKEYVALUE
server server
^^^^^^ ======
This is the main server command that will listen to APRS-IS servers and This is the main server command that will listen to APRS-IS servers and
look for incomming commands to the callsign configured in the config file look for incomming commands to the callsign configured in the config file
@ -277,7 +190,7 @@ look for incomming commands to the callsign configured in the config file
send-message send-message
^^^^^^^^^^^^ ============
This command is typically used for development to send another aprsd instance This command is typically used for development to send another aprsd instance
test messages test messages
@ -310,7 +223,7 @@ test messages
SEND EMAIL (radio to smtp server) SEND EMAIL (radio to smtp server)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ =================================
:: ::
@ -332,7 +245,7 @@ SEND EMAIL (radio to smtp server)
RECEIVE EMAIL (imap server to radio) RECEIVE EMAIL (imap server to radio)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ====================================
:: ::
@ -349,7 +262,7 @@ RECEIVE EMAIL (imap server to radio)
LOCATION LOCATION
^^^^^^^^ ========
:: ::
@ -384,7 +297,7 @@ Development
* ``make`` * ``make``
Workflow Workflow
-------- ========
While working aprsd, The workflow is as follows: While working aprsd, The workflow is as follows:
@ -413,7 +326,7 @@ While working aprsd, The workflow is as follows:
Release Release
------- =======
To do release to pypi: To do release to pypi:
@ -435,10 +348,10 @@ To do release to pypi:
Docker Container Docker Container
---------------- ================
Building Building
^^^^^^^^ ========
There are 2 versions of the container Dockerfile that can be used. There are 2 versions of the container Dockerfile that can be used.
The main Dockerfile, which is for building the official release container The main Dockerfile, which is for building the official release container
@ -447,18 +360,18 @@ which is used for building a container based off of a git branch of
the repo. the repo.
Official Build Official Build
^^^^^^^^^^^^^^ ==============
``docker build -t hemna6969/aprsd:latest .`` ``docker build -t hemna6969/aprsd:latest .``
Development Build Development Build
^^^^^^^^^^^^^^^^^ =================
``docker build -t hemna6969/aprsd:latest -f Dockerfile-dev .`` ``docker build -t hemna6969/aprsd:latest -f Dockerfile-dev .``
Running the container Running the container
^^^^^^^^^^^^^^^^^^^^^ =====================
There is a ``docker-compose.yml`` file in the ``docker/`` directory There is a ``docker-compose.yml`` file in the ``docker/`` directory
that can be used to run your container. To provide the container that can be used to run your container. To provide the container

View File

@ -21,6 +21,8 @@
# python included libs # python included libs
import datetime import datetime
from importlib.metadata import entry_points
from importlib.metadata import version as metadata_version
import logging import logging
import os import os
import signal import signal
@ -29,19 +31,20 @@ import time
import click import click
import click_completion import click_completion
from oslo_config import cfg, generator
# local imports here # local imports here
import aprsd import aprsd
from aprsd import cli_helper from aprsd import cli_helper, packets, stats, threads, utils
from aprsd import config as aprsd_config
from aprsd import packets, stats, threads, utils
# setup the global logger # setup the global logger
# logging.basicConfig(level=logging.DEBUG) # level=10 # logging.basicConfig(level=logging.DEBUG) # level=10
CONF = cfg.CONF
LOG = logging.getLogger("APRSD") 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
rpc_serv = None
def custom_startswith(string, incomplete): def custom_startswith(string, incomplete):
@ -91,6 +94,7 @@ def signal_handler(sig, frame):
LOG.info(stats.APRSDStats()) LOG.info(stats.APRSDStats())
# signal.signal(signal.SIGTERM, sys.exit(0)) # signal.signal(signal.SIGTERM, sys.exit(0))
# sys.exit(0) # sys.exit(0)
if flask_enabled: if flask_enabled:
signal.signal(signal.SIGTERM, sys.exit(0)) signal.signal(signal.SIGTERM, sys.exit(0))
@ -111,8 +115,32 @@ def check_version(ctx):
@cli.command() @cli.command()
@click.pass_context @click.pass_context
def sample_config(ctx): def sample_config(ctx):
"""This dumps the config to stdout.""" """Generate a sample Config file from aprsd and all installed plugins."""
click.echo(aprsd_config.dump_default_cfg())
def get_namespaces():
args = []
selected = entry_points(group="oslo.config.opts")
for entry in selected:
if "aprsd" in entry.name:
args.append("--namespace")
args.append(entry.name)
return args
args = get_namespaces()
config_version = metadata_version("oslo.config")
logging.basicConfig(level=logging.WARN)
conf = cfg.ConfigOpts()
generator.register_cli_opts(conf)
try:
conf(args, version=config_version)
except cfg.RequiredOptError:
conf.print_help()
if not sys.argv[1:]:
raise SystemExit
raise
generator.generate(conf)
@cli.command() @cli.command()

View File

@ -1,13 +1,24 @@
from functools import update_wrapper from functools import update_wrapper
import logging
from pathlib import Path
import typing as t import typing as t
import click import click
from oslo_config import cfg
from aprsd import config as aprsd_config import aprsd
from aprsd import conf # noqa: F401
from aprsd.logging import log from aprsd.logging import log
from aprsd.utils import trace from aprsd.utils import trace
CONF = cfg.CONF
home = str(Path.home())
DEFAULT_CONFIG_DIR = f"{home}/.config/aprsd/"
DEFAULT_SAVE_FILE = f"{home}/.config/aprsd/aprsd.p"
DEFAULT_CONFIG_FILE = f"{home}/.config/aprsd/aprsd.conf"
F = t.TypeVar("F", bound=t.Callable[..., t.Any]) F = t.TypeVar("F", bound=t.Callable[..., t.Any])
common_options = [ common_options = [
@ -27,7 +38,7 @@ common_options = [
"--config", "--config",
"config_file", "config_file",
show_default=True, show_default=True,
default=aprsd_config.DEFAULT_CONFIG_FILE, default=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(
@ -51,18 +62,32 @@ def process_standard_options(f: F) -> F:
def new_func(*args, **kwargs): def new_func(*args, **kwargs):
ctx = args[0] ctx = args[0]
ctx.ensure_object(dict) ctx.ensure_object(dict)
config_file_found = True
if kwargs["config_file"]:
default_config_files = [kwargs["config_file"]]
else:
default_config_files = None
try:
CONF(
[], project="aprsd", version=aprsd.__version__,
default_config_files=default_config_files,
)
except cfg.ConfigFilesNotFoundError:
config_file_found = False
ctx.obj["loglevel"] = kwargs["loglevel"] ctx.obj["loglevel"] = kwargs["loglevel"]
ctx.obj["config_file"] = kwargs["config_file"] # ctx.obj["config_file"] = kwargs["config_file"]
ctx.obj["quiet"] = kwargs["quiet"] ctx.obj["quiet"] = kwargs["quiet"]
ctx.obj["config"] = aprsd_config.parse_config(kwargs["config_file"])
log.setup_logging( log.setup_logging(
ctx.obj["config"],
ctx.obj["loglevel"], ctx.obj["loglevel"],
ctx.obj["quiet"], ctx.obj["quiet"],
) )
if ctx.obj["config"]["aprsd"].get("trace", False): if CONF.trace_enabled:
trace.setup_tracing(["method", "api"]) trace.setup_tracing(["method", "api"])
if not config_file_found:
LOG = logging.getLogger("APRSD") # noqa: N806
LOG.error("No config file found!! run 'aprsd sample-config'")
del kwargs["loglevel"] del kwargs["loglevel"]
del kwargs["config_file"] del kwargs["config_file"]
del kwargs["quiet"] del kwargs["quiet"]

View File

@ -4,14 +4,15 @@ import time
import aprslib import aprslib
from aprslib.exceptions import LoginError from aprslib.exceptions import LoginError
from oslo_config import cfg
from aprsd import config as aprsd_config
from aprsd import exception from aprsd import exception
from aprsd.clients import aprsis, kiss from aprsd.clients import aprsis, kiss
from aprsd.packets import core, packet_list from aprsd.packets import core, packet_list
from aprsd.utils import trace from aprsd.utils import trace
CONF = cfg.CONF
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
TRANSPORT_APRSIS = "aprsis" TRANSPORT_APRSIS = "aprsis"
TRANSPORT_TCPKISS = "tcpkiss" TRANSPORT_TCPKISS = "tcpkiss"
@ -28,7 +29,6 @@ class Client:
_instance = None _instance = None
_client = None _client = None
config = None
connected = False connected = False
server_string = None server_string = None
@ -41,11 +41,6 @@ class Client:
# Put any initialization here. # Put any initialization here.
return cls._instance return cls._instance
def __init__(self, config=None):
"""Initialize the object instance."""
if config:
self.config = config
def set_filter(self, filter): def set_filter(self, filter):
self.filter = filter self.filter = filter
if self._client: if self._client:
@ -74,12 +69,12 @@ class Client:
@staticmethod @staticmethod
@abc.abstractmethod @abc.abstractmethod
def is_enabled(config): def is_enabled():
pass pass
@staticmethod @staticmethod
@abc.abstractmethod @abc.abstractmethod
def transport(config): def transport():
pass pass
@abc.abstractmethod @abc.abstractmethod
@ -90,26 +85,38 @@ class Client:
class APRSISClient(Client): class APRSISClient(Client):
@staticmethod @staticmethod
def is_enabled(config): def is_enabled():
# Defaults to True if the enabled flag is non existent # Defaults to True if the enabled flag is non existent
try: try:
return config["aprs"].get("enabled", True) return CONF.aprs_network.enabled
except KeyError: except KeyError:
return False return False
@staticmethod @staticmethod
def is_configured(config): def is_configured():
if APRSISClient.is_enabled(config): if APRSISClient.is_enabled():
# Ensure that the config vars are correctly set # Ensure that the config vars are correctly set
config.check_option("aprs.login") if not CONF.aprs_network.login:
config.check_option("aprs.password") LOG.error("Config aprs_network.login not set.")
config.check_option("aprs.host") raise exception.MissingConfigOptionException(
return True "aprs_network.login is not set.",
)
if not CONF.aprs_network.password:
LOG.error("Config aprs_network.password not set.")
raise exception.MissingConfigOptionException(
"aprs_network.password is not set.",
)
if not CONF.aprs_network.host:
LOG.error("Config aprs_network.host not set.")
raise exception.MissingConfigOptionException(
"aprs_network.host is not set.",
)
return True
return True return True
@staticmethod @staticmethod
def transport(config): def transport():
return TRANSPORT_APRSIS return TRANSPORT_APRSIS
def decode_packet(self, *args, **kwargs): def decode_packet(self, *args, **kwargs):
@ -118,10 +125,10 @@ class APRSISClient(Client):
@trace.trace @trace.trace
def setup_connection(self): def setup_connection(self):
user = self.config["aprs"]["login"] user = CONF.aprs_network.login
password = self.config["aprs"]["password"] password = CONF.aprs_network.password
host = self.config["aprs"].get("host", "rotate.aprs.net") host = CONF.aprs_network.host
port = self.config["aprs"].get("port", 14580) port = CONF.aprs_network.port
connected = False connected = False
backoff = 1 backoff = 1
aprs_client = None aprs_client = None
@ -151,45 +158,43 @@ class APRSISClient(Client):
class KISSClient(Client): class KISSClient(Client):
@staticmethod @staticmethod
def is_enabled(config): def is_enabled():
"""Return if tcp or serial KISS is enabled.""" """Return if tcp or serial KISS is enabled."""
if "kiss" not in config: if CONF.kiss_serial.enabled:
return False
if config.get("kiss.serial.enabled", default=False):
return True return True
if config.get("kiss.tcp.enabled", default=False): if CONF.kiss_tcp.enabled:
return True return True
return False return False
@staticmethod @staticmethod
def is_configured(config): def is_configured():
# Ensure that the config vars are correctly set # Ensure that the config vars are correctly set
if KISSClient.is_enabled(config): if KISSClient.is_enabled():
config.check_option( transport = KISSClient.transport()
"aprsd.callsign",
default_fail=aprsd_config.DEFAULT_CONFIG_DICT["aprsd"]["callsign"],
)
transport = KISSClient.transport(config)
if transport == TRANSPORT_SERIALKISS: if transport == TRANSPORT_SERIALKISS:
config.check_option("kiss.serial") if not CONF.kiss_serial.device:
config.check_option("kiss.serial.device") LOG.error("KISS serial enabled, but no device is set.")
raise exception.MissingConfigOptionException(
"kiss_serial.device is not set.",
)
elif transport == TRANSPORT_TCPKISS: elif transport == TRANSPORT_TCPKISS:
config.check_option("kiss.tcp") if not CONF.kiss_tcp.host:
config.check_option("kiss.tcp.host") LOG.error("KISS TCP enabled, but no host is set.")
config.check_option("kiss.tcp.port") raise exception.MissingConfigOptionException(
"kiss_tcp.host is not set.",
)
return True return True
return True return False
@staticmethod @staticmethod
def transport(config): def transport():
if config.get("kiss.serial.enabled", default=False): if CONF.kiss_serial.enabled:
return TRANSPORT_SERIALKISS return TRANSPORT_SERIALKISS
if config.get("kiss.tcp.enabled", default=False): if CONF.kiss_tcp.enabled:
return TRANSPORT_TCPKISS return TRANSPORT_TCPKISS
def decode_packet(self, *args, **kwargs): def decode_packet(self, *args, **kwargs):
@ -208,7 +213,7 @@ class KISSClient(Client):
@trace.trace @trace.trace
def setup_connection(self): def setup_connection(self):
client = kiss.KISS3Client(self.config) client = kiss.KISS3Client()
return client return client
@ -222,8 +227,7 @@ class ClientFactory:
# Put any initialization here. # Put any initialization here.
return cls._instance return cls._instance
def __init__(self, config): def __init__(self):
self.config = config
self._builders = {} self._builders = {}
def register(self, key, builder): def register(self, key, builder):
@ -231,23 +235,23 @@ class ClientFactory:
def create(self, key=None): def create(self, key=None):
if not key: if not key:
if APRSISClient.is_enabled(self.config): if APRSISClient.is_enabled():
key = TRANSPORT_APRSIS key = TRANSPORT_APRSIS
elif KISSClient.is_enabled(self.config): elif KISSClient.is_enabled():
key = KISSClient.transport(self.config) key = KISSClient.transport()
LOG.debug(f"GET client '{key}'") LOG.debug(f"GET client '{key}'")
builder = self._builders.get(key) builder = self._builders.get(key)
if not builder: if not builder:
raise ValueError(key) raise ValueError(key)
return builder(self.config) return builder()
def is_client_enabled(self): def is_client_enabled(self):
"""Make sure at least one client is enabled.""" """Make sure at least one client is enabled."""
enabled = False enabled = False
for key in self._builders.keys(): for key in self._builders.keys():
try: try:
enabled |= self._builders[key].is_enabled(self.config) enabled |= self._builders[key].is_enabled()
except KeyError: except KeyError:
pass pass
@ -257,7 +261,7 @@ class ClientFactory:
enabled = False enabled = False
for key in self._builders.keys(): for key in self._builders.keys():
try: try:
enabled |= self._builders[key].is_configured(self.config) enabled |= self._builders[key].is_configured()
except KeyError: except KeyError:
pass pass
except exception.MissingConfigOptionException as ex: except exception.MissingConfigOptionException as ex:
@ -270,11 +274,11 @@ class ClientFactory:
return enabled return enabled
@staticmethod @staticmethod
def setup(config): def setup():
"""Create and register all possible client objects.""" """Create and register all possible client objects."""
global factory global factory
factory = ClientFactory(config) factory = ClientFactory()
factory.register(TRANSPORT_APRSIS, APRSISClient) factory.register(TRANSPORT_APRSIS, APRSISClient)
factory.register(TRANSPORT_TCPKISS, KISSClient) factory.register(TRANSPORT_TCPKISS, KISSClient)
factory.register(TRANSPORT_SERIALKISS, KISSClient) factory.register(TRANSPORT_SERIALKISS, KISSClient)

View File

@ -6,13 +6,15 @@
import logging import logging
import click import click
from oslo_config import cfg
# local imports here # local imports here
from aprsd import cli_helper, client, packets, plugin, stats, utils from aprsd import cli_helper, client, conf, packets, plugin
from aprsd.aprsd import cli from aprsd.aprsd import cli
from aprsd.utils import trace from aprsd.utils import trace
CONF = cfg.CONF
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
@ -68,50 +70,40 @@ def test_plugin(
message, message,
): ):
"""Test an individual APRSD plugin given a python path.""" """Test an individual APRSD plugin given a python path."""
config = ctx.obj["config"]
flat_config = utils.flatten_dict(config) CONF.log_opt_values(LOG, logging.DEBUG)
LOG.info("Using CONFIG values:")
for x in flat_config:
if "password" in x or "aprsd.web.users.admin" in x:
LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX")
else:
LOG.info(f"{x} = {flat_config[x]}")
if not aprs_login: if not aprs_login:
if not config.exists("aprs.login"): if CONF.aprs_network.login == conf.client.DEFAULT_LOGIN:
click.echo("Must set --aprs_login or APRS_LOGIN") click.echo("Must set --aprs_login or APRS_LOGIN")
ctx.exit(-1) ctx.exit(-1)
return return
else: else:
fromcall = config.get("aprs.login") fromcall = CONF.aprs_network.login
else: else:
fromcall = aprs_login fromcall = aprs_login
if not plugin_path: if not plugin_path:
click.echo(ctx.get_help()) click.echo(ctx.get_help())
click.echo("") click.echo("")
ctx.fail("Failed to provide -p option to test a plugin") click.echo("Failed to provide -p option to test a plugin")
ctx.exit(-1)
return return
if type(message) is tuple: if type(message) is tuple:
message = " ".join(message) message = " ".join(message)
if config["aprsd"].get("trace", False): if CONF.trace_enabled:
trace.setup_tracing(["method", "api"]) trace.setup_tracing(["method", "api"])
client.Client(config) client.Client()
stats.APRSDStats(config)
packets.PacketTrack(config=config)
packets.WatchList(config=config)
packets.SeenList(config=config)
pm = plugin.PluginManager(config) pm = plugin.PluginManager()
if load_all: if load_all:
pm.setup_plugins() pm.setup_plugins()
else: else:
pm._init() pm._init()
obj = pm._create_class(plugin_path, plugin.APRSDPluginBase, config=config) obj = pm._create_class(plugin_path, plugin.APRSDPluginBase)
if not obj: if not obj:
click.echo(ctx.get_help()) click.echo(ctx.get_help())
click.echo("") click.echo("")
@ -125,14 +117,13 @@ def test_plugin(
), ),
) )
pm._pluggy_pm.register(obj) pm._pluggy_pm.register(obj)
login = config["aprs"]["login"]
packet = { packet = packets.MessagePacket(
"from": fromcall, "addresse": login, from_call=fromcall,
"message_text": message, to_call=CONF.callsign,
"format": "message", msgNo=1,
"msgNo": 1, message_text=message,
} )
LOG.info(f"P'{plugin_path}' F'{fromcall}' C'{message}'") LOG.info(f"P'{plugin_path}' F'{fromcall}' C'{message}'")
for x in range(number): for x in range(number):

View File

@ -10,11 +10,12 @@ import sys
import time import time
import click import click
from oslo_config import cfg
from rich.console import Console from rich.console import Console
# local imports here # local imports here
import aprsd import aprsd
from aprsd import cli_helper, client, packets, stats, threads, utils from aprsd import cli_helper, client, packets, stats, threads
from aprsd.aprsd import cli from aprsd.aprsd import cli
from aprsd.threads import rx from aprsd.threads import rx
@ -22,6 +23,7 @@ from aprsd.threads import rx
# setup the global logger # setup the global logger
# logging.basicConfig(level=logging.DEBUG) # level=10 # logging.basicConfig(level=logging.DEBUG) # level=10
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
CONF = cfg.CONF
console = Console() console = Console()
@ -38,8 +40,8 @@ def signal_handler(sig, frame):
class APRSDListenThread(rx.APRSDRXThread): class APRSDListenThread(rx.APRSDRXThread):
def __init__(self, config, packet_queue, packet_filter=None): def __init__(self, packet_queue, packet_filter=None):
super().__init__(config, packet_queue) super().__init__(packet_queue)
self.packet_filter = packet_filter self.packet_filter = packet_filter
def process_packet(self, *args, **kwargs): def process_packet(self, *args, **kwargs):
@ -118,7 +120,6 @@ def listen(
""" """
signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGTERM, signal_handler)
config = ctx.obj["config"]
if not aprs_login: if not aprs_login:
click.echo(ctx.get_help()) click.echo(ctx.get_help())
@ -132,27 +133,19 @@ def listen(
ctx.fail("Must set --aprs-password or APRS_PASSWORD") ctx.fail("Must set --aprs-password or APRS_PASSWORD")
ctx.exit() ctx.exit()
config["aprs"]["login"] = aprs_login # CONF.aprs_network.login = aprs_login
config["aprs"]["password"] = aprs_password # config["aprs"]["password"] = aprs_password
LOG.info(f"APRSD Listen Started version: {aprsd.__version__}") LOG.info(f"APRSD Listen Started version: {aprsd.__version__}")
flat_config = utils.flatten_dict(config) CONF.log_opt_values(LOG, logging.DEBUG)
LOG.info("Using CONFIG values:")
for x in flat_config:
if "password" in x or "aprsd.web.users.admin" in x:
LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX")
else:
LOG.info(f"{x} = {flat_config[x]}")
stats.APRSDStats(config)
# Try and load saved MsgTrack list # Try and load saved MsgTrack list
LOG.debug("Loading saved MsgTrack object.") LOG.debug("Loading saved MsgTrack object.")
# Initialize the client factory and create # Initialize the client factory and create
# The correct client object ready for use # The correct client object ready for use
client.ClientFactory.setup(config) client.ClientFactory.setup()
# Make sure we have 1 client transport enabled # Make sure we have 1 client transport enabled
if not client.factory.is_client_enabled(): if not client.factory.is_client_enabled():
LOG.error("No Clients are enabled in config.") LOG.error("No Clients are enabled in config.")
@ -166,12 +159,11 @@ def listen(
LOG.debug(f"Filter by '{filter}'") LOG.debug(f"Filter by '{filter}'")
aprs_client.set_filter(filter) aprs_client.set_filter(filter)
keepalive = threads.KeepAliveThread(config=config) keepalive = threads.KeepAliveThread()
keepalive.start() keepalive.start()
LOG.debug("Create APRSDListenThread") LOG.debug("Create APRSDListenThread")
listen_thread = APRSDListenThread( listen_thread = APRSDListenThread(
config=config,
packet_queue=threads.packet_queue, packet_queue=threads.packet_queue,
packet_filter=packet_filter, packet_filter=packet_filter,
) )

View File

@ -5,13 +5,16 @@ import time
import aprslib import aprslib
from aprslib.exceptions import LoginError from aprslib.exceptions import LoginError
import click import click
from oslo_config import cfg
import aprsd import aprsd
from aprsd import cli_helper, client, packets from aprsd import cli_helper, client, packets
from aprsd import conf # noqa : F401
from aprsd.aprsd import cli from aprsd.aprsd import cli
from aprsd.threads import tx from aprsd.threads import tx
CONF = cfg.CONF
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -62,24 +65,24 @@ 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 = ctx.obj["config"]
quiet = ctx.obj["quiet"] quiet = ctx.obj["quiet"]
if not aprs_login: if not aprs_login:
if not config.exists("aprs.login"): if CONF.aprs_network.login == conf.client.DEFAULT_LOGIN:
click.echo("Must set --aprs_login or APRS_LOGIN") click.echo("Must set --aprs_login or APRS_LOGIN")
ctx.exit(-1) ctx.exit(-1)
return return
else: else:
config["aprs"]["login"] = aprs_login aprs_login = CONF.aprs_network.login
if not aprs_password: if not aprs_password:
if not config.exists("aprs.password"): LOG.warning(CONF.aprs_network.password)
if not CONF.aprs_network.password:
click.echo("Must set --aprs-password or APRS_PASSWORD") click.echo("Must set --aprs-password or APRS_PASSWORD")
ctx.exit(-1) ctx.exit(-1)
return return
else: else:
config["aprs"]["password"] = aprs_password aprs_password = CONF.aprs_network.password
LOG.info(f"APRSD LISTEN Started version: {aprsd.__version__}") LOG.info(f"APRSD LISTEN Started version: {aprsd.__version__}")
if type(command) is tuple: if type(command) is tuple:
@ -90,9 +93,9 @@ def send_message(
else: else:
LOG.info(f"L'{aprs_login}' To'{tocallsign}' C'{command}'") LOG.info(f"L'{aprs_login}' To'{tocallsign}' C'{command}'")
packets.PacketList(config=config) packets.PacketList()
packets.WatchList(config=config) packets.WatchList()
packets.SeenList(config=config) packets.SeenList()
got_ack = False got_ack = False
got_response = False got_response = False
@ -109,7 +112,7 @@ def send_message(
else: else:
got_response = True got_response = True
from_call = packet.from_call from_call = packet.from_call
our_call = config["aprsd"]["callsign"].lower() our_call = CONF.callsign.lower()
tx.send( tx.send(
packets.AckPacket( packets.AckPacket(
from_call=our_call, from_call=our_call,
@ -127,7 +130,7 @@ def send_message(
sys.exit(0) sys.exit(0)
try: try:
client.ClientFactory.setup(config) client.ClientFactory.setup()
client.factory.create().client client.factory.create().client
except LoginError: except LoginError:
sys.exit(-1) sys.exit(-1)

View File

@ -3,16 +3,18 @@ import signal
import sys import sys
import click import click
from oslo_config import cfg
import aprsd import aprsd
from aprsd import ( from aprsd import (
cli_helper, client, flask, packets, plugin, stats, threads, utils, cli_helper, client, packets, plugin, rpc_server, threads, utils,
) )
from aprsd import aprsd as aprsd_main from aprsd import aprsd as aprsd_main
from aprsd.aprsd import cli from aprsd.aprsd import cli
from aprsd.threads import rx from aprsd.threads import rx
CONF = cfg.CONF
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -32,17 +34,9 @@ LOG = logging.getLogger("APRSD")
@cli_helper.process_standard_options @cli_helper.process_standard_options
def server(ctx, flush): def server(ctx, flush):
"""Start the aprsd server gateway process.""" """Start the aprsd server gateway process."""
ctx.obj["config_file"]
loglevel = ctx.obj["loglevel"]
quiet = ctx.obj["quiet"]
config = ctx.obj["config"]
signal.signal(signal.SIGINT, aprsd_main.signal_handler) signal.signal(signal.SIGINT, aprsd_main.signal_handler)
signal.signal(signal.SIGTERM, aprsd_main.signal_handler) signal.signal(signal.SIGTERM, aprsd_main.signal_handler)
if not quiet:
click.echo("Load config")
level, msg = utils._check_version() level, msg = utils._check_version()
if level: if level:
LOG.warning(msg) LOG.warning(msg)
@ -50,19 +44,11 @@ def server(ctx, flush):
LOG.info(msg) LOG.info(msg)
LOG.info(f"APRSD Started version: {aprsd.__version__}") LOG.info(f"APRSD Started version: {aprsd.__version__}")
flat_config = utils.flatten_dict(config) CONF.log_opt_values(LOG, logging.DEBUG)
LOG.info("Using CONFIG values:")
for x in flat_config:
if "password" in x or "aprsd.web.users.admin" in x:
LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX")
else:
LOG.info(f"{x} = {flat_config[x]}")
stats.APRSDStats(config)
# Initialize the client factory and create # Initialize the client factory and create
# The correct client object ready for use # The correct client object ready for use
client.ClientFactory.setup(config) client.ClientFactory.setup()
# Make sure we have 1 client transport enabled # Make sure we have 1 client transport enabled
if not client.factory.is_client_enabled(): if not client.factory.is_client_enabled():
LOG.error("No Clients are enabled in config.") LOG.error("No Clients are enabled in config.")
@ -77,30 +63,28 @@ def server(ctx, flush):
client.factory.create().client client.factory.create().client
# Now load the msgTrack from disk if any # Now load the msgTrack from disk if any
packets.PacketList(config=config) packets.PacketList()
if flush: if flush:
LOG.debug("Deleting saved MsgTrack.") LOG.debug("Deleting saved MsgTrack.")
packets.PacketTrack(config=config).flush() packets.PacketTrack().flush()
packets.WatchList(config=config) packets.WatchList()
packets.SeenList(config=config) packets.SeenList()
else: else:
# Try and load saved MsgTrack list # Try and load saved MsgTrack list
LOG.debug("Loading saved MsgTrack object.") LOG.debug("Loading saved MsgTrack object.")
packets.PacketTrack(config=config).load() packets.PacketTrack().load()
packets.WatchList(config=config).load() packets.WatchList().load()
packets.SeenList(config=config).load() packets.SeenList().load()
# Create the initial PM singleton and Register plugins # Create the initial PM singleton and Register plugins
LOG.info("Loading Plugin Manager and registering plugins") LOG.info("Loading Plugin Manager and registering plugins")
plugin_manager = plugin.PluginManager(config) plugin_manager = plugin.PluginManager()
plugin_manager.setup_plugins() plugin_manager.setup_plugins()
rx_thread = rx.APRSDPluginRXThread( rx_thread = rx.APRSDPluginRXThread(
packet_queue=threads.packet_queue, packet_queue=threads.packet_queue,
config=config,
) )
process_thread = rx.APRSDPluginProcessPacketThread( process_thread = rx.APRSDPluginProcessPacketThread(
config=config,
packet_queue=threads.packet_queue, packet_queue=threads.packet_queue,
) )
rx_thread.start() rx_thread.start()
@ -108,21 +92,13 @@ def server(ctx, flush):
packets.PacketTrack().restart() packets.PacketTrack().restart()
keepalive = threads.KeepAliveThread(config=config) keepalive = threads.KeepAliveThread()
keepalive.start() keepalive.start()
web_enabled = config.get("aprsd.web.enabled", default=False) if CONF.rpc_settings.enabled:
rpc = rpc_server.APRSDRPCThread()
rpc.start()
log_monitor = threads.log_monitor.LogMonitorThread()
log_monitor.start()
if web_enabled:
aprsd_main.flask_enabled = True
(socketio, app) = flask.init_flask(config, loglevel, quiet)
socketio.run(
app,
allow_unsafe_werkzeug=True,
host=config["aprsd"]["web"]["host"],
port=config["aprsd"]["web"]["port"],
)
# If there are items in the msgTracker, then save them
LOG.info("APRSD Exiting.")
return 0 return 0

View File

@ -15,23 +15,24 @@ from flask.logging import default_handler
import flask_classful import flask_classful
from flask_httpauth import HTTPBasicAuth from flask_httpauth import HTTPBasicAuth
from flask_socketio import Namespace, SocketIO from flask_socketio import Namespace, SocketIO
from oslo_config import cfg
from user_agents import parse as ua_parse from user_agents import parse as ua_parse
from werkzeug.security import check_password_hash, generate_password_hash from werkzeug.security import check_password_hash, generate_password_hash
import wrapt import wrapt
import aprsd import aprsd
from aprsd import cli_helper, client from aprsd import cli_helper, client, conf, packets, stats, threads, utils
from aprsd import config as aprsd_config
from aprsd import packets, stats, threads, utils
from aprsd.aprsd import cli from aprsd.aprsd import cli
from aprsd.logging import rich as aprsd_logging from aprsd.logging import rich as aprsd_logging
from aprsd.threads import rx, tx from aprsd.threads import rx, tx
from aprsd.utils import objectstore, trace from aprsd.utils import objectstore, trace
CONF = cfg.CONF
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
auth = HTTPBasicAuth() auth = HTTPBasicAuth()
users = None users = None
socketio = None
def signal_handler(sig, frame): def signal_handler(sig, frame):
@ -128,16 +129,16 @@ class SentMessages(objectstore.ObjectStoreMixin):
def verify_password(username, password): def verify_password(username, password):
global users global users
if username in users and check_password_hash(users.get(username), password): if username in users and check_password_hash(users[username], password):
return username return username
class WebChatProcessPacketThread(rx.APRSDProcessPacketThread): class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
"""Class that handles packets being sent to us.""" """Class that handles packets being sent to us."""
def __init__(self, config, packet_queue, socketio): def __init__(self, packet_queue, socketio):
self.socketio = socketio self.socketio = socketio
self.connected = False self.connected = False
super().__init__(config, packet_queue) super().__init__(packet_queue)
def process_ack_packet(self, packet: packets.AckPacket): def process_ack_packet(self, packet: packets.AckPacket):
super().process_ack_packet(packet) super().process_ack_packet(packet)
@ -174,21 +175,16 @@ class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
class WebChatFlask(flask_classful.FlaskView): class WebChatFlask(flask_classful.FlaskView):
config = None
def set_config(self, config): def set_config(self):
global users global users
self.config = config
self.users = {} self.users = {}
for user in self.config["aprsd"]["web"]["users"]: user = CONF.admin.user
self.users[user] = generate_password_hash( self.users[user] = generate_password_hash(CONF.admin.password)
self.config["aprsd"]["web"]["users"][user],
)
users = self.users users = self.users
def _get_transport(self, stats): def _get_transport(self, stats):
if self.config["aprs"].get("enabled", True): if CONF.aprs_network.enabled:
transport = "aprs-is" transport = "aprs-is"
aprs_connection = ( aprs_connection = (
"APRS-IS Server: <a href='http://status.aprs2.net' >" "APRS-IS Server: <a href='http://status.aprs2.net' >"
@ -196,27 +192,22 @@ class WebChatFlask(flask_classful.FlaskView):
) )
else: else:
# We might be connected to a KISS socket? # We might be connected to a KISS socket?
if client.KISSClient.is_enabled(self.config): if client.KISSClient.is_enabled():
transport = client.KISSClient.transport(self.config) transport = client.KISSClient.transport()
if transport == client.TRANSPORT_TCPKISS: if transport == client.TRANSPORT_TCPKISS:
aprs_connection = ( aprs_connection = (
"TCPKISS://{}:{}".format( "TCPKISS://{}:{}".format(
self.config["kiss"]["tcp"]["host"], CONF.kiss_tcp.host,
self.config["kiss"]["tcp"]["port"], CONF.kiss_tcp.port,
) )
) )
elif transport == client.TRANSPORT_SERIALKISS: elif transport == client.TRANSPORT_SERIALKISS:
# for pep8 violation # for pep8 violation
kiss_default = aprsd_config.DEFAULT_DATE_FORMAT["kiss"]
default_baudrate = kiss_default["serial"]["baudrate"]
aprs_connection = ( aprs_connection = (
"SerialKISS://{}@{} baud".format( "SerialKISS://{}@{} baud".format(
self.config["kiss"]["serial"]["device"], CONF.kiss_serial.device,
self.config["kiss"]["serial"].get( CONF.kiss_serial.baudrate,
"baudrate", ),
default_baudrate,
),
)
) )
return transport, aprs_connection return transport, aprs_connection
@ -250,7 +241,7 @@ class WebChatFlask(flask_classful.FlaskView):
html_template, html_template,
initial_stats=stats, initial_stats=stats,
aprs_connection=aprs_connection, aprs_connection=aprs_connection,
callsign=self.config["aprsd"]["callsign"], callsign=CONF.callsign,
version=aprsd.__version__, version=aprsd.__version__,
) )
@ -287,14 +278,12 @@ class WebChatFlask(flask_classful.FlaskView):
class SendMessageNamespace(Namespace): class SendMessageNamespace(Namespace):
"""Class to handle the socketio interactions.""" """Class to handle the socketio interactions."""
_config = None
got_ack = False got_ack = False
reply_sent = False reply_sent = False
msg = None msg = None
request = None request = None
def __init__(self, namespace=None, config=None): def __init__(self, namespace=None, config=None):
self._config = config
super().__init__(namespace) super().__init__(namespace)
def on_connect(self): def on_connect(self):
@ -312,7 +301,7 @@ class SendMessageNamespace(Namespace):
global socketio global socketio
LOG.debug(f"WS: on_send {data}") LOG.debug(f"WS: on_send {data}")
self.request = data self.request = data
data["from"] = self._config["aprs"]["login"] data["from"] = CONF.callsign
pkt = packets.MessagePacket( pkt = packets.MessagePacket(
from_call=data["from"], from_call=data["from"],
to_call=data["to"].upper(), to_call=data["to"].upper(),
@ -338,7 +327,7 @@ class SendMessageNamespace(Namespace):
tx.send( tx.send(
packets.GPSPacket( packets.GPSPacket(
from_call=self._config["aprs"]["login"], from_call=CONF.callsign,
to_call="APDW16", to_call="APDW16",
latitude=lat, latitude=lat,
longitude=long, longitude=long,
@ -354,25 +343,16 @@ class SendMessageNamespace(Namespace):
LOG.debug(f"WS json {data}") LOG.debug(f"WS json {data}")
def setup_logging(config, flask_app, loglevel, quiet): def setup_logging(flask_app, loglevel, quiet):
flask_log = logging.getLogger("werkzeug") flask_log = logging.getLogger("werkzeug")
flask_app.logger.removeHandler(default_handler) flask_app.logger.removeHandler(default_handler)
flask_log.removeHandler(default_handler) flask_log.removeHandler(default_handler)
log_level = aprsd_config.LOG_LEVELS[loglevel] log_level = conf.log.LOG_LEVELS[loglevel]
flask_log.setLevel(log_level) flask_log.setLevel(log_level)
date_format = config["aprsd"].get( date_format = CONF.logging.date_format
"dateformat",
aprsd_config.DEFAULT_DATE_FORMAT,
)
if not config["aprsd"]["web"].get("logging_enabled", False): if CONF.logging.rich_logging and not quiet:
# disable web logging
flask_log.disabled = True
flask_app.logger.disabled = True
# return
if config["aprsd"].get("rich_logging", False) and not quiet:
log_format = "%(message)s" log_format = "%(message)s"
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
rh = aprsd_logging.APRSDRichHandler( rh = aprsd_logging.APRSDRichHandler(
@ -382,13 +362,10 @@ def setup_logging(config, flask_app, loglevel, quiet):
rh.setFormatter(log_formatter) rh.setFormatter(log_formatter)
flask_log.addHandler(rh) flask_log.addHandler(rh)
log_file = config["aprsd"].get("logfile", None) log_file = CONF.logging.logfile
if log_file: if log_file:
log_format = config["aprsd"].get( log_format = CONF.loging.logformat
"logformat",
aprsd_config.DEFAULT_LOG_FORMAT,
)
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
fh = RotatingFileHandler( fh = RotatingFileHandler(
log_file, maxBytes=(10248576 * 5), log_file, maxBytes=(10248576 * 5),
@ -399,7 +376,7 @@ def setup_logging(config, flask_app, loglevel, quiet):
@trace.trace @trace.trace
def init_flask(config, loglevel, quiet): def init_flask(loglevel, quiet):
global socketio global socketio
flask_app = flask.Flask( flask_app = flask.Flask(
@ -408,9 +385,9 @@ def init_flask(config, loglevel, quiet):
static_folder="web/chat/static", static_folder="web/chat/static",
template_folder="web/chat/templates", template_folder="web/chat/templates",
) )
setup_logging(config, flask_app, loglevel, quiet) setup_logging(flask_app, loglevel, quiet)
server = WebChatFlask() server = WebChatFlask()
server.set_config(config) server.set_config()
flask_app.route("/", methods=["GET"])(server.index) flask_app.route("/", methods=["GET"])(server.index)
flask_app.route("/stats", methods=["GET"])(server.stats) flask_app.route("/stats", methods=["GET"])(server.stats)
# flask_app.route("/send-message", methods=["GET"])(server.send_message) # flask_app.route("/send-message", methods=["GET"])(server.send_message)
@ -427,7 +404,7 @@ def init_flask(config, loglevel, quiet):
socketio.on_namespace( socketio.on_namespace(
SendMessageNamespace( SendMessageNamespace(
"/sendmsg", config=config, "/sendmsg",
), ),
) )
return socketio, flask_app return socketio, flask_app
@ -457,17 +434,12 @@ def init_flask(config, loglevel, quiet):
@cli_helper.process_standard_options @cli_helper.process_standard_options
def webchat(ctx, flush, port): def webchat(ctx, flush, port):
"""Web based HAM Radio chat program!""" """Web based HAM Radio chat program!"""
ctx.obj["config_file"]
loglevel = ctx.obj["loglevel"] loglevel = ctx.obj["loglevel"]
quiet = ctx.obj["quiet"] quiet = ctx.obj["quiet"]
config = ctx.obj["config"]
signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGTERM, signal_handler)
if not quiet:
click.echo("Load config")
level, msg = utils._check_version() level, msg = utils._check_version()
if level: if level:
LOG.warning(msg) LOG.warning(msg)
@ -475,19 +447,11 @@ def webchat(ctx, flush, port):
LOG.info(msg) LOG.info(msg)
LOG.info(f"APRSD Started version: {aprsd.__version__}") LOG.info(f"APRSD Started version: {aprsd.__version__}")
flat_config = utils.flatten_dict(config) CONF.log_opt_values(LOG, logging.DEBUG)
LOG.info("Using CONFIG values:")
for x in flat_config:
if "password" in x or "aprsd.web.users.admin" in x:
LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX")
else:
LOG.info(f"{x} = {flat_config[x]}")
stats.APRSDStats(config)
# Initialize the client factory and create # Initialize the client factory and create
# The correct client object ready for use # The correct client object ready for use
client.ClientFactory.setup(config) client.ClientFactory.setup()
# Make sure we have 1 client transport enabled # Make sure we have 1 client transport enabled
if not client.factory.is_client_enabled(): if not client.factory.is_client_enabled():
LOG.error("No Clients are enabled in config.") LOG.error("No Clients are enabled in config.")
@ -497,32 +461,30 @@ def webchat(ctx, flush, port):
LOG.error("APRS client is not properly configured in config file.") LOG.error("APRS client is not properly configured in config file.")
sys.exit(-1) sys.exit(-1)
packets.PacketList(config=config) packets.PacketList()
packets.PacketTrack(config=config) packets.PacketTrack()
packets.WatchList(config=config) packets.WatchList()
packets.SeenList(config=config) packets.SeenList()
(socketio, app) = init_flask(config, loglevel, quiet) (socketio, app) = init_flask(loglevel, quiet)
rx_thread = rx.APRSDPluginRXThread( rx_thread = rx.APRSDPluginRXThread(
config=config,
packet_queue=threads.packet_queue, packet_queue=threads.packet_queue,
) )
rx_thread.start() rx_thread.start()
process_thread = WebChatProcessPacketThread( process_thread = WebChatProcessPacketThread(
config=config,
packet_queue=threads.packet_queue, packet_queue=threads.packet_queue,
socketio=socketio, socketio=socketio,
) )
process_thread.start() process_thread.start()
keepalive = threads.KeepAliveThread(config=config) keepalive = threads.KeepAliveThread()
LOG.info("Start KeepAliveThread") LOG.info("Start KeepAliveThread")
keepalive.start() keepalive.start()
LOG.info("Start socketio.run()") LOG.info("Start socketio.run()")
socketio.run( socketio.run(
app, app,
ssl_context="adhoc", ssl_context="adhoc",
host=config["aprsd"]["web"]["host"], host=CONF.admin.web_ip,
port=port, port=port,
) )

56
aprsd/conf/__init__.py Normal file
View File

@ -0,0 +1,56 @@
from oslo_config import cfg
from aprsd.conf import client, common, log, plugin_common, plugin_email
CONF = cfg.CONF
log.register_opts(CONF)
common.register_opts(CONF)
client.register_opts(CONF)
# plugins
plugin_common.register_opts(CONF)
plugin_email.register_opts(CONF)
def set_lib_defaults():
"""Update default value for configuration options from other namespace.
Example, oslo lib config options. This is needed for
config generator tool to pick these default value changes.
https://docs.openstack.org/oslo.config/latest/cli/
generator.html#modifying-defaults-from-other-namespaces
"""
# Update default value of oslo_log default_log_levels and
# logging_context_format_string config option.
set_log_defaults()
def set_log_defaults():
# logging.set_defaults(default_log_levels=logging.get_default_log_levels())
pass
def conf_to_dict():
"""Convert the CONF options to a single level dictionary."""
entries = {}
def _sanitize(opt, value):
"""Obfuscate values of options declared secret."""
return value if not opt.secret else "*" * 4
for opt_name in sorted(CONF._opts):
opt = CONF._get_opt_info(opt_name)["opt"]
val = str(_sanitize(opt, getattr(CONF, opt_name)))
entries[str(opt)] = val
for group_name in list(CONF._groups):
group_attr = CONF.GroupAttr(CONF, CONF._get_group(group_name))
for opt_name in sorted(CONF._groups[group_name]._opts):
opt = CONF._get_opt_info(opt_name, group_name)["opt"]
val = str(_sanitize(opt, getattr(group_attr, opt_name)))
gname_opt_name = f"{group_name}.{opt_name}"
entries[gname_opt_name] = val
return entries

102
aprsd/conf/client.py Normal file
View File

@ -0,0 +1,102 @@
"""
The options for logging setup
"""
from oslo_config import cfg
DEFAULT_LOGIN = "NOCALL"
aprs_group = cfg.OptGroup(
name="aprs_network",
title="APRS-IS Network settings",
)
kiss_serial_group = cfg.OptGroup(
name="kiss_serial",
title="KISS Serial device connection",
)
kiss_tcp_group = cfg.OptGroup(
name="kiss_tcp",
title="KISS TCP/IP Device connection",
)
aprs_opts = [
cfg.BoolOpt(
"enabled",
default=True,
help="Set enabled to False if there is no internet connectivity."
"This is useful for a direwolf KISS aprs connection only.",
),
cfg.StrOpt(
"login",
default=DEFAULT_LOGIN,
help="APRS Username",
),
cfg.StrOpt(
"password",
secret=True,
help="APRS Password "
"Get the passcode for your callsign here: "
"https://apps.magicbug.co.uk/passcode",
),
cfg.HostnameOpt(
"host",
default="noam.aprs2.net",
help="The APRS-IS hostname",
),
cfg.PortOpt(
"port",
default=14580,
help="APRS-IS port",
),
]
kiss_serial_opts = [
cfg.BoolOpt(
"enabled",
default=False,
help="Enable Serial KISS interface connection.",
),
cfg.StrOpt(
"device",
help="Serial Device file to use. /dev/ttyS0",
),
cfg.IntOpt(
"baudrate",
default=9600,
help="The Serial device baud rate for communication",
),
]
kiss_tcp_opts = [
cfg.BoolOpt(
"enabled",
default=False,
help="Enable Serial KISS interface connection.",
),
cfg.HostnameOpt(
"host",
help="The KISS TCP Host to connect to.",
),
cfg.PortOpt(
"port",
default=8001,
help="The KISS TCP/IP network port",
),
]
def register_opts(config):
config.register_group(aprs_group)
config.register_opts(aprs_opts, group=aprs_group)
config.register_group(kiss_serial_group)
config.register_group(kiss_tcp_group)
config.register_opts(kiss_serial_opts, group=kiss_serial_group)
config.register_opts(kiss_tcp_opts, group=kiss_tcp_group)
def list_opts():
return {
aprs_group.name: aprs_opts,
kiss_serial_group.name: kiss_serial_opts,
kiss_tcp_group.name: kiss_tcp_opts,
}

165
aprsd/conf/common.py Normal file
View File

@ -0,0 +1,165 @@
from oslo_config import cfg
APRSD_DEFAULT_MAGIC_WORD = "CHANGEME!!!"
admin_group = cfg.OptGroup(
name="admin",
title="Admin web interface settings",
)
watch_list_group = cfg.OptGroup(
name="watch_list",
title="Watch List settings",
)
rpc_group = cfg.OptGroup(
name="rpc_settings",
title="RPC Settings for admin <--> web",
)
aprsd_opts = [
cfg.StrOpt(
"callsign",
required=True,
help="Callsign to use for messages sent by APRSD",
),
cfg.BoolOpt(
"enable_save",
default=True,
help="Enable saving of watch list, packet tracker between restarts.",
),
cfg.StrOpt(
"save_location",
default="~/.config/aprsd",
help="Save location for packet tracking files.",
),
cfg.BoolOpt(
"trace_enabled",
default=False,
help="Enable code tracing",
),
cfg.StrOpt(
"units",
default="imperial",
help="Units for display, imperial or metric",
),
]
watch_list_opts = [
cfg.BoolOpt(
"enabled",
default=False,
help="Enable the watch list feature. Still have to enable "
"the correct plugin. Built-in plugin to use is "
"aprsd.plugins.notify.NotifyPlugin",
),
cfg.ListOpt(
"callsigns",
help="Callsigns to watch for messsages",
),
cfg.StrOpt(
"alert_callsign",
help="The Ham Callsign to send messages to for watch list alerts.",
),
cfg.IntOpt(
"packet_keep_count",
default=10,
help="The number of packets to store.",
),
cfg.IntOpt(
"alert_time_seconds",
default=3600,
help="Time to wait before alert is sent on new message for "
"users in callsigns.",
),
]
admin_opts = [
cfg.BoolOpt(
"web_enabled",
default=False,
help="Enable the Admin Web Interface",
),
cfg.IPOpt(
"web_ip",
default="0.0.0.0",
help="The ip address to listen on",
),
cfg.PortOpt(
"web_port",
default=8001,
help="The port to listen on",
),
cfg.StrOpt(
"user",
default="admin",
help="The admin user for the admin web interface",
),
cfg.StrOpt(
"password",
secret=True,
help="Admin interface password",
),
]
rpc_opts = [
cfg.BoolOpt(
"enabled",
default=True,
help="Enable RPC calls",
),
cfg.StrOpt(
"ip",
default="localhost",
help="The ip address to listen on",
),
cfg.PortOpt(
"port",
default=18861,
help="The port to listen on",
),
cfg.StrOpt(
"magic_word",
default=APRSD_DEFAULT_MAGIC_WORD,
help="Magic word to authenticate requests between client/server",
),
]
enabled_plugins_opts = [
cfg.ListOpt(
"enabled_plugins",
default=[
"aprsd.plugins.email.EmailPlugin",
"aprsd.plugins.fortune.FortunePlugin",
"aprsd.plugins.location.LocationPlugin",
"aprsd.plugins.ping.PingPlugin",
"aprsd.plugins.query.QueryPlugin",
"aprsd.plugins.time.TimePlugin",
"aprsd.plugins.weather.OWMWeatherPlugin",
"aprsd.plugins.version.VersionPlugin",
],
help="Comma separated list of enabled plugins for APRSD."
"To enable installed external plugins add them here."
"The full python path to the class name must be used",
),
]
def register_opts(config):
config.register_opts(aprsd_opts)
config.register_opts(enabled_plugins_opts)
config.register_group(admin_group)
config.register_opts(admin_opts, group=admin_group)
config.register_group(watch_list_group)
config.register_opts(watch_list_opts, group=watch_list_group)
config.register_group(rpc_group)
config.register_opts(rpc_opts, group=rpc_group)
def list_opts():
return {
"DEFAULT": (aprsd_opts + enabled_plugins_opts),
admin_group.name: admin_opts,
watch_list_group.name: watch_list_opts,
rpc_group.name: rpc_opts,
}

61
aprsd/conf/log.py Normal file
View File

@ -0,0 +1,61 @@
"""
The options for logging setup
"""
import logging
from oslo_config import cfg
LOG_LEVELS = {
"CRITICAL": logging.CRITICAL,
"ERROR": logging.ERROR,
"WARNING": logging.WARNING,
"INFO": logging.INFO,
"DEBUG": logging.DEBUG,
}
DEFAULT_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p"
DEFAULT_LOG_FORMAT = (
"[%(asctime)s] [%(threadName)-20.20s] [%(levelname)-5.5s]"
" %(message)s - [%(pathname)s:%(lineno)d]"
)
logging_group = cfg.OptGroup(
name="logging",
title="Logging options",
)
logging_opts = [
cfg.StrOpt(
"date_format",
default=DEFAULT_DATE_FORMAT,
help="Date format for log entries",
),
cfg.BoolOpt(
"rich_logging",
default=True,
help="Enable Rich logging",
),
cfg.StrOpt(
"logfile",
default=None,
help="File to log to",
),
cfg.StrOpt(
"logformat",
default=DEFAULT_LOG_FORMAT,
help="Log file format, unless rich_logging enabled.",
),
]
def register_opts(config):
config.register_group(logging_group)
config.register_opts(logging_opts, group=logging_group)
def list_opts():
return {
logging_group.name: (
logging_opts
),
}

80
aprsd/conf/opts.py Normal file
View File

@ -0,0 +1,80 @@
# Copyright 2015 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
This is the single point of entry to generate the sample configuration
file for Nova. It collects all the necessary info from the other modules
in this package. It is assumed that:
* every other module in this package has a 'list_opts' function which
return a dict where
* the keys are strings which are the group names
* the value of each key is a list of config options for that group
* the nova.conf package doesn't have further packages with config options
* this module is only used in the context of sample file generation
"""
import collections
import importlib
import os
import pkgutil
LIST_OPTS_FUNC_NAME = "list_opts"
def _tupleize(dct):
"""Take the dict of options and convert to the 2-tuple format."""
return [(key, val) for key, val in dct.items()]
def list_opts():
opts = collections.defaultdict(list)
module_names = _list_module_names()
imported_modules = _import_modules(module_names)
_append_config_options(imported_modules, opts)
return _tupleize(opts)
def _list_module_names():
module_names = []
package_path = os.path.dirname(os.path.abspath(__file__))
for _, modname, ispkg in pkgutil.iter_modules(path=[package_path]):
if modname == "opts" or ispkg:
continue
else:
module_names.append(modname)
return module_names
def _import_modules(module_names):
imported_modules = []
for modname in module_names:
mod = importlib.import_module("aprsd.conf." + modname)
if not hasattr(mod, LIST_OPTS_FUNC_NAME):
msg = "The module 'aprsd.conf.%s' should have a '%s' "\
"function which returns the config options." % \
(modname, LIST_OPTS_FUNC_NAME)
raise Exception(msg)
else:
imported_modules.append(mod)
return imported_modules
def _append_config_options(imported_modules, config_options):
for mod in imported_modules:
configs = mod.list_opts()
for key, val in configs.items():
config_options[key].extend(val)

View File

@ -0,0 +1,83 @@
from oslo_config import cfg
aprsfi_group = cfg.OptGroup(
name="aprs_fi",
title="APRS.FI website settings",
)
query_group = cfg.OptGroup(
name="query_plugin",
title="Options for the Query Plugin",
)
avwx_group = cfg.OptGroup(
name="avwx_plugin",
title="Options for the AVWXWeatherPlugin",
)
owm_wx_group = cfg.OptGroup(
name="owm_weather_plugin",
title="Options for the OWMWeatherPlugin",
)
aprsfi_opts = [
cfg.StrOpt(
"apiKey",
help="Get the apiKey from your aprs.fi account here:"
"http://aprs.fi/account",
),
]
query_plugin_opts = [
cfg.StrOpt(
"callsign",
help="The Ham callsign to allow access to the query plugin from RF.",
),
]
owm_wx_opts = [
cfg.StrOpt(
"apiKey",
help="OWMWeatherPlugin api key to OpenWeatherMap's API."
"This plugin uses the openweathermap API to fetch"
"location and weather information."
"To use this plugin you need to get an openweathermap"
"account and apikey."
"https://home.openweathermap.org/api_keys",
),
]
avwx_opts = [
cfg.StrOpt(
"apiKey",
help="avwx-api is an opensource project that has"
"a hosted service here: https://avwx.rest/"
"You can launch your own avwx-api in a container"
"by cloning the githug repo here:"
"https://github.com/avwx-rest/AVWX-API",
),
cfg.StrOpt(
"base_url",
default="https://avwx.rest",
help="The base url for the avwx API. If you are hosting your own"
"Here is where you change the url to point to yours.",
),
]
def register_opts(config):
config.register_group(aprsfi_group)
config.register_opts(aprsfi_opts, group=aprsfi_group)
config.register_group(query_group)
config.register_opts(query_plugin_opts, group=query_group)
config.register_group(owm_wx_group)
config.register_opts(owm_wx_opts, group=owm_wx_group)
config.register_group(avwx_group)
config.register_opts(avwx_opts, group=avwx_group)
def list_opts():
return {
aprsfi_group.name: aprsfi_opts,
query_group.name: query_plugin_opts,
owm_wx_group.name: owm_wx_opts,
avwx_group.name: avwx_opts,
}

106
aprsd/conf/plugin_email.py Normal file
View File

@ -0,0 +1,106 @@
from oslo_config import cfg
email_group = cfg.OptGroup(
name="email_plugin",
title="Options for the APRSD Email plugin",
)
email_opts = [
cfg.StrOpt(
"callsign",
required=True,
help="(Required) Callsign to validate for doing email commands."
"Only this callsign can check email. This is also where the "
"email notifications for new emails will be sent.",
),
cfg.BoolOpt(
"enabled",
default=False,
help="Enable the Email plugin?",
),
cfg.BoolOpt(
"debug",
default=False,
help="Enable the Email plugin Debugging?",
),
]
email_imap_opts = [
cfg.StrOpt(
"imap_login",
help="Login username/email for IMAP server",
),
cfg.StrOpt(
"imap_password",
secret=True,
help="Login password for IMAP server",
),
cfg.HostnameOpt(
"imap_host",
help="Hostname/IP of the IMAP server",
),
cfg.PortOpt(
"imap_port",
default=993,
help="Port to use for IMAP server",
),
cfg.BoolOpt(
"imap_use_ssl",
default=True,
help="Use SSL for connection to IMAP Server",
),
]
email_smtp_opts = [
cfg.StrOpt(
"smtp_login",
help="Login username/email for SMTP server",
),
cfg.StrOpt(
"smtp_password",
secret=True,
help="Login password for SMTP server",
),
cfg.HostnameOpt(
"smtp_host",
help="Hostname/IP of the SMTP server",
),
cfg.PortOpt(
"smtp_port",
default=465,
help="Port to use for SMTP server",
),
cfg.BoolOpt(
"smtp_use_ssl",
default=True,
help="Use SSL for connection to SMTP Server",
),
]
email_shortcuts_opts = [
cfg.ListOpt(
"email_shortcuts",
help="List of email shortcuts for checking/sending email "
"For Exmaple: wb=walt@walt.com,cl=cl@cl.com\n"
"Means use 'wb' to send an email to walt@walt.com",
),
]
ALL_OPTS = (
email_opts
+ email_imap_opts
+ email_smtp_opts
+ email_shortcuts_opts
)
def register_opts(config):
config.register_group(email_group)
config.register_opts(ALL_OPTS, group=email_group)
def list_opts():
return {
email_group.name: ALL_OPTS,
}

View File

@ -1,404 +0,0 @@
import collections
import logging
import os
from pathlib import Path
import sys
import click
import yaml
from aprsd import exception, utils
home = str(Path.home())
DEFAULT_CONFIG_DIR = f"{home}/.config/aprsd/"
DEFAULT_SAVE_FILE = f"{home}/.config/aprsd/aprsd.p"
DEFAULT_CONFIG_FILE = f"{home}/.config/aprsd/aprsd.yml"
LOG_LEVELS = {
"CRITICAL": logging.CRITICAL,
"ERROR": logging.ERROR,
"WARNING": logging.WARNING,
"INFO": logging.INFO,
"DEBUG": logging.DEBUG,
}
DEFAULT_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p"
DEFAULT_LOG_FORMAT = (
"[%(asctime)s] [%(threadName)-20.20s] [%(levelname)-5.5s]"
" %(message)s - [%(pathname)s:%(lineno)d]"
)
QUEUE_DATE_FORMAT = "[%m/%d/%Y] [%I:%M:%S %p]"
QUEUE_LOG_FORMAT = (
"%(asctime)s [%(threadName)-20.20s] [%(levelname)-5.5s]"
" %(message)s - [%(pathname)s:%(lineno)d]"
)
CORE_MESSAGE_PLUGINS = [
"aprsd.plugins.email.EmailPlugin",
"aprsd.plugins.fortune.FortunePlugin",
"aprsd.plugins.location.LocationPlugin",
"aprsd.plugins.ping.PingPlugin",
"aprsd.plugins.query.QueryPlugin",
"aprsd.plugins.time.TimePlugin",
"aprsd.plugins.weather.USWeatherPlugin",
"aprsd.plugins.version.VersionPlugin",
]
CORE_NOTIFY_PLUGINS = [
"aprsd.plugins.notify.NotifySeenPlugin",
]
ALL_PLUGINS = []
for i in CORE_MESSAGE_PLUGINS:
ALL_PLUGINS.append(i)
for i in CORE_NOTIFY_PLUGINS:
ALL_PLUGINS.append(i)
# an example of what should be in the ~/.aprsd/config.yml
DEFAULT_CONFIG_DICT = {
"ham": {"callsign": "NOCALL"},
"aprs": {
"enabled": True,
# Only used as the login for aprsis.
"login": "CALLSIGN",
"password": "00000",
"host": "rotate.aprs2.net",
"port": 14580,
},
"kiss": {
"tcp": {
"enabled": False,
"host": "direwolf.ip.address",
"port": "8001",
},
"serial": {
"enabled": False,
"device": "/dev/ttyS0",
"baudrate": 9600,
},
},
"aprsd": {
# Callsign to use for all packets to/from aprsd instance
# regardless of the client (aprsis vs kiss)
"callsign": "NOCALL",
"logfile": "/tmp/aprsd.log",
"logformat": DEFAULT_LOG_FORMAT,
"dateformat": DEFAULT_DATE_FORMAT,
"save_location": DEFAULT_CONFIG_DIR,
"rich_logging": True,
"trace": False,
"enabled_plugins": ALL_PLUGINS,
"units": "imperial",
"watch_list": {
"enabled": False,
# Who gets the alert?
"alert_callsign": "NOCALL",
# 43200 is 12 hours
"alert_time_seconds": 43200,
# How many packets to save in a ring Buffer
# for a particular callsign
"packet_keep_count": 10,
"callsigns": [],
},
"web": {
"enabled": True,
"logging_enabled": True,
"host": "0.0.0.0",
"port": 8001,
"users": {
"admin": "password-here",
},
},
"email": {
"enabled": True,
"shortcuts": {
"aa": "5551239999@vtext.com",
"cl": "craiglamparter@somedomain.org",
"wb": "555309@vtext.com",
},
"smtp": {
"login": "SMTP_USERNAME",
"password": "SMTP_PASSWORD",
"host": "smtp.gmail.com",
"port": 465,
"use_ssl": False,
"debug": False,
},
"imap": {
"login": "IMAP_USERNAME",
"password": "IMAP_PASSWORD",
"host": "imap.gmail.com",
"port": 993,
"use_ssl": True,
"debug": False,
},
},
},
"services": {
"aprs.fi": {"apiKey": "APIKEYVALUE"},
"openweathermap": {"apiKey": "APIKEYVALUE"},
"opencagedata": {"apiKey": "APIKEYVALUE"},
"avwx": {"base_url": "http://host:port", "apiKey": "APIKEYVALUE"},
},
}
class Config(collections.UserDict):
def _get(self, d, keys, default=None):
"""
Example:
d = {'meta': {'status': 'OK', 'status_code': 200}}
_get(d, ['meta', 'status_code']) # => 200
_get(d, ['garbage', 'status_code']) # => None
_get(d, ['meta', 'garbage'], default='-') # => '-'
"""
if type(keys) is str and "." in keys:
keys = keys.split(".")
assert type(keys) is list
if d is None:
return default
if not keys:
return d
if type(d) is str:
return default
return self._get(d.get(keys[0]), keys[1:], default)
def get(self, path, default=None):
return self._get(self.data, path, default=default)
def exists(self, path):
"""See if a conf value exists."""
test = "-3.14TEST41.3-"
return self.get(path, default=test) != test
def check_option(self, path, default_fail=None):
"""Make sure the config option doesn't have default value."""
if not self.exists(path):
if type(path) is list:
path = ".".join(path)
raise exception.MissingConfigOptionException(path)
val = self.get(path)
if val == default_fail:
# We have to fail and bail if the user hasn't edited
# this config option.
raise exception.ConfigOptionBogusDefaultException(
path, default_fail,
)
def add_config_comments(raw_yaml):
end_idx = utils.end_substr(raw_yaml, "ham:")
if end_idx != -1:
# lets insert a comment
raw_yaml = utils.insert_str(
raw_yaml,
"\n # Callsign that owns this instance of APRSD.",
end_idx,
)
end_idx = utils.end_substr(raw_yaml, "aprsd:")
if end_idx != -1:
# lets insert a comment
raw_yaml = utils.insert_str(
raw_yaml,
"\n # Callsign to use for all APRSD Packets as the to/from."
"\n # regardless of client type (aprsis vs tcpkiss vs serial)",
end_idx,
)
end_idx = utils.end_substr(raw_yaml, "aprs:")
if end_idx != -1:
# lets insert a comment
raw_yaml = utils.insert_str(
raw_yaml,
"\n # Set enabled to False if there is no internet connectivity."
"\n # This is useful for a direwolf KISS aprs connection only. "
"\n"
"\n # Get the passcode for your callsign here: "
"\n # https://apps.magicbug.co.uk/passcode",
end_idx,
)
end_idx = utils.end_substr(raw_yaml, "aprs.fi:")
if end_idx != -1:
# lets insert a comment
raw_yaml = utils.insert_str(
raw_yaml,
"\n # Get the apiKey from your aprs.fi account here: "
"\n # http://aprs.fi/account",
end_idx,
)
end_idx = utils.end_substr(raw_yaml, "opencagedata:")
if end_idx != -1:
# lets insert a comment
raw_yaml = utils.insert_str(
raw_yaml,
"\n # (Optional for TimeOpenCageDataPlugin) "
"\n # Get the apiKey from your opencagedata account here: "
"\n # https://opencagedata.com/dashboard#api-keys",
end_idx,
)
end_idx = utils.end_substr(raw_yaml, "openweathermap:")
if end_idx != -1:
# lets insert a comment
raw_yaml = utils.insert_str(
raw_yaml,
"\n # (Optional for OWMWeatherPlugin) "
"\n # Get the apiKey from your "
"\n # openweathermap account here: "
"\n # https://home.openweathermap.org/api_keys",
end_idx,
)
end_idx = utils.end_substr(raw_yaml, "avwx:")
if end_idx != -1:
# lets insert a comment
raw_yaml = utils.insert_str(
raw_yaml,
"\n # (Optional for AVWXWeatherPlugin) "
"\n # Use hosted avwx-api here: https://avwx.rest "
"\n # or deploy your own from here: "
"\n # https://github.com/avwx-rest/avwx-api",
end_idx,
)
return raw_yaml
def dump_default_cfg():
return add_config_comments(
yaml.dump(
DEFAULT_CONFIG_DICT,
indent=4,
),
)
def create_default_config():
"""Create a default config file."""
# make sure the directory location exists
config_file_expanded = os.path.expanduser(DEFAULT_CONFIG_FILE)
config_dir = os.path.dirname(config_file_expanded)
if not os.path.exists(config_dir):
click.echo(f"Config dir '{config_dir}' doesn't exist, creating.")
utils.mkdir_p(config_dir)
with open(config_file_expanded, "w+") as cf:
cf.write(dump_default_cfg())
def get_config(config_file):
"""This tries to read the yaml config from <config_file>."""
config_file_expanded = os.path.expanduser(config_file)
if os.path.exists(config_file_expanded):
with open(config_file_expanded) as stream:
config = yaml.load(stream, Loader=yaml.FullLoader)
return Config(config)
else:
if config_file == DEFAULT_CONFIG_FILE:
click.echo(
f"{config_file_expanded} is missing, creating config file",
)
create_default_config()
msg = (
"Default config file created at {}. Please edit with your "
"settings.".format(config_file)
)
click.echo(msg)
else:
# The user provided a config file path different from the
# Default, so we won't try and create it, just bitch and bail.
msg = f"Custom config file '{config_file}' is missing."
click.echo(msg)
sys.exit(-1)
# This method tries to parse the config yaml file
# and consume the settings.
# If the required params don't exist,
# it will look in the environment
def parse_config(config_file):
config = get_config(config_file)
def fail(msg):
click.echo(msg)
sys.exit(-1)
def check_option(config, path, default_fail=None):
try:
config.check_option(path, default_fail=default_fail)
except Exception as ex:
fail(repr(ex))
else:
return config
# special check here to make sure user has edited the config file
# and changed the ham callsign
check_option(
config,
"ham.callsign",
default_fail=DEFAULT_CONFIG_DICT["ham"]["callsign"],
)
check_option(
config,
["aprsd"],
)
check_option(
config,
"aprsd.callsign",
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["callsign"],
)
# Ensure they change the admin password
if config.get("aprsd.web.enabled") is True:
check_option(
config,
["aprsd", "web", "users", "admin"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["web"]["users"]["admin"],
)
if config.get("aprsd.watch_list.enabled") is True:
check_option(
config,
["aprsd", "watch_list", "alert_callsign"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["watch_list"]["alert_callsign"],
)
if config.get("aprsd.email.enabled") is True:
# Check IMAP server settings
check_option(config, ["aprsd", "email", "imap", "host"])
check_option(config, ["aprsd", "email", "imap", "port"])
check_option(
config,
["aprsd", "email", "imap", "login"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["login"],
)
check_option(
config,
["aprsd", "email", "imap", "password"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["password"],
)
# Check SMTP server settings
check_option(config, ["aprsd", "email", "smtp", "host"])
check_option(config, ["aprsd", "email", "smtp", "port"])
check_option(
config,
["aprsd", "email", "smtp", "login"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["login"],
)
check_option(
config,
["aprsd", "email", "smtp", "password"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["password"],
)
return config

View File

@ -2,100 +2,42 @@ import datetime
import json import json
import logging import logging
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
import threading
import time import time
import aprslib
from aprslib.exceptions import LoginError
import flask import flask
from flask import request
from flask.logging import default_handler from flask.logging import default_handler
import flask_classful import flask_classful
from flask_httpauth import HTTPBasicAuth from flask_httpauth import HTTPBasicAuth
from flask_socketio import Namespace, SocketIO from flask_socketio import Namespace, SocketIO
from oslo_config import cfg
import rpyc
from werkzeug.security import check_password_hash, generate_password_hash from werkzeug.security import check_password_hash, generate_password_hash
import wrapt
import aprsd import aprsd
from aprsd import client from aprsd import cli_helper, client, conf, packets, plugin, threads
from aprsd import config as aprsd_config from aprsd.conf import common
from aprsd import packets, plugin, stats, threads, utils
from aprsd.clients import aprsis
from aprsd.logging import log
from aprsd.logging import rich as aprsd_logging from aprsd.logging import rich as aprsd_logging
from aprsd.threads import tx
CONF = cfg.CONF
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
auth = HTTPBasicAuth() auth = HTTPBasicAuth()
users = None users = None
app = None
class SentMessages: class AuthSocketStream(rpyc.SocketStream):
_instance = None """Used to authenitcate the RPC stream to remote."""
lock = threading.Lock()
msgs = {} @classmethod
def connect(cls, *args, authorizer=None, **kwargs):
stream_obj = super().connect(*args, **kwargs)
def __new__(cls, *args, **kwargs): if callable(authorizer):
"""This magic turns this into a singleton.""" authorizer(stream_obj.sock)
if cls._instance is None:
cls._instance = super().__new__(cls)
# Put any initialization here.
return cls._instance
@wrapt.synchronized(lock) return stream_obj
def add(self, packet):
self.msgs[packet.msgNo] = self._create(packet.msgNo)
self.msgs[packet.msgNo]["from"] = packet.from_call
self.msgs[packet.msgNo]["to"] = packet.to_call
self.msgs[packet.msgNo]["message"] = packet.message_text.rstrip("\n")
packet._build_raw()
self.msgs[packet.msgNo]["raw"] = packet.raw.rstrip("\n")
def _create(self, id):
return {
"id": id,
"ts": time.time(),
"ack": False,
"from": None,
"to": None,
"raw": None,
"message": None,
"status": None,
"last_update": None,
"reply": None,
}
@wrapt.synchronized(lock)
def __len__(self):
return len(self.msgs.keys())
@wrapt.synchronized(lock)
def get(self, id):
if id in self.msgs:
return self.msgs[id]
@wrapt.synchronized(lock)
def get_all(self):
return self.msgs
@wrapt.synchronized(lock)
def set_status(self, id, status):
self.msgs[id]["last_update"] = str(datetime.datetime.now())
self.msgs[id]["status"] = status
@wrapt.synchronized(lock)
def ack(self, id):
"""The message got an ack!"""
self.msgs[id]["last_update"] = str(datetime.datetime.now())
self.msgs[id]["ack"] = True
@wrapt.synchronized(lock)
def reply(self, id, packet):
"""We got a packet back from the sent message."""
self.msgs[id]["reply"] = packet
# HTTPBasicAuth doesn't work on a class method. # HTTPBasicAuth doesn't work on a class method.
@ -109,215 +51,168 @@ def verify_password(username, password):
return username return username
class SendMessageThread(threads.APRSDRXThread): class RPCClient:
"""Thread for sending a message from web.""" _instance = None
_rpc_client = None
aprsis_client = None def __new__(cls, *args, **kwargs):
request = None if cls._instance is None:
got_ack = False cls._instance = super().__new__(cls)
got_reply = False return cls._instance
def __init__(self, config, info, packet, namespace): def __init__(self):
self.config = config self._check_settings()
self.request = info self.get_rpc_client()
self.packet = packet
self.namespace = namespace
self.start_time = datetime.datetime.now()
msg = "({} -> {}) : {}".format(
info["from"],
info["to"],
info["message"],
)
super().__init__(f"WEB_SEND_MSG-{msg}")
def setup_connection(self): def _check_settings(self):
user = self.request["from"] if not CONF.rpc_settings.enabled:
password = self.request["password"] LOG.error("RPC is not enabled, no way to get stats!!")
host = self.config["aprs"].get("host", "rotate.aprs.net")
port = self.config["aprs"].get("port", 14580)
connected = False
backoff = 1
while not connected:
try:
LOG.info("Creating aprslib client")
aprs_client = aprsis.Aprsdis( if CONF.rpc_settings.magic_word == common.APRSD_DEFAULT_MAGIC_WORD:
user, LOG.warning("You are using the default RPC magic word!!!")
passwd=password, LOG.warning("edit aprsd.conf and change rpc_settings.magic_word")
host=host,
port=port,
)
# Force the logging to be the same
aprs_client.logger = LOG
aprs_client.connect()
connected = True
backoff = 1
except LoginError as e:
LOG.error(f"Failed to login to APRS-IS Server '{e}'")
connected = False
raise e
except Exception as e:
LOG.error(f"Unable to connect to APRS-IS server. '{e}' ")
time.sleep(backoff)
backoff = backoff * 2
continue
LOG.debug(f"Logging in to APRS-IS with user '{user}'")
return aprs_client
def run(self): def _rpyc_connect(
LOG.debug("Starting") self, host, port,
from_call = self.request["from"] service=rpyc.VoidService,
to_call = self.request["to"] config={}, ipv6=False,
message = self.request["message"] keepalive=False, authorizer=None,
LOG.info( ):
"From: '{}' To: '{}' Send '{}'".format(
from_call,
to_call,
message,
),
)
print(f"Connecting to RPC host {host}:{port}")
try: try:
self.aprs_client = self.setup_connection() s = AuthSocketStream.connect(
except LoginError as e: host, port, ipv6=ipv6, keepalive=keepalive,
f"Failed to setup Connection {e}" authorizer=authorizer,
tx.send(
self.packet,
direct=True,
aprs_client=self.aprs_client,
)
SentMessages().set_status(self.packet.msgNo, "Sent")
while not self.thread_stop:
can_loop = self.loop()
if not can_loop:
self.stop()
threads.APRSDThreadList().remove(self)
LOG.debug("Exiting")
def process_ack_packet(self, packet):
global socketio
ack_num = packet.msgNo
LOG.info(f"We got ack for our sent message {ack_num}")
packet.log("RXACK")
SentMessages().ack(self.packet.msgNo)
stats.APRSDStats().ack_rx_inc()
socketio.emit(
"ack", SentMessages().get(self.packet.msgNo),
namespace="/sendmsg",
)
if self.request["wait_reply"] == "0" or self.got_reply:
# We aren't waiting for a reply, so we can bail
self.stop()
self.thread_stop = self.aprs_client.thread_stop = True
def process_our_message_packet(self, packet):
global socketio
packets.PacketList().rx(packet)
stats.APRSDStats().msgs_rx_inc()
msg_number = packet.msgNo
SentMessages().reply(self.packet.msgNo, packet)
SentMessages().set_status(self.packet.msgNo, "Got Reply")
socketio.emit(
"reply", SentMessages().get(self.packet.msgNo),
namespace="/sendmsg",
)
tx.send(
packets.AckPacket(
from_call=self.request["from"],
to_call=packet.from_call,
msgNo=msg_number,
),
direct=True,
aprs_client=self.aprsis_client,
)
SentMessages().set_status(self.packet.msgNo, "Ack Sent")
# Now we can exit, since we are done.
self.got_reply = True
if self.got_ack:
self.stop()
self.thread_stop = self.aprs_client.thread_stop = True
def process_packet(self, *args, **kwargs):
packet = self._client.decode_packet(*args, **kwargs)
packet.log(header="RX Packet")
if isinstance(packet, packets.AckPacket):
self.process_ack_packet(packet)
else:
self.process_our_message_packet(packet)
def loop(self):
# we have a general time limit expecting results of
# around 120 seconds before we exit
now = datetime.datetime.now()
start_delta = str(now - self.start_time)
delta = utils.parse_delta_str(start_delta)
d = datetime.timedelta(**delta)
max_timeout = {"hours": 0.0, "minutes": 1, "seconds": 0}
max_delta = datetime.timedelta(**max_timeout)
if d > max_delta:
LOG.error("XXXXXX Haven't completed everything in 60 seconds. BAIL!")
return False
if self.got_ack and self.got_reply:
LOG.warning("We got everything already. BAIL")
return False
try:
# This will register a packet consumer with aprslib
# When new packets come in the consumer will process
# the packet
self.aprs_client.consumer(
self.process_packet, raw=False, blocking=False,
) )
except aprslib.exceptions.ConnectionDrop: return rpyc.utils.factory.connect_stream(s, service, config=config)
LOG.error("Connection dropped.") except ConnectionRefusedError:
return False LOG.error(f"Failed to connect to RPC host {host}")
return None
return True def get_rpc_client(self):
if not self._rpc_client:
magic = CONF.rpc_settings.magic_word
self._rpc_client = self._rpyc_connect(
CONF.rpc_settings.ip,
CONF.rpc_settings.port,
authorizer=lambda sock: sock.send(magic.encode()),
)
return self._rpc_client
def get_stats_dict(self):
cl = self.get_rpc_client()
result = {}
if not cl:
return result
try:
rpc_stats_dict = cl.root.get_stats()
result = json.loads(rpc_stats_dict)
except EOFError:
LOG.error("Lost connection to RPC Host")
self._rpc_client = None
return result
def get_packet_track(self):
cl = self.get_rpc_client()
result = None
if not cl:
return result
try:
result = cl.root.get_packet_track()
except EOFError:
LOG.error("Lost connection to RPC Host")
self._rpc_client = None
return result
def get_packet_list(self):
cl = self.get_rpc_client()
result = None
if not cl:
return result
try:
result = cl.root.get_packet_list()
except EOFError:
LOG.error("Lost connection to RPC Host")
self._rpc_client = None
return result
def get_watch_list(self):
cl = self.get_rpc_client()
result = None
if not cl:
return result
try:
result = cl.root.get_watch_list()
except EOFError:
LOG.error("Lost connection to RPC Host")
self._rpc_client = None
return result
def get_seen_list(self):
cl = self.get_rpc_client()
result = None
if not cl:
return result
try:
result = cl.root.get_seen_list()
except EOFError:
LOG.error("Lost connection to RPC Host")
self._rpc_client = None
return result
def get_log_entries(self):
cl = self.get_rpc_client()
result = None
if not cl:
return result
try:
result_str = cl.root.get_log_entries()
result = json.loads(result_str)
except EOFError:
LOG.error("Lost connection to RPC Host")
self._rpc_client = None
return result
class APRSDFlask(flask_classful.FlaskView): class APRSDFlask(flask_classful.FlaskView):
config = None
def set_config(self, config): def set_config(self):
global users global users
self.config = config
self.users = {} self.users = {}
for user in self.config["aprsd"]["web"]["users"]: user = CONF.admin.user
self.users[user] = generate_password_hash( self.users[user] = generate_password_hash(CONF.admin.password)
self.config["aprsd"]["web"]["users"][user],
)
users = self.users users = self.users
@auth.login_required @auth.login_required
def index(self): def index(self):
stats = self._stats() stats = self._stats()
print(stats)
LOG.debug( LOG.debug(
"watch list? {}".format( "watch list? {}".format(
self.config["aprsd"]["watch_list"], CONF.watch_list.callsigns,
), ),
) )
wl = packets.WatchList() wl = RPCClient().get_watch_list()
if wl.is_enabled(): if wl and wl.is_enabled():
watch_count = len(wl) watch_count = len(wl)
watch_age = wl.max_delta() watch_age = wl.max_delta()
else: else:
watch_count = 0 watch_count = 0
watch_age = 0 watch_age = 0
sl = packets.SeenList() sl = RPCClient().get_seen_list()
seen_count = len(sl) if sl:
seen_count = len(sl)
else:
seen_count = 0
pm = plugin.PluginManager() pm = plugin.PluginManager()
plugins = pm.get_plugins() plugins = pm.get_plugins()
plugin_count = len(plugins) plugin_count = len(plugins)
if self.config["aprs"].get("enabled", True): if CONF.aprs_network.enabled:
transport = "aprs-is" transport = "aprs-is"
aprs_connection = ( aprs_connection = (
"APRS-IS Server: <a href='http://status.aprs2.net' >" "APRS-IS Server: <a href='http://status.aprs2.net' >"
@ -325,33 +220,37 @@ class APRSDFlask(flask_classful.FlaskView):
) )
else: else:
# We might be connected to a KISS socket? # We might be connected to a KISS socket?
if client.KISSClient.kiss_enabled(self.config): if client.KISSClient.kiss_enabled():
transport = client.KISSClient.transport(self.config) transport = client.KISSClient.transport()
if transport == client.TRANSPORT_TCPKISS: if transport == client.TRANSPORT_TCPKISS:
aprs_connection = ( aprs_connection = (
"TCPKISS://{}:{}".format( "TCPKISS://{}:{}".format(
self.config["kiss"]["tcp"]["host"], CONF.kiss_tcp.host,
self.config["kiss"]["tcp"]["port"], CONF.kiss_tcp.port,
) )
) )
elif transport == client.TRANSPORT_SERIALKISS: elif transport == client.TRANSPORT_SERIALKISS:
aprs_connection = ( aprs_connection = (
"SerialKISS://{}@{} baud".format( "SerialKISS://{}@{} baud".format(
self.config["kiss"]["serial"]["device"], CONF.kiss_serial.device,
self.config["kiss"]["serial"]["baudrate"], CONF.kiss_serial.baudrate,
) )
) )
stats["transport"] = transport stats["transport"] = transport
stats["aprs_connection"] = aprs_connection stats["aprs_connection"] = aprs_connection
entries = conf.conf_to_dict()
return flask.render_template( return flask.render_template(
"index.html", "index.html",
initial_stats=stats, initial_stats=stats,
aprs_connection=aprs_connection, aprs_connection=aprs_connection,
callsign=self.config["aprs"]["login"], callsign=CONF.callsign,
version=aprsd.__version__, version=aprsd.__version__,
config_json=json.dumps(self.config.data), config_json=json.dumps(
entries, indent=4,
sort_keys=True, default=str,
),
watch_count=watch_count, watch_count=watch_count,
watch_age=watch_age, watch_age=watch_age,
seen_count=seen_count, seen_count=seen_count,
@ -368,32 +267,18 @@ class APRSDFlask(flask_classful.FlaskView):
return flask.render_template("messages.html", messages=json.dumps(msgs)) return flask.render_template("messages.html", messages=json.dumps(msgs))
@auth.login_required
def send_message_status(self):
LOG.debug(request)
msgs = SentMessages()
info = msgs.get_all()
return json.dumps(info)
@auth.login_required
def send_message(self):
LOG.debug(request)
if request.method == "GET":
return flask.render_template(
"send-message.html",
callsign=self.config["aprs"]["login"],
version=aprsd.__version__,
)
@auth.login_required @auth.login_required
def packets(self): def packets(self):
packet_list = packets.PacketList().get() packet_list = RPCClient().get_packet_list()
tmp_list = [] if packet_list:
for pkt in packet_list: packets = packet_list.get()
tmp_list.append(pkt.json) tmp_list = []
for pkt in packets:
tmp_list.append(pkt.json)
LOG.info(f"PACKETS {tmp_list}") return json.dumps(tmp_list)
return json.dumps(tmp_list) else:
return json.dumps([])
@auth.login_required @auth.login_required
def plugins(self): def plugins(self):
@ -410,39 +295,69 @@ class APRSDFlask(flask_classful.FlaskView):
return json.dumps({"messages": "saved"}) return json.dumps({"messages": "saved"})
def _stats(self): def _stats(self):
stats_obj = stats.APRSDStats() track = RPCClient().get_packet_track()
track = packets.PacketTrack()
now = datetime.datetime.now() now = datetime.datetime.now()
time_format = "%m-%d-%Y %H:%M:%S" time_format = "%m-%d-%Y %H:%M:%S"
stats_dict = stats_obj.stats() stats_dict = RPCClient().get_stats_dict()
if not stats_dict:
# Convert the watch_list entries to age stats_dict = {
wl = packets.WatchList() "aprsd": {},
new_list = {} "aprs-is": {"server": ""},
for call in wl.get_all(): "messages": {
# call_date = datetime.datetime.strptime( "sent": 0,
# str(wl.last_seen(call)), "received": 0,
# "%Y-%m-%d %H:%M:%S.%f", },
# ) "email": {
new_list[call] = { "sent": 0,
"last": wl.age(call), "received": 0,
"packets": wl.get(call)["packets"].get(), },
"seen_list": {
"sent": 0,
"received": 0,
},
} }
# Convert the watch_list entries to age
wl = RPCClient().get_watch_list()
new_list = {}
if wl:
for call in wl.get_all():
# call_date = datetime.datetime.strptime(
# str(wl.last_seen(call)),
# "%Y-%m-%d %H:%M:%S.%f",
# )
# We have to convert the RingBuffer to a real list
# so that json.dumps works.
# pkts = []
# for pkt in wl.get(call)["packets"].get():
# pkts.append(pkt)
new_list[call] = {
"last": wl.age(call),
# "packets": pkts
}
stats_dict["aprsd"]["watch_list"] = new_list stats_dict["aprsd"]["watch_list"] = new_list
packet_list = packets.PacketList() packet_list = RPCClient().get_packet_list()
rx = packet_list.total_rx() rx = tx = 0
tx = packet_list.total_tx() if packet_list:
rx = packet_list.total_rx()
tx = packet_list.total_tx()
stats_dict["packets"] = { stats_dict["packets"] = {
"sent": tx, "sent": tx,
"received": rx, "received": rx,
} }
if track:
size_tracker = len(track)
else:
size_tracker = 0
result = { result = {
"time": now.strftime(time_format), "time": now.strftime(time_format),
"size_tracker": len(track), "size_tracker": size_tracker,
"stats": stats_dict, "stats": stats_dict,
} }
@ -452,139 +367,58 @@ class APRSDFlask(flask_classful.FlaskView):
return json.dumps(self._stats()) return json.dumps(self._stats())
class SendMessageNamespace(Namespace): class LogUpdateThread(threads.APRSDThread):
_config = None
got_ack = False
reply_sent = False
packet = None
request = None
def __init__(self, namespace=None, config=None):
self._config = config
super().__init__(namespace)
def on_connect(self):
global socketio
LOG.debug("Web socket connected")
socketio.emit(
"connected", {"data": "/sendmsg Connected"},
namespace="/sendmsg",
)
def on_disconnect(self):
LOG.debug("WS Disconnected")
def on_send(self, data):
global socketio
LOG.debug(f"WS: on_send {data}")
self.request = data
self.packet = packets.MessagePacket(
from_call=data["from"],
to_call=data["to"],
message_text=data["message"],
)
msgs = SentMessages()
msgs.add(self.packet)
msgs.set_status(self.packet.msgNo, "Sending")
socketio.emit(
"sent", SentMessages().get(self.packet.msgNo),
namespace="/sendmsg",
)
socketio.start_background_task(
self._start, self._config, data,
self.packet, self,
)
LOG.warning("WS: on_send: exit")
def _start(self, config, data, packet, namespace):
msg_thread = SendMessageThread(self._config, data, packet, self)
msg_thread.start()
def handle_message(self, data):
LOG.debug(f"WS Data {data}")
def handle_json(self, data):
LOG.debug(f"WS json {data}")
class LogMonitorThread(threads.APRSDThread):
def __init__(self): def __init__(self):
super().__init__("LogMonitorThread") super().__init__("LogUpdate")
def loop(self): def loop(self):
global socketio global socketio
try:
record = log.logging_queue.get(block=True, timeout=5)
json_record = self.json_record(record)
socketio.emit(
"log_entry", json_record,
namespace="/logs",
)
except Exception:
# Just ignore thi
pass
if socketio:
log_entries = RPCClient().get_log_entries()
if log_entries:
for entry in log_entries:
socketio.emit(
"log_entry", entry,
namespace="/logs",
)
time.sleep(5)
return True return True
def json_record(self, record):
entry = {}
entry["filename"] = record.filename
entry["funcName"] = record.funcName
entry["levelname"] = record.levelname
entry["lineno"] = record.lineno
entry["module"] = record.module
entry["name"] = record.name
entry["pathname"] = record.pathname
entry["process"] = record.process
entry["processName"] = record.processName
if hasattr(record, "stack_info"):
entry["stack_info"] = record.stack_info
else:
entry["stack_info"] = None
entry["thread"] = record.thread
entry["threadName"] = record.threadName
entry["message"] = record.getMessage()
return entry
class LoggingNamespace(Namespace): class LoggingNamespace(Namespace):
log_thread = None
def on_connect(self): def on_connect(self):
global socketio global socketio
LOG.debug("Web socket connected")
socketio.emit( socketio.emit(
"connected", {"data": "/logs Connected"}, "connected", {"data": "/logs Connected"},
namespace="/logs", namespace="/logs",
) )
self.log_thread = LogMonitorThread() self.log_thread = LogUpdateThread()
self.log_thread.start() self.log_thread.start()
def on_disconnect(self): def on_disconnect(self):
LOG.debug("WS Disconnected") LOG.debug("LOG Disconnected")
self.log_thread.stop() if self.log_thread:
self.log_thread.stop()
def setup_logging(config, flask_app, loglevel, quiet): def setup_logging(flask_app, loglevel, quiet):
flask_log = logging.getLogger("werkzeug") flask_log = logging.getLogger("werkzeug")
flask_app.logger.removeHandler(default_handler) flask_app.logger.removeHandler(default_handler)
flask_log.removeHandler(default_handler) flask_log.removeHandler(default_handler)
log_level = aprsd_config.LOG_LEVELS[loglevel] log_level = conf.log.LOG_LEVELS[loglevel]
flask_log.setLevel(log_level) flask_log.setLevel(log_level)
date_format = config["aprsd"].get( date_format = CONF.logging.date_format
"dateformat", flask_log.disabled = True
aprsd_config.DEFAULT_DATE_FORMAT, flask_app.logger.disabled = True
)
if not config["aprsd"]["web"].get("logging_enabled", False): if CONF.logging.rich_logging:
# disable web logging
flask_log.disabled = True
flask_app.logger.disabled = True
return
if config["aprsd"].get("rich_logging", False) and not quiet:
log_format = "%(message)s" log_format = "%(message)s"
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
rh = aprsd_logging.APRSDRichHandler( rh = aprsd_logging.APRSDRichHandler(
@ -594,13 +428,10 @@ def setup_logging(config, flask_app, loglevel, quiet):
rh.setFormatter(log_formatter) rh.setFormatter(log_formatter)
flask_log.addHandler(rh) flask_log.addHandler(rh)
log_file = config["aprsd"].get("logfile", None) log_file = CONF.logging.logfile
if log_file: if log_file:
log_format = config["aprsd"].get( log_format = CONF.logging.logformat
"logformat",
aprsd_config.DEFAULT_LOG_FORMAT,
)
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
fh = RotatingFileHandler( fh = RotatingFileHandler(
log_file, maxBytes=(10248576 * 5), log_file, maxBytes=(10248576 * 5),
@ -610,7 +441,7 @@ def setup_logging(config, flask_app, loglevel, quiet):
flask_log.addHandler(fh) flask_log.addHandler(fh)
def init_flask(config, loglevel, quiet): def init_flask(loglevel, quiet):
global socketio global socketio
flask_app = flask.Flask( flask_app = flask.Flask(
@ -619,15 +450,13 @@ def init_flask(config, loglevel, quiet):
static_folder="web/admin/static", static_folder="web/admin/static",
template_folder="web/admin/templates", template_folder="web/admin/templates",
) )
setup_logging(config, flask_app, loglevel, quiet) setup_logging(flask_app, loglevel, quiet)
server = APRSDFlask() server = APRSDFlask()
server.set_config(config) server.set_config()
flask_app.route("/", methods=["GET"])(server.index) flask_app.route("/", methods=["GET"])(server.index)
flask_app.route("/stats", methods=["GET"])(server.stats) flask_app.route("/stats", methods=["GET"])(server.stats)
flask_app.route("/messages", methods=["GET"])(server.messages) flask_app.route("/messages", methods=["GET"])(server.messages)
flask_app.route("/packets", methods=["GET"])(server.packets) flask_app.route("/packets", methods=["GET"])(server.packets)
flask_app.route("/send-message", methods=["GET"])(server.send_message)
flask_app.route("/send-message-status", methods=["GET"])(server.send_message_status)
flask_app.route("/save", methods=["GET"])(server.save) flask_app.route("/save", methods=["GET"])(server.save)
flask_app.route("/plugins", methods=["GET"])(server.plugins) flask_app.route("/plugins", methods=["GET"])(server.plugins)
@ -637,7 +466,21 @@ def init_flask(config, loglevel, quiet):
) )
# import eventlet # import eventlet
# eventlet.monkey_patch() # eventlet.monkey_patch()
gunicorn_logger = logging.getLogger("gunicorn.error")
flask_app.logger.handlers = gunicorn_logger.handlers
flask_app.logger.setLevel(gunicorn_logger.level)
socketio.on_namespace(SendMessageNamespace("/sendmsg", config=config))
socketio.on_namespace(LoggingNamespace("/logs")) socketio.on_namespace(LoggingNamespace("/logs"))
return socketio, flask_app return socketio, flask_app
if __name__ == "aprsd.flask":
try:
default_config_file = cli_helper.DEFAULT_CONFIG_FILE
CONF(
[], project="aprsd", version=aprsd.__version__,
default_config_files=[default_config_file],
)
except cfg.ConfigFilesNotFoundError:
pass
sio, app = init_flask("DEBUG", False)

View File

@ -4,10 +4,13 @@ from logging.handlers import RotatingFileHandler
import queue import queue
import sys import sys
from aprsd import config as aprsd_config from oslo_config import cfg
from aprsd import conf
from aprsd.logging import rich as aprsd_logging from aprsd.logging import rich as aprsd_logging
CONF = cfg.CONF
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
logging_queue = queue.Queue() logging_queue = queue.Queue()
@ -15,13 +18,15 @@ logging_queue = queue.Queue()
# Setup the logging faciility # Setup the logging faciility
# 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(loglevel, quiet):
log_level = aprsd_config.LOG_LEVELS[loglevel] log_level = conf.log.LOG_LEVELS[loglevel]
LOG.setLevel(log_level) LOG.setLevel(log_level)
date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT) date_format = CONF.logging.date_format
rh = None
fh = None
rich_logging = False rich_logging = False
if config["aprsd"].get("rich_logging", False) and not quiet: if CONF.logging.get("rich_logging", False) and not quiet:
log_format = "%(message)s" log_format = "%(message)s"
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
rh = aprsd_logging.APRSDRichHandler( rh = aprsd_logging.APRSDRichHandler(
@ -32,8 +37,8 @@ def setup_logging(config, loglevel, quiet):
LOG.addHandler(rh) LOG.addHandler(rh)
rich_logging = True rich_logging = True
log_file = config["aprsd"].get("logfile", None) log_file = CONF.logging.logfile
log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT) log_format = CONF.logging.logformat
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
if log_file: if log_file:
@ -42,16 +47,19 @@ def setup_logging(config, loglevel, quiet):
LOG.addHandler(fh) LOG.addHandler(fh)
imap_logger = None imap_logger = None
if config.get("aprsd.email.enabled", default=False) and config.get("aprsd.email.imap.debug", default=False): if CONF.email_plugin.enabled and CONF.email_plugin.debug:
imap_logger = logging.getLogger("imapclient.imaplib") imap_logger = logging.getLogger("imapclient.imaplib")
imap_logger.setLevel(log_level) imap_logger.setLevel(log_level)
imap_logger.addHandler(fh) if rh:
imap_logger.addHandler(rh)
if fh:
imap_logger.addHandler(fh)
if config.get("aprsd.web.enabled", default=False): if CONF.admin.web_enabled:
qh = logging.handlers.QueueHandler(logging_queue) qh = logging.handlers.QueueHandler(logging_queue)
q_log_formatter = logging.Formatter( q_log_formatter = logging.Formatter(
fmt=aprsd_config.QUEUE_LOG_FORMAT, fmt=CONF.logging.logformat,
datefmt=aprsd_config.QUEUE_DATE_FORMAT, datefmt=CONF.logging.date_format,
) )
qh.setFormatter(q_log_formatter) qh.setFormatter(q_log_formatter)
LOG.addHandler(qh) LOG.addHandler(qh)
@ -65,10 +73,10 @@ def setup_logging(config, loglevel, quiet):
def setup_logging_no_config(loglevel, quiet): def setup_logging_no_config(loglevel, quiet):
log_level = aprsd_config.LOG_LEVELS[loglevel] log_level = conf.log.LOG_LEVELS[loglevel]
LOG.setLevel(log_level) LOG.setLevel(log_level)
log_format = aprsd_config.DEFAULT_LOG_FORMAT log_format = CONF.logging.logformat
date_format = aprsd_config.DEFAULT_DATE_FORMAT date_format = CONF.logging.date_format
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
fh = NullHandler() fh = NullHandler()

View File

@ -84,6 +84,9 @@ class Packet(metaclass=abc.ABCMeta):
else: else:
return default return default
def update_timestamp(self):
self.timestamp = _int_timestamp()
def prepare(self): def prepare(self):
"""Do stuff here that is needed prior to sending over the air.""" """Do stuff here that is needed prior to sending over the air."""
# now build the raw message for sending # now build the raw message for sending

View File

@ -1,12 +1,14 @@
import logging import logging
import threading import threading
from oslo_config import cfg
import wrapt import wrapt
from aprsd import stats, utils from aprsd import stats, utils
from aprsd.packets import seen_list from aprsd.packets import seen_list
CONF = cfg.CONF
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -15,7 +17,6 @@ class PacketList:
_instance = None _instance = None
lock = threading.Lock() lock = threading.Lock()
config = None
packet_list: utils.RingBuffer = utils.RingBuffer(1000) packet_list: utils.RingBuffer = utils.RingBuffer(1000)
@ -25,17 +26,8 @@ class PacketList:
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
if cls._instance is None: if cls._instance is None:
cls._instance = super().__new__(cls) cls._instance = super().__new__(cls)
if "config" in kwargs:
cls._instance.config = kwargs["config"]
return cls._instance return cls._instance
def __init__(self, config=None):
if config:
self.config = config
def _is_initialized(self):
return self.config is not None
@wrapt.synchronized(lock) @wrapt.synchronized(lock)
def __iter__(self): def __iter__(self):
return iter(self.packet_list) return iter(self.packet_list)

View File

@ -2,11 +2,13 @@ import datetime
import logging import logging
import threading import threading
from oslo_config import cfg
import wrapt import wrapt
from aprsd.utils import objectstore from aprsd.utils import objectstore
CONF = cfg.CONF
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -16,21 +18,14 @@ class SeenList(objectstore.ObjectStoreMixin):
_instance = None _instance = None
lock = threading.Lock() lock = threading.Lock()
data: dict = {} data: dict = {}
config = None
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
if cls._instance is None: if cls._instance is None:
cls._instance = super().__new__(cls) cls._instance = super().__new__(cls)
if "config" in kwargs: cls._instance._init_store()
if "config" in kwargs:
cls._instance.config = kwargs["config"]
cls._instance._init_store()
cls._instance.data = {} cls._instance.data = {}
return cls._instance return cls._instance
def is_initialized(self):
return self.config is not None
@wrapt.synchronized(lock) @wrapt.synchronized(lock)
def update_seen(self, packet): def update_seen(self, packet):
callsign = None callsign = None

View File

@ -1,12 +1,16 @@
import datetime import datetime
import threading import threading
from oslo_config import cfg
import wrapt import wrapt
from aprsd.threads import tx from aprsd.threads import tx
from aprsd.utils import objectstore from aprsd.utils import objectstore
CONF = cfg.CONF
class PacketTrack(objectstore.ObjectStoreMixin): class PacketTrack(objectstore.ObjectStoreMixin):
"""Class to keep track of outstanding text messages. """Class to keep track of outstanding text messages.
@ -23,7 +27,6 @@ class PacketTrack(objectstore.ObjectStoreMixin):
_instance = None _instance = None
_start_time = None _start_time = None
lock = threading.Lock() lock = threading.Lock()
config = None
data: dict = {} data: dict = {}
total_tracked: int = 0 total_tracked: int = 0
@ -32,14 +35,9 @@ class PacketTrack(objectstore.ObjectStoreMixin):
if cls._instance is None: if cls._instance is None:
cls._instance = super().__new__(cls) cls._instance = super().__new__(cls)
cls._instance._start_time = datetime.datetime.now() cls._instance._start_time = datetime.datetime.now()
if "config" in kwargs:
cls._instance.config = kwargs["config"]
cls._instance._init_store() cls._instance._init_store()
return cls._instance return cls._instance
def is_initialized(self):
return self.config is not None
@wrapt.synchronized(lock) @wrapt.synchronized(lock)
def __getitem__(self, name): def __getitem__(self, name):
return self.data[name] return self.data[name]

View File

@ -2,12 +2,14 @@ import datetime
import logging import logging
import threading import threading
from oslo_config import cfg
import wrapt import wrapt
from aprsd import utils from aprsd import utils
from aprsd.utils import objectstore from aprsd.utils import objectstore
CONF = cfg.CONF
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -17,24 +19,19 @@ class WatchList(objectstore.ObjectStoreMixin):
_instance = None _instance = None
lock = threading.Lock() lock = threading.Lock()
data = {} data = {}
config = None
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
if cls._instance is None: if cls._instance is None:
cls._instance = super().__new__(cls) cls._instance = super().__new__(cls)
if "config" in kwargs: cls._instance._init_store()
cls._instance.config = kwargs["config"]
cls._instance._init_store()
cls._instance.data = {} cls._instance.data = {}
return cls._instance return cls._instance
def __init__(self, config=None): def __init__(self, config=None):
if config: ring_size = CONF.watch_list.packet_keep_count
self.config = config
ring_size = config["aprsd"]["watch_list"].get("packet_keep_count", 10) if CONF.watch_list.callsigns:
for callsign in CONF.watch_list.callsigns:
for callsign in config["aprsd"]["watch_list"].get("callsigns", []):
call = callsign.replace("*", "") call = callsign.replace("*", "")
# FIXME(waboring) - we should fetch the last time we saw # FIXME(waboring) - we should fetch the last time we saw
# a beacon from a callsign or some other mechanism to find # a beacon from a callsign or some other mechanism to find
@ -47,14 +44,8 @@ class WatchList(objectstore.ObjectStoreMixin):
), ),
} }
def is_initialized(self):
return self.config is not None
def is_enabled(self): def is_enabled(self):
if self.config and "watch_list" in self.config["aprsd"]: return CONF.watch_list.enabled
return self.config["aprsd"]["watch_list"].get("enabled", False)
else:
return False
def callsign_in_watchlist(self, callsign): def callsign_in_watchlist(self, callsign):
return callsign in self.data return callsign in self.data
@ -78,9 +69,8 @@ class WatchList(objectstore.ObjectStoreMixin):
return str(now - self.last_seen(callsign)) return str(now - self.last_seen(callsign))
def max_delta(self, seconds=None): def max_delta(self, seconds=None):
watch_list_conf = self.config["aprsd"]["watch_list"]
if not seconds: if not seconds:
seconds = watch_list_conf["alert_time_seconds"] seconds = CONF.watch_list.alert_time_seconds
max_timeout = {"seconds": seconds} max_timeout = {"seconds": seconds}
return datetime.timedelta(**max_timeout) return datetime.timedelta(**max_timeout)

View File

@ -7,6 +7,7 @@ import re
import textwrap import textwrap
import threading import threading
from oslo_config import cfg
import pluggy import pluggy
import aprsd import aprsd
@ -15,6 +16,7 @@ from aprsd.packets import watch_list
# setup the global logger # setup the global logger
CONF = cfg.CONF
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
CORE_MESSAGE_PLUGINS = [ CORE_MESSAGE_PLUGINS = [
@ -57,8 +59,7 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
# Set this in setup() # Set this in setup()
enabled = False enabled = False
def __init__(self, config): def __init__(self):
self.config = config
self.message_counter = 0 self.message_counter = 0
self.setup() self.setup()
self.threads = self.create_threads() or [] self.threads = self.create_threads() or []
@ -140,15 +141,10 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
def setup(self): def setup(self):
# if we have a watch list enabled, we need to add filtering # if we have a watch list enabled, we need to add filtering
# to enable seeing packets from the watch list. # to enable seeing packets from the watch list.
if "watch_list" in self.config["aprsd"] and self.config["aprsd"][ if CONF.watch_list.enabled:
"watch_list"
].get("enabled", False):
# watch list is enabled # watch list is enabled
self.enabled = True self.enabled = True
watch_list = self.config["aprsd"]["watch_list"].get( watch_list = CONF.watch_list.callsigns
"callsigns",
[],
)
# 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.factory.create().client aprs_client = client.factory.create().client
@ -211,36 +207,40 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
@hookimpl @hookimpl
def filter(self, packet: packets.core.MessagePacket): def filter(self, packet: packets.core.MessagePacket):
if not self.enabled:
result = f"{self.__class__.__name__} isn't enabled"
LOG.warning(result)
return result
if not isinstance(packet, packets.core.MessagePacket):
LOG.warning(f"Got a {packet.__class__.__name__} ignoring")
return packets.NULL_MESSAGE
result = None result = None
message = packet.get("message_text", None) message = packet.message_text
msg_format = packet.get("format", None) tocall = packet.to_call
tocall = packet.get("addresse", None)
# Only process messages destined for us # Only process messages destined for us
# and is an APRS message format and has a message. # and is an APRS message format and has a message.
if ( if (
tocall == self.config["aprs"]["login"] tocall == CONF.callsign
and msg_format == "message" and isinstance(packet, packets.core.MessagePacket)
and message and message
): ):
if re.search(self.command_regex, message): if re.search(self.command_regex, message):
self.rx_inc() self.rx_inc()
if self.enabled: try:
try: result = self.process(packet)
result = self.process(packet) except Exception as ex:
except Exception as ex: LOG.error(
LOG.error( "Plugin {} failed to process packet {}".format(
"Plugin {} failed to process packet {}".format( self.__class__, ex,
self.__class__, ex, ),
), )
) LOG.exception(ex)
LOG.exception(ex) if result:
if result: self.tx_inc()
self.tx_inc()
else:
result = f"{self.__class__.__name__} isn't enabled"
LOG.warning(result)
return result return result
@ -249,12 +249,11 @@ class APRSFIKEYMixin:
"""Mixin class to enable checking the existence of the aprs.fi apiKey.""" """Mixin class to enable checking the existence of the aprs.fi apiKey."""
def ensure_aprs_fi_key(self): def ensure_aprs_fi_key(self):
try: if not CONF.aprs_fi.apiKey:
self.config.check_option(["services", "aprs.fi", "apiKey"]) LOG.error("Config aprs_fi.apiKey is not set")
self.enabled = True
except Exception as ex:
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
self.enabled = False self.enabled = False
else:
self.enabled = True
class HelpPlugin(APRSDRegexCommandPluginBase): class HelpPlugin(APRSDRegexCommandPluginBase):
@ -371,12 +370,17 @@ class PluginManager:
:param kwargs: parameters to pass :param kwargs: parameters to pass
:return: :return:
""" """
module_name, class_name = module_class_string.rsplit(".", 1) module_name = None
class_name = None
try: try:
module_name, class_name = module_class_string.rsplit(".", 1)
module = importlib.import_module(module_name) module = importlib.import_module(module_name)
module = importlib.reload(module) module = importlib.reload(module)
except Exception as ex: except Exception as ex:
LOG.error(f"Failed to load Plugin '{module_name}' : '{ex}'") if not module_name:
LOG.error(f"Failed to load Plugin {module_class_string}")
else:
LOG.error(f"Failed to load Plugin '{module_name}' : '{ex}'")
return return
assert hasattr(module, class_name), "class {} is not in {}".format( assert hasattr(module, class_name), "class {} is not in {}".format(
@ -406,25 +410,31 @@ class PluginManager:
plugin_obj = self._create_class( plugin_obj = self._create_class(
plugin_name, plugin_name,
APRSDPluginBase, APRSDPluginBase,
config=self.config,
) )
if plugin_obj: if plugin_obj:
if isinstance(plugin_obj, APRSDWatchListPluginBase): if isinstance(plugin_obj, APRSDWatchListPluginBase):
LOG.info( if plugin_obj.enabled:
"Registering WatchList plugin '{}'({})".format( LOG.info(
plugin_name, "Registering WatchList plugin '{}'({})".format(
plugin_obj.version, plugin_name,
), plugin_obj.version,
) ),
self._watchlist_pm.register(plugin_obj) )
self._watchlist_pm.register(plugin_obj)
else:
LOG.warning(f"Plugin {plugin_obj.__class__.__name__} is disabled")
else: else:
LOG.info( if plugin_obj.enabled:
"Registering plugin '{}'({})".format( LOG.info(
plugin_name, "Registering plugin '{}'({}) -- {}".format(
plugin_obj.version, plugin_name,
), plugin_obj.version,
) plugin_obj.command_regex,
self._pluggy_pm.register(plugin_obj) ),
)
self._pluggy_pm.register(plugin_obj)
else:
LOG.warning(f"Plugin {plugin_obj.__class__.__name__} is disabled")
except Exception as ex: except Exception as ex:
LOG.error(f"Couldn't load plugin '{plugin_name}'") LOG.error(f"Couldn't load plugin '{plugin_name}'")
LOG.exception(ex) LOG.exception(ex)
@ -440,10 +450,10 @@ class PluginManager:
LOG.info("Loading APRSD Plugins") LOG.info("Loading APRSD Plugins")
self._init() self._init()
# Help plugin is always enabled. # Help plugin is always enabled.
_help = HelpPlugin(self.config) _help = HelpPlugin()
self._pluggy_pm.register(_help) self._pluggy_pm.register(_help)
enabled_plugins = self.config["aprsd"].get("enabled_plugins", None) enabled_plugins = CONF.enabled_plugins
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

@ -25,14 +25,20 @@ def get_aprs_fi(api_key, callsign):
def get_weather_gov_for_gps(lat, lon): def get_weather_gov_for_gps(lat, lon):
# FIXME(hemna) This is currently BROKEN
LOG.debug(f"Fetch station at {lat}, {lon}") LOG.debug(f"Fetch station at {lat}, {lon}")
headers = requests.utils.default_headers()
headers.update(
{"User-Agent": "(aprsd, waboring@hemna.com)"},
)
try: try:
url2 = ( url2 = (
"https://forecast.weather.gov/MapClick.php?lat=%s" # "https://forecast.weather.gov/MapClick.php?lat=%s"
"&lon=%s&FcstType=json" % (lat, lon) # "&lon=%s&FcstType=json" % (lat, lon)
f"https://api.weather.gov/points/{lat},{lon}"
) )
LOG.debug(f"Fetching weather '{url2}'") LOG.debug(f"Fetching weather '{url2}'")
response = requests.get(url2) response = requests.get(url2, headers=headers)
except Exception as e: except Exception as e:
LOG.error(e) LOG.error(e)
raise Exception("Failed to get weather") raise Exception("Failed to get weather")

View File

@ -9,13 +9,16 @@ import threading
import time import time
import imapclient import imapclient
from oslo_config import cfg
from aprsd import packets, plugin, stats, threads from aprsd import packets, plugin, stats, threads
from aprsd.threads import tx from aprsd.threads import tx
from aprsd.utils import trace from aprsd.utils import trace
CONF = cfg.CONF
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
shortcuts_dict = None
class EmailInfo: class EmailInfo:
@ -71,18 +74,18 @@ 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."""
if CONF.email_plugin.enabled:
email_enabled = self.config["aprsd"]["email"].get("enabled", False)
if email_enabled:
self.enabled = True self.enabled = True
shortcuts = _build_shortcuts_dict()
LOG.info(f"Email shortcuts {shortcuts}")
else: else:
LOG.info("Email services not enabled.") LOG.info("Email services not enabled.")
self.enabled = False
def create_threads(self): def create_threads(self):
if self.enabled: if self.enabled:
return APRSDEmailThread( return APRSDEmailThread()
config=self.config,
)
@trace.trace @trace.trace
def process(self, packet: packets.MessagePacket): def process(self, packet: packets.MessagePacket):
@ -97,18 +100,18 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
ack = packet.get("msgNo", "0") ack = packet.get("msgNo", "0")
reply = None reply = None
if not self.config["aprsd"]["email"].get("enabled", False): if not CONF.email_plugin.enabled:
LOG.debug("Email is not enabled in config file ignoring.") LOG.debug("Email is not enabled in config file ignoring.")
return "Email not enabled." return "Email not enabled."
searchstring = "^" + self.config["ham"]["callsign"] + ".*" searchstring = "^" + CONF.email_plugin.callsign + ".*"
# only I can do email # only I can do email
if re.search(searchstring, fromcall): if re.search(searchstring, fromcall):
# digits only, first one is number of emails to resend # digits only, first one is number of emails to resend
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(self.config, r.group(1), fromcall) resend_email(r.group(1), fromcall)
reply = packets.NULL_MESSAGE reply = packets.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):
@ -118,7 +121,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(self.config, to_addr) email_address = get_email_from_shortcut(to_addr)
if not email_address: if not email_address:
reply = "Bad email address" reply = "Bad email address"
return reply return reply
@ -128,7 +131,7 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
content = ( content = (
"Click for my location: http://aprs.fi/{}" "" "Click for my location: http://aprs.fi/{}" ""
).format( ).format(
self.config["ham"]["callsign"], CONF.email_plugin.callsign,
) )
too_soon = 0 too_soon = 0
now = time.time() now = time.time()
@ -141,7 +144,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 = send_email(self.config, to_addr, content) send_result = send_email(to_addr, content)
reply = packets.NULL_MESSAGE reply = packets.NULL_MESSAGE
if send_result != 0: if send_result != 0:
reply = f"-{to_addr} failed" reply = f"-{to_addr} failed"
@ -169,9 +172,9 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
return reply return reply
def _imap_connect(config): def _imap_connect():
imap_port = config["aprsd"]["email"]["imap"].get("port", 143) imap_port = CONF.email_plugin.imap_port
use_ssl = config["aprsd"]["email"]["imap"].get("use_ssl", False) use_ssl = CONF.email_plugin.imap_use_ssl
# 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 '{}'".
@ -179,7 +182,7 @@ def _imap_connect(config):
try: try:
server = imapclient.IMAPClient( server = imapclient.IMAPClient(
config["aprsd"]["email"]["imap"]["host"], CONF.email_plugin.imap_host,
port=imap_port, port=imap_port,
use_uid=True, use_uid=True,
ssl=use_ssl, ssl=use_ssl,
@ -191,8 +194,8 @@ def _imap_connect(config):
try: try:
server.login( server.login(
config["aprsd"]["email"]["imap"]["login"], CONF.email_plugin.imap_login,
config["aprsd"]["email"]["imap"]["password"], CONF.email_plugin.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))
@ -208,15 +211,15 @@ def _imap_connect(config):
return server return server
def _smtp_connect(config): def _smtp_connect():
host = config["aprsd"]["email"]["smtp"]["host"] host = CONF.email_plugin.smtp_host
smtp_port = config["aprsd"]["email"]["smtp"]["port"] smtp_port = CONF.email_plugin.smtp_port
use_ssl = config["aprsd"]["email"]["smtp"].get("use_ssl", False) use_ssl = CONF.email_plugin.smtp_use_ssl
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"], CONF.email_plugin.smtp_login,
), ),
) )
@ -239,15 +242,15 @@ def _smtp_connect(config):
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 = CONF.email_plugin.debug
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"], CONF.email_plugin.smtp_login,
config["aprsd"]["email"]["smtp"]["password"], CONF.email_plugin.smtp_password,
) )
except Exception: except Exception:
LOG.error("Couldn't connect to SMTP Server") LOG.error("Couldn't connect to SMTP Server")
@ -257,22 +260,39 @@ def _smtp_connect(config):
return server return server
def get_email_from_shortcut(config, addr): def _build_shortcuts_dict():
if config["aprsd"]["email"].get("shortcuts", False): global shortcuts_dict
return config["aprsd"]["email"]["shortcuts"].get(addr, addr) if not shortcuts_dict:
if CONF.email_plugin.email_shortcuts:
shortcuts_dict = {}
tmp = CONF.email_plugin.email_shortcuts
for combo in tmp:
entry = combo.split("=")
shortcuts_dict[entry[0]] = entry[1]
else:
shortcuts_dict = {}
return shortcuts_dict
def get_email_from_shortcut(addr):
if CONF.email_plugin.email_shortcuts:
shortcuts = _build_shortcuts_dict()
LOG.info(f"Shortcut lookup {addr} returns {shortcuts.get(addr, addr)}")
return shortcuts.get(addr, addr)
else: else:
return addr return addr
def validate_email_config(config, disable_validation=False): def validate_email_config(disable_validation=False):
"""function to simply ensure we can connect to email services. """function to simply ensure we can connect to email services.
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(config) imap_server = _imap_connect()
LOG.info("Checking SMTP configuration") LOG.info("Checking SMTP configuration")
smtp_server = _smtp_connect(config) smtp_server = _smtp_connect()
if imap_server and smtp_server: if imap_server and smtp_server:
return True return True
@ -376,16 +396,16 @@ def parse_email(msgid, data, server):
@trace.trace @trace.trace
def send_email(config, to_addr, content): def send_email(to_addr, content):
shortcuts = config["aprsd"]["email"]["shortcuts"] shortcuts = _build_shortcuts_dict()
email_address = get_email_from_shortcut(config, to_addr) 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(f"To : {to_addr}") LOG.info(f"To : {to_addr}")
to_addr = email_address to_addr = email_address
LOG.info(f" ({to_addr})") LOG.info(f" ({to_addr})")
subject = config["ham"]["callsign"] subject = CONF.email_plugin.callsign
# content = content + "\n\n(NOTE: reply with one line)" # content = content + "\n\n(NOTE: reply with one line)"
LOG.info(f"Subject : {subject}") LOG.info(f"Subject : {subject}")
LOG.info(f"Body : {content}") LOG.info(f"Body : {content}")
@ -395,13 +415,13 @@ def send_email(config, to_addr, content):
msg = MIMEText(content) msg = MIMEText(content)
msg["Subject"] = subject msg["Subject"] = subject
msg["From"] = config["aprsd"]["email"]["smtp"]["login"] msg["From"] = CONF.email_plugin.smtp_login
msg["To"] = to_addr msg["To"] = to_addr
server = _smtp_connect(config) server = _smtp_connect()
if server: if server:
try: try:
server.sendmail( server.sendmail(
config["aprsd"]["email"]["smtp"]["login"], CONF.email_plugin.smtp_login,
[to_addr], [to_addr],
msg.as_string(), msg.as_string(),
) )
@ -415,19 +435,19 @@ def send_email(config, to_addr, content):
@trace.trace @trace.trace
def resend_email(config, count, fromcall): def resend_email(count, fromcall):
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 = _build_shortcuts_dict()
# 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(config) server = _imap_connect()
except Exception: except Exception:
LOG.exception("Failed to Connect to IMAP. Cannot resend email ") LOG.exception("Failed to Connect to IMAP. Cannot resend email ")
return return
@ -467,7 +487,7 @@ def resend_email(config, count, fromcall):
reply = "-" + from_addr + " * " + body.decode(errors="ignore") reply = "-" + from_addr + " * " + body.decode(errors="ignore")
tx.send( tx.send(
packets.MessagePacket( packets.MessagePacket(
from_call=config["aprsd"]["callsign"], from_call=CONF.callsign,
to_call=fromcall, to_call=fromcall,
message_text=reply, message_text=reply,
), ),
@ -490,7 +510,7 @@ def resend_email(config, count, fromcall):
) )
tx.send( tx.send(
packets.MessagePacket( packets.MessagePacket(
from_call=config["aprsd"]["callsign"], from_call=CONF.callsign,
to_call=fromcall, to_call=fromcall,
message_text=reply, message_text=reply,
), ),
@ -504,9 +524,8 @@ def resend_email(config, count, fromcall):
class APRSDEmailThread(threads.APRSDThread): class APRSDEmailThread(threads.APRSDThread):
def __init__(self, config): def __init__(self):
super().__init__("EmailThread") super().__init__("EmailThread")
self.config = config
self.past = datetime.datetime.now() self.past = datetime.datetime.now()
def loop(self): def loop(self):
@ -527,7 +546,7 @@ class APRSDEmailThread(threads.APRSDThread):
f"check_email_delay is {EmailInfo().delay} seconds ", f"check_email_delay is {EmailInfo().delay} seconds ",
) )
shortcuts = self.config["aprsd"]["email"]["shortcuts"] shortcuts = _build_shortcuts_dict()
# 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()}
@ -538,7 +557,7 @@ class APRSDEmailThread(threads.APRSDThread):
today = f"{day}-{month}-{year}" today = f"{day}-{month}-{year}"
try: try:
server = _imap_connect(self.config) server = _imap_connect()
except Exception: except Exception:
LOG.exception("IMAP Failed to connect") LOG.exception("IMAP Failed to connect")
return True return True
@ -611,8 +630,8 @@ class APRSDEmailThread(threads.APRSDThread):
# config ham.callsign # config ham.callsign
tx.send( tx.send(
packets.MessagePacket( packets.MessagePacket(
from_call=self.config["aprsd"]["callsign"], from_call=CONF.callsign,
to_call=self.config["ham"]["callsign"], to_call=CONF.email_plugin.callsign,
message_text=reply, message_text=reply,
), ),
) )

View File

@ -2,10 +2,13 @@ import logging
import re import re
import time import time
from oslo_config import cfg
from aprsd import packets, plugin, plugin_utils from aprsd import packets, plugin, plugin_utils
from aprsd.utils import trace from aprsd.utils import trace
CONF = cfg.CONF
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -24,9 +27,8 @@ class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
LOG.info("Location Plugin") LOG.info("Location Plugin")
fromcall = packet.from_call fromcall = packet.from_call
message = packet.get("message_text", None) message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0")
api_key = self.config["services"]["aprs.fi"]["apiKey"] api_key = CONF.aprs_fi.apiKey
# optional second argument is a callsign to search # optional second argument is a callsign to search
a = re.search(r"^.*\s+(.*)", message) a = re.search(r"^.*\s+(.*)", message)

View File

@ -1,8 +1,11 @@
import logging import logging
from oslo_config import cfg
from aprsd import packets, plugin from aprsd import packets, plugin
CONF = cfg.CONF
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -20,7 +23,7 @@ class NotifySeenPlugin(plugin.APRSDWatchListPluginBase):
def process(self, packet: packets.MessagePacket): def process(self, packet: packets.MessagePacket):
LOG.info("NotifySeenPlugin") LOG.info("NotifySeenPlugin")
notify_callsign = self.config["aprsd"]["watch_list"]["alert_callsign"] notify_callsign = CONF.watch_list.alert_callsign
fromcall = packet.from_call fromcall = packet.from_call
wl = packets.WatchList() wl = packets.WatchList()
@ -38,7 +41,7 @@ class NotifySeenPlugin(plugin.APRSDWatchListPluginBase):
packet_type = packet.__class__.__name__ packet_type = packet.__class__.__name__
# we shouldn't notify the alert user that they are online. # we shouldn't notify the alert user that they are online.
pkt = packets.MessagePacket( pkt = packets.MessagePacket(
from_call=self.config["aprsd"]["callsign"], from_call=CONF.callsign,
to_call=notify_callsign, to_call=notify_callsign,
message_text=( message_text=(
f"{fromcall} was just seen by type:'{packet_type}'" f"{fromcall} was just seen by type:'{packet_type}'"

View File

@ -2,11 +2,14 @@ import datetime
import logging import logging
import re import re
from oslo_config import cfg
from aprsd import packets, plugin from aprsd import packets, plugin
from aprsd.packets import tracker from aprsd.packets import tracker
from aprsd.utils import trace from aprsd.utils import trace
CONF = cfg.CONF
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -17,13 +20,19 @@ class QueryPlugin(plugin.APRSDRegexCommandPluginBase):
command_name = "query" command_name = "query"
short_description = "APRSD Owner command to query messages in the MsgTrack" short_description = "APRSD Owner command to query messages in the MsgTrack"
def setup(self):
"""Do any plugin setup here."""
if not CONF.query_plugin.callsign:
LOG.error("Config query_plugin.callsign not set. Disabling plugin")
self.enabled = False
self.enabled = True
@trace.trace @trace.trace
def process(self, packet: packets.MessagePacket): def process(self, packet: packets.MessagePacket):
LOG.info("Query COMMAND") LOG.info("Query COMMAND")
fromcall = packet.from_call fromcall = packet.from_call
message = packet.get("message_text", None) message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0")
pkt_tracker = tracker.PacketTrack() pkt_tracker = tracker.PacketTrack()
now = datetime.datetime.now() now = datetime.datetime.now()
@ -32,7 +41,7 @@ class QueryPlugin(plugin.APRSDRegexCommandPluginBase):
now.strftime("%H:%M:%S"), now.strftime("%H:%M:%S"),
) )
searchstring = "^" + self.config["ham"]["callsign"] + ".*" searchstring = "^" + CONF.query_plugin.callsign + ".*"
# only I can do admin commands # only I can do admin commands
if re.search(searchstring, fromcall): if re.search(searchstring, fromcall):

View File

@ -2,12 +2,14 @@ import logging
import re import re
import time import time
from oslo_config import cfg
import pytz import pytz
from aprsd import packets, plugin, plugin_utils from aprsd import packets, plugin, plugin_utils
from aprsd.utils import fuzzy, trace from aprsd.utils import fuzzy, trace
CONF = cfg.CONF
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -74,7 +76,7 @@ class TimeOWMPlugin(TimePlugin, plugin.APRSFIKEYMixin):
# if no second argument, search for calling station # if no second argument, search for calling station
searchcall = fromcall searchcall = fromcall
api_key = self.config["services"]["aprs.fi"]["apiKey"] api_key = CONF.aprs_fi.apiKey
try: try:
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall) aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
except Exception as ex: except Exception as ex:

View File

@ -2,7 +2,6 @@ import logging
import aprsd import aprsd
from aprsd import plugin, stats from aprsd import plugin, stats
from aprsd.utils import trace
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -19,7 +18,6 @@ class VersionPlugin(plugin.APRSDRegexCommandPluginBase):
# five mins {int:int} # five mins {int:int}
email_sent_dict = {} email_sent_dict = {}
@trace.trace
def process(self, packet): def process(self, packet):
LOG.info("Version COMMAND") LOG.info("Version COMMAND")
# fromcall = packet.get("from") # fromcall = packet.get("from")
@ -27,6 +25,7 @@ class VersionPlugin(plugin.APRSDRegexCommandPluginBase):
# ack = packet.get("msgNo", "0") # ack = packet.get("msgNo", "0")
stats_obj = stats.APRSDStats() stats_obj = stats.APRSDStats()
s = stats_obj.stats() s = stats_obj.stats()
print(s)
return "APRSD ver:{} uptime:{}".format( return "APRSD ver:{} uptime:{}".format(
aprsd.__version__, aprsd.__version__,
s["aprsd"]["uptime"], s["aprsd"]["uptime"],

View File

@ -2,12 +2,14 @@ import json
import logging import logging
import re import re
from oslo_config import cfg
import requests import requests
from aprsd import plugin, plugin_utils from aprsd import plugin, plugin_utils
from aprsd.utils import trace from aprsd.utils import trace
CONF = cfg.CONF
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -34,10 +36,10 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin)
@trace.trace @trace.trace
def process(self, packet): def process(self, packet):
LOG.info("Weather Plugin") LOG.info("Weather Plugin")
fromcall = packet.get("from") fromcall = packet.from_call
# message = packet.get("message_text", None) # message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0") # ack = packet.get("msgNo", "0")
api_key = self.config["services"]["aprs.fi"]["apiKey"] api_key = CONF.aprs_fi.apiKey
try: try:
aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall) aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall)
except Exception as ex: except Exception as ex:
@ -58,6 +60,8 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin)
LOG.error(f"Couldn't fetch forecast.weather.gov '{ex}'") LOG.error(f"Couldn't fetch forecast.weather.gov '{ex}'")
return "Unable to get weather" return "Unable to get weather"
LOG.info(f"WX data {wx_data}")
reply = ( reply = (
"%sF(%sF/%sF) %s. %s, %s." "%sF(%sF/%sF) %s. %s, %s."
% ( % (
@ -97,6 +101,7 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
@trace.trace @trace.trace
def process(self, packet): def process(self, packet):
print("FISTY")
fromcall = packet.get("from") fromcall = packet.get("from")
message = packet.get("message_text", None) message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0") # ack = packet.get("msgNo", "0")
@ -119,13 +124,7 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
# if no second argument, search for calling station # if no second argument, search for calling station
fromcall = fromcall fromcall = fromcall
try: api_key = CONF.aprs_fi.apiKey
self.config.exists(["services", "aprs.fi", "apiKey"])
except Exception as ex:
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
return "No aprs.fi apikey found"
api_key = self.config["services"]["aprs.fi"]["apiKey"]
try: try:
aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall) aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall)
@ -187,6 +186,13 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
command_name = "OpenWeatherMap" command_name = "OpenWeatherMap"
short_description = "OpenWeatherMap weather of GPS Beacon location" short_description = "OpenWeatherMap weather of GPS Beacon location"
def setup(self):
if not CONF.owm_weather_plugin.apiKey:
LOG.error("Config.owm_weather_plugin.apiKey is not set. Disabling")
self.enabled = False
else:
self.enabled = True
def help(self): def help(self):
_help = [ _help = [
"openweathermap: Send {} to get weather " "openweathermap: Send {} to get weather "
@ -209,13 +215,8 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
else: else:
searchcall = fromcall searchcall = fromcall
try: api_key = CONF.aprs_fi.apiKey
self.config.exists(["services", "aprs.fi", "apiKey"])
except Exception as ex:
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
return "No aprs.fi apikey found"
api_key = self.config["services"]["aprs.fi"]["apiKey"]
try: try:
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall) aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
except Exception as ex: except Exception as ex:
@ -230,21 +231,8 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
lat = aprs_data["entries"][0]["lat"] lat = aprs_data["entries"][0]["lat"]
lon = aprs_data["entries"][0]["lng"] lon = aprs_data["entries"][0]["lng"]
try: units = CONF.units
self.config.exists(["services", "openweathermap", "apiKey"]) api_key = CONF.owm_weather_plugin.apiKey
except Exception as ex:
LOG.error(f"Failed to find config openweathermap:apiKey {ex}")
return "No openweathermap apiKey found"
try:
self.config.exists(["aprsd", "units"])
except Exception:
LOG.debug("Couldn't find untis in aprsd:services:units")
units = "metric"
else:
units = self.config["aprsd"]["units"]
api_key = self.config["services"]["openweathermap"]["apiKey"]
try: try:
wx_data = plugin_utils.fetch_openweathermap( wx_data = plugin_utils.fetch_openweathermap(
api_key, api_key,
@ -317,6 +305,16 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
command_name = "AVWXWeather" command_name = "AVWXWeather"
short_description = "AVWX weather of GPS Beacon location" short_description = "AVWX weather of GPS Beacon location"
def setup(self):
if not CONF.avwx_plugin.base_url:
LOG.error("Config avwx_plugin.base_url not specified. Disabling")
return False
elif not CONF.avwx_plugin.apiKey:
LOG.error("Config avwx_plugin.apiKey not specified. Disabling")
return False
else:
return True
def help(self): def help(self):
_help = [ _help = [
"avwxweather: Send {} to get weather " "avwxweather: Send {} to get weather "
@ -339,13 +337,7 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
else: else:
searchcall = fromcall searchcall = fromcall
try: api_key = CONF.aprs_fi.apiKey
self.config.exists(["services", "aprs.fi", "apiKey"])
except Exception as ex:
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
return "No aprs.fi apikey found"
api_key = self.config["services"]["aprs.fi"]["apiKey"]
try: try:
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall) aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
except Exception as ex: except Exception as ex:
@ -360,21 +352,8 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
lat = aprs_data["entries"][0]["lat"] lat = aprs_data["entries"][0]["lat"]
lon = aprs_data["entries"][0]["lng"] lon = aprs_data["entries"][0]["lng"]
try: api_key = CONF.avwx_plugin.apiKey
self.config.exists(["services", "avwx", "apiKey"]) base_url = CONF.avwx_plugin.base_url
except Exception as ex:
LOG.error(f"Failed to find config avwx:apiKey {ex}")
return "No avwx apiKey found"
try:
self.config.exists(self.config, ["services", "avwx", "base_url"])
except Exception as ex:
LOG.debug(f"Didn't find avwx:base_url {ex}")
base_url = "https://avwx.rest"
else:
base_url = self.config["services"]["avwx"]["base_url"]
api_key = self.config["services"]["avwx"]["apiKey"]
token = f"TOKEN {api_key}" token = f"TOKEN {api_key}"
headers = {"Authorization": token} headers = {"Authorization": token}
try: try:

90
aprsd/rpc_server.py Normal file
View File

@ -0,0 +1,90 @@
import json
import logging
from oslo_config import cfg
import rpyc
from rpyc.utils.authenticators import AuthenticationError
from rpyc.utils.server import ThreadPoolServer
from aprsd import conf # noqa: F401
from aprsd import packets, stats, threads
from aprsd.threads import log_monitor
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
def magic_word_authenticator(sock):
magic = sock.recv(len(CONF.rpc_settings.magic_word)).decode()
if magic != CONF.rpc_settings.magic_word:
raise AuthenticationError(f"wrong magic word {magic}")
return sock, None
class APRSDRPCThread(threads.APRSDThread):
def __init__(self):
super().__init__(name="RPCThread")
self.thread = ThreadPoolServer(
APRSDService,
port=CONF.rpc_settings.port,
protocol_config={"allow_public_attrs": True},
authenticator=magic_word_authenticator,
)
def stop(self):
if self.thread:
self.thread.close()
self.thread_stop = True
def loop(self):
# there is no loop as run is blocked
if self.thread and not self.thread_stop:
# This is a blocking call
self.thread.start()
@rpyc.service
class APRSDService(rpyc.Service):
def on_connect(self, conn):
# code that runs when a connection is created
# (to init the service, if needed)
LOG.info("Connected")
self._conn = conn
def on_disconnect(self, conn):
# code that runs after the connection has already closed
# (to finalize the service, if needed)
LOG.info("Disconnected")
self._conn = None
@rpyc.exposed
def get_stats(self):
stat = stats.APRSDStats()
stats_dict = stat.stats()
return json.dumps(stats_dict, indent=4, sort_keys=True, default=str)
@rpyc.exposed
def get_stats_obj(self):
return stats.APRSDStats()
@rpyc.exposed
def get_packet_list(self):
return packets.PacketList()
@rpyc.exposed
def get_packet_track(self):
return packets.PacketTrack()
@rpyc.exposed
def get_watch_list(self):
return packets.WatchList()
@rpyc.exposed
def get_seen_list(self):
return packets.SeenList()
@rpyc.exposed
def get_log_entries(self):
entries = log_monitor.LogEntries().get_all_and_purge()
return json.dumps(entries, default=str)

View File

@ -2,12 +2,14 @@ import datetime
import logging import logging
import threading import threading
from oslo_config import cfg
import wrapt import wrapt
import aprsd import aprsd
from aprsd import packets, plugin, utils from aprsd import packets, plugin, utils
CONF = cfg.CONF
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -15,7 +17,6 @@ class APRSDStats:
_instance = None _instance = None
lock = threading.Lock() lock = threading.Lock()
config = None
start_time = None start_time = None
_aprsis_server = None _aprsis_server = None
@ -62,15 +63,11 @@ class APRSDStats:
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
if cls._instance is None: if cls._instance is None:
cls._instance = super().__new__(cls) cls._instance = super().__new__(cls)
# any initializetion here # any init here
cls._instance.start_time = datetime.datetime.now() cls._instance.start_time = datetime.datetime.now()
cls._instance._aprsis_keepalive = datetime.datetime.now() cls._instance._aprsis_keepalive = datetime.datetime.now()
return cls._instance return cls._instance
def __init__(self, config=None):
if config:
self.config = config
@wrapt.synchronized(lock) @wrapt.synchronized(lock)
@property @property
def uptime(self): def uptime(self):
@ -191,7 +188,7 @@ class APRSDStats:
"aprsd": { "aprsd": {
"version": aprsd.__version__, "version": aprsd.__version__,
"uptime": utils.strfdelta(self.uptime), "uptime": utils.strfdelta(self.uptime),
"callsign": self.config["aprsd"]["callsign"], "callsign": CONF.callsign,
"memory_current": int(self.memory), "memory_current": int(self.memory),
"memory_current_str": utils.human_size(self.memory), "memory_current_str": utils.human_size(self.memory),
"memory_peak": int(self.memory_peak), "memory_peak": int(self.memory_peak),
@ -201,7 +198,7 @@ class APRSDStats:
}, },
"aprs-is": { "aprs-is": {
"server": str(self.aprsis_server), "server": str(self.aprsis_server),
"callsign": self.config["aprs"]["login"], "callsign": CONF.aprs_network.login,
"last_update": last_aprsis_keepalive, "last_update": last_aprsis_keepalive,
}, },
"packets": { "packets": {
@ -215,7 +212,7 @@ class APRSDStats:
"ack_sent": self._pkt_cnt["AckPacket"]["tx"], "ack_sent": self._pkt_cnt["AckPacket"]["tx"],
}, },
"email": { "email": {
"enabled": self.config["aprsd"]["email"]["enabled"], "enabled": CONF.email_plugin.enabled,
"sent": int(self._email_tx), "sent": int(self._email_tx),
"received": int(self._email_rx), "received": int(self._email_rx),
"thread_last_update": last_update, "thread_last_update": last_update,

View File

@ -3,10 +3,13 @@ import logging
import time import time
import tracemalloc import tracemalloc
from oslo_config import cfg
from aprsd import client, packets, stats, utils from aprsd import client, packets, stats, utils
from aprsd.threads import APRSDThread, APRSDThreadList from aprsd.threads import APRSDThread, APRSDThreadList
CONF = cfg.CONF
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -14,10 +17,9 @@ class KeepAliveThread(APRSDThread):
cntr = 0 cntr = 0
checker_time = datetime.datetime.now() checker_time = datetime.datetime.now()
def __init__(self, config): def __init__(self):
tracemalloc.start() tracemalloc.start()
super().__init__("KeepAlive") super().__init__("KeepAlive")
self.config = config
max_timeout = {"hours": 0.0, "minutes": 2, "seconds": 0} max_timeout = {"hours": 0.0, "minutes": 2, "seconds": 0}
self.max_delta = datetime.timedelta(**max_timeout) self.max_delta = datetime.timedelta(**max_timeout)
@ -40,15 +42,9 @@ class KeepAliveThread(APRSDThread):
stats_obj.set_memory(current) stats_obj.set_memory(current)
stats_obj.set_memory_peak(peak) stats_obj.set_memory_peak(peak)
try: login = CONF.callsign
login = self.config["aprsd"]["callsign"]
except KeyError:
login = self.config["ham"]["callsign"]
if pkt_tracker.is_initialized(): tracked_packets = len(pkt_tracker)
tracked_packets = len(pkt_tracker)
else:
tracked_packets = 0
keepalive = ( keepalive = (
"{} - Uptime {} RX:{} TX:{} Tracker:{} Msgs TX:{} RX:{} " "{} - Uptime {} RX:{} TX:{} Tracker:{} Msgs TX:{} RX:{} "
@ -77,7 +73,7 @@ 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 client.KISSClient.is_enabled(self.config): if not client.KISSClient.is_enabled():
LOG.warning(f"Resetting connection to APRS-IS {delta}") LOG.warning(f"Resetting connection to APRS-IS {delta}")
client.factory.create().reset() client.factory.create().reset()

View File

@ -0,0 +1,77 @@
import logging
import threading
import wrapt
from aprsd import threads
from aprsd.logging import log
LOG = logging.getLogger("APRSD")
class LogEntries:
entries = []
lock = threading.Lock()
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
@wrapt.synchronized(lock)
def add(self, entry):
self.entries.append(entry)
@wrapt.synchronized(lock)
def get_all_and_purge(self):
entries = self.entries.copy()
self.entries = []
return entries
@wrapt.synchronized(lock)
def __len__(self):
return len(self.entries)
class LogMonitorThread(threads.APRSDThread):
def __init__(self):
super().__init__("LogMonitorThread")
def loop(self):
try:
record = log.logging_queue.get(block=True, timeout=2)
if isinstance(record, list):
for item in record:
entry = self.json_record(item)
LogEntries().add(entry)
else:
entry = self.json_record(record)
LogEntries().add(entry)
except Exception:
# Just ignore thi
pass
return True
def json_record(self, record):
entry = {}
entry["filename"] = record.filename
entry["funcName"] = record.funcName
entry["levelname"] = record.levelname
entry["lineno"] = record.lineno
entry["module"] = record.module
entry["name"] = record.name
entry["pathname"] = record.pathname
entry["process"] = record.process
entry["processName"] = record.processName
if hasattr(record, "stack_info"):
entry["stack_info"] = record.stack_info
else:
entry["stack_info"] = None
entry["thread"] = record.thread
entry["threadName"] = record.threadName
entry["message"] = record.getMessage()
return entry

View File

@ -4,18 +4,19 @@ import queue
import time import time
import aprslib import aprslib
from oslo_config import cfg
from aprsd import client, packets, plugin from aprsd import client, packets, plugin
from aprsd.threads import APRSDThread, tx from aprsd.threads import APRSDThread, tx
CONF = cfg.CONF
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
class APRSDRXThread(APRSDThread): class APRSDRXThread(APRSDThread):
def __init__(self, config, packet_queue): def __init__(self, packet_queue):
super().__init__("RX_MSG") super().__init__("RX_MSG")
self.config = config
self.packet_queue = packet_queue self.packet_queue = packet_queue
self._client = client.factory.create() self._client = client.factory.create()
@ -80,8 +81,7 @@ class APRSDProcessPacketThread(APRSDThread):
will ack a message before sending the packet to the subclass will ack a message before sending the packet to the subclass
for processing.""" for processing."""
def __init__(self, config, packet_queue): def __init__(self, packet_queue):
self.config = config
self.packet_queue = packet_queue self.packet_queue = packet_queue
super().__init__("ProcessPKT") super().__init__("ProcessPKT")
self._loop_cnt = 1 self._loop_cnt = 1
@ -106,7 +106,7 @@ class APRSDProcessPacketThread(APRSDThread):
def process_packet(self, packet): def process_packet(self, packet):
"""Process a packet received from aprs-is server.""" """Process a packet received from aprs-is server."""
LOG.debug(f"RXPKT-LOOP {self._loop_cnt}") LOG.debug(f"RXPKT-LOOP {self._loop_cnt}")
our_call = self.config["aprsd"]["callsign"].lower() our_call = CONF.callsign.lower()
from_call = packet.from_call from_call = packet.from_call
if packet.addresse: if packet.addresse:
@ -133,7 +133,7 @@ class APRSDProcessPacketThread(APRSDThread):
# send an ack last # send an ack last
tx.send( tx.send(
packets.AckPacket( packets.AckPacket(
from_call=self.config["aprsd"]["callsign"], from_call=CONF.callsign,
to_call=from_call, to_call=from_call,
msgNo=msg_id, msgNo=msg_id,
), ),
@ -178,11 +178,11 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
if isinstance(subreply, packets.Packet): if isinstance(subreply, packets.Packet):
tx.send(subreply) tx.send(subreply)
else: else:
wl = self.config["aprsd"]["watch_list"] wl = CONF.watch_list
to_call = wl["alert_callsign"] to_call = wl["alert_callsign"]
tx.send( tx.send(
packets.MessagePacket( packets.MessagePacket(
from_call=self.config["aprsd"]["callsign"], from_call=CONF.callsign,
to_call=to_call, to_call=to_call,
message_text=subreply, message_text=subreply,
), ),
@ -219,7 +219,7 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
else: else:
tx.send( tx.send(
packets.MessagePacket( packets.MessagePacket(
from_call=self.config["aprsd"]["callsign"], from_call=CONF.callsign,
to_call=from_call, to_call=from_call,
message_text=subreply, message_text=subreply,
), ),
@ -238,7 +238,7 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
LOG.debug(f"Sending '{reply}'") LOG.debug(f"Sending '{reply}'")
tx.send( tx.send(
packets.MessagePacket( packets.MessagePacket(
from_call=self.config["aprsd"]["callsign"], from_call=CONF.callsign,
to_call=from_call, to_call=from_call,
message_text=reply, message_text=reply,
), ),
@ -246,12 +246,12 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
# If the message was for us and we didn't have a # If the message was for us and we didn't have a
# response, then we send a usage statement. # response, then we send a usage statement.
if to_call == self.config["aprsd"]["callsign"] and not replied: if to_call == CONF.callsign and not replied:
LOG.warning("Sending help!") LOG.warning("Sending help!")
message_text = "Unknown command! Send 'help' message for help" message_text = "Unknown command! Send 'help' message for help"
tx.send( tx.send(
packets.MessagePacket( packets.MessagePacket(
from_call=self.config["aprsd"]["callsign"], from_call=CONF.callsign,
to_call=from_call, to_call=from_call,
message_text=message_text, message_text=message_text,
), ),
@ -260,11 +260,11 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
LOG.error("Plugin failed!!!") LOG.error("Plugin failed!!!")
LOG.exception(ex) LOG.exception(ex)
# Do we need to send a reply? # Do we need to send a reply?
if to_call == self.config["aprsd"]["callsign"]: if to_call == CONF.callsign:
reply = "A Plugin failed! try again?" reply = "A Plugin failed! try again?"
tx.send( tx.send(
packets.MessagePacket( packets.MessagePacket(
from_call=self.config["aprsd"]["callsign"], from_call=CONF.callsign,
to_call=from_call, to_call=from_call,
message_text=reply, message_text=reply,
), ),

View File

@ -4,7 +4,7 @@ import time
from aprsd import client from aprsd import client
from aprsd import threads as aprsd_threads from aprsd import threads as aprsd_threads
from aprsd.packets import core, packet_list, tracker from aprsd.packets import core, tracker
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -27,9 +27,9 @@ def send(packet: core.Packet, direct=False, aprs_client=None):
else: else:
cl = client.factory.create() cl = client.factory.create()
packet.update_timestamp()
packet.log(header="TX") packet.log(header="TX")
cl.send(packet) cl.send(packet)
packet_list.PacketList().tx(packet)
class SendPacketThread(aprsd_threads.APRSDThread): class SendPacketThread(aprsd_threads.APRSDThread):
@ -94,8 +94,8 @@ class SendPacketThread(aprsd_threads.APRSDThread):
if send_now: if send_now:
# no attempt time, so lets send it, and start # no attempt time, so lets send it, and start
# tracking the time. # tracking the time.
send(packet, direct=True)
packet.last_send_time = datetime.datetime.now() packet.last_send_time = datetime.datetime.now()
send(packet, direct=True)
packet.send_count += 1 packet.send_count += 1
time.sleep(1) time.sleep(1)

View File

@ -1,16 +1,16 @@
import abc
import logging import logging
import os import os
import pathlib import pathlib
import pickle import pickle
from aprsd import config as aprsd_config from oslo_config import cfg
CONF = cfg.CONF
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
class ObjectStoreMixin(metaclass=abc.ABCMeta): class ObjectStoreMixin:
"""Class 'MIXIN' intended to save/load object data. """Class 'MIXIN' intended to save/load object data.
The asumption of how this mixin is used: The asumption of how this mixin is used:
@ -24,13 +24,6 @@ class ObjectStoreMixin(metaclass=abc.ABCMeta):
When APRSD Starts, it calls load() When APRSD Starts, it calls load()
aprsd server -f (flush) will wipe all saved objects. aprsd server -f (flush) will wipe all saved objects.
""" """
@abc.abstractmethod
def is_initialized(self):
"""Return True if the class has been setup correctly.
If this returns False, the ObjectStore doesn't save anything.
"""
def __len__(self): def __len__(self):
return len(self.data) return len(self.data)
@ -44,25 +37,18 @@ class ObjectStoreMixin(metaclass=abc.ABCMeta):
return self.data[id] return self.data[id]
def _init_store(self): def _init_store(self):
if self.is_initialized(): if not CONF.enable_save:
sl = self._save_location() return
if not os.path.exists(sl): sl = CONF.save_location
LOG.warning(f"Save location {sl} doesn't exist") if not os.path.exists(sl):
try: LOG.warning(f"Save location {sl} doesn't exist")
os.makedirs(sl) try:
except Exception as ex: os.makedirs(sl)
LOG.exception(ex) except Exception as ex:
else: LOG.exception(ex)
LOG.warning(f"{self.__class__.__name__} is not initialized")
def _save_location(self):
save_location = self.config.get("aprsd.save_location", None)
if not save_location:
save_location = aprsd_config.DEFAULT_CONFIG_DIR
return save_location
def _save_filename(self): def _save_filename(self):
save_location = self._save_location() save_location = CONF.save_location
return "{}/{}.p".format( return "{}/{}.p".format(
save_location, save_location,
@ -79,45 +65,48 @@ class ObjectStoreMixin(metaclass=abc.ABCMeta):
def save(self): def save(self):
"""Save any queued to disk?""" """Save any queued to disk?"""
if self.is_initialized(): if not CONF.enable_save:
if len(self) > 0: return
LOG.info( if len(self) > 0:
f"{self.__class__.__name__}::Saving" LOG.info(
f" {len(self)} entries to disk at" f"{self.__class__.__name__}::Saving"
f"{self._save_location()}", f" {len(self)} entries to disk at"
) f"{CONF.save_location}",
with open(self._save_filename(), "wb+") as fp: )
pickle.dump(self._dump(), fp) with open(self._save_filename(), "wb+") as fp:
else: pickle.dump(self._dump(), fp)
LOG.debug( else:
"{} Nothing to save, flushing old save file '{}'".format( LOG.debug(
self.__class__.__name__, "{} Nothing to save, flushing old save file '{}'".format(
self._save_filename(), self.__class__.__name__,
), self._save_filename(),
) ),
self.flush() )
self.flush()
def load(self): def load(self):
if self.is_initialized(): if not CONF.enable_save:
if os.path.exists(self._save_filename()): return
try: if os.path.exists(self._save_filename()):
with open(self._save_filename(), "rb") as fp: try:
raw = pickle.load(fp) with open(self._save_filename(), "rb") as fp:
if raw: raw = pickle.load(fp)
self.data = raw if raw:
LOG.debug( self.data = raw
f"{self.__class__.__name__}::Loaded {len(self)} entries from disk.", LOG.debug(
) f"{self.__class__.__name__}::Loaded {len(self)} entries from disk.",
LOG.debug(f"{self.data}") )
except (pickle.UnpicklingError, Exception) as ex: LOG.debug(f"{self.data}")
LOG.error(f"Failed to UnPickle {self._save_filename()}") except (pickle.UnpicklingError, Exception) as ex:
LOG.error(ex) LOG.error(f"Failed to UnPickle {self._save_filename()}")
self.data = {} LOG.error(ex)
self.data = {}
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 self.is_initialized(): if not CONF.enable_save:
if os.path.exists(self._save_filename()): return
pathlib.Path(self._save_filename()).unlink() if os.path.exists(self._save_filename()):
with self.lock: pathlib.Path(self._save_filename()).unlink()
self.data = {} with self.lock:
self.data = {}

View File

@ -109,16 +109,17 @@ function update_packets( data ) {
} }
jQuery.each(data, function(i, val) { jQuery.each(data, function(i, val) {
pkt = JSON.parse(val); pkt = JSON.parse(val);
update_watchlist_from_packet(pkt['from_call'], pkt); update_watchlist_from_packet(pkt['from_call'], pkt);
if ( packet_list.hasOwnProperty(val["timestamp"]) == false ) { if ( packet_list.hasOwnProperty(pkt.timestamp) == false ) {
// Store the packet // Store the packet
packet_list[pkt["timestamp"]] = pkt; packet_list[pkt.timestamp] = pkt;
//ts_str = val["timestamp"].toString(); //ts_str = val["timestamp"].toString();
//ts = ts_str.split(".")[0]*1000; //ts = ts_str.split(".")[0]*1000;
ts = pkt["timestamp"] ts = pkt.timestamp
var d = new Date(ts).toLocaleDateString("en-US"); var d = new Date(ts).toLocaleDateString("en-US");
var t = new Date(ts).toLocaleTimeString("en-US"); var t = new Date(ts).toLocaleTimeString("en-US");
var from_call = pkt['from_call']; var from_call = pkt.from_call;
if (from_call == our_callsign) { if (from_call == our_callsign) {
title_id = 'title_tx'; title_id = 'title_tx';
} else { } else {

View File

@ -28,36 +28,6 @@ function init_messages() {
update_msg(msg); update_msg(msg);
}); });
$("#sendform").submit(function(event) {
event.preventDefault();
var $checkboxes = $(this).find('input[type=checkbox]');
//loop through the checkboxes and change to hidden fields
$checkboxes.each(function() {
if ($(this)[0].checked) {
$(this).attr('type', 'hidden');
$(this).val(1);
} else {
$(this).attr('type', 'hidden');
$(this).val(0);
}
});
msg = {'from': $('#from').val(),
'password': $('#password').val(),
'to': $('#to').val(),
'message': $('#message').val(),
'wait_reply': $('#wait_reply').val(),
}
socket.emit("send", msg);
//loop through the checkboxes and change to hidden fields
$checkboxes.each(function() {
$(this).attr('type', 'checkbox');
});
});
} }
function add_msg(msg) { function add_msg(msg) {

View File

@ -82,7 +82,6 @@
<div class="item" data-tab="watch-tab">Watch List</div> <div class="item" data-tab="watch-tab">Watch List</div>
<div class="item" data-tab="plugin-tab">Plugins</div> <div class="item" data-tab="plugin-tab">Plugins</div>
<div class="item" data-tab="config-tab">Config</div> <div class="item" data-tab="config-tab">Config</div>
<div class="item" data-tab="send-tab">Send Message</div>
<div class="item" data-tab="log-tab">LogFile</div> <div class="item" data-tab="log-tab">LogFile</div>
<div class="item" data-tab="raw-tab">Raw JSON</div> <div class="item" data-tab="raw-tab">Raw JSON</div>
</div> </div>
@ -160,29 +159,6 @@
<pre id="configjson" class="language-json">{{ config_json|safe }}</pre> <pre id="configjson" class="language-json">{{ config_json|safe }}</pre>
</div> </div>
<div class="ui bottom attached tab segment" data-tab="send-tab">
<h3 class="ui dividing header">Send Message</h3>
<div id="sendMsgDiv" class="ui mini text">
<form id="sendform" name="sendmsg" action="">
<p><label for="from_call">From Callsign:</label>
<input type="text" name="from_call" id="from"></p>
<p><label for="from_call_password">Password:</label>
<input type="password" name="from_call_password" id='password'></p>
<p><label for="to_call">To Callsign:</label>
<input type="text" name="to_call" id="to" ></p>
<p><label for="message">Message:</label>
<input type="text" name="message" id="message" ></p>
<p><label for="wait">Wait for Reply?</label>
<input type="checkbox" name="wait_reply" id="wait_reply" value="off" checked>
</p>
<input type="submit" name="submit" class="button" id="send_msg" value="Send" />
</form>
<div class="ui styled fluid accordion" id="accordion">
<div id="sendMsgsDiv" class="ui mini text">Messages</div>
</div>
</div>
</div>
<div class="ui bottom attached tab segment" data-tab="log-tab"> <div class="ui bottom attached tab segment" data-tab="log-tab">
<h3 class="ui dividing header">LOGFILE</h3> <h3 class="ui dividing header">LOGFILE</h3>
<pre id="logContainer" style="height: 600px;overflow-y:auto;overflow-x:auto;"><code id="logtext" class="language-log" ></code></pre> <pre id="logContainer" style="height: 600px;overflow-y:auto;overflow-x:auto;"><code id="logtext" class="language-log" ></code></pre>

View File

@ -1,15 +0,0 @@
<html>
<head>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="json-viewer/jquery.json-viewer.js"></script>
<link href="json-viewer/jquery.json-viewer.css" type="text/css" rel="stylesheet" />
</head>
<pre id="json-viewer"></pre>
<script>
var data = {{ messages | safe }}
$('#json-viewer').jsonViewer(data)
</script>
</html>

View File

@ -1,74 +0,0 @@
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery-simple-websocket@1.1.4/src/jquery.simple.websocket.min.js"></script>
<script src="https://cdn.socket.io/4.1.2/socket.io.min.js" integrity="sha384-toS6mmwu70G0fw54EGlWWeA4z3dyJ+dlXBtSURSKN4vyRFOcxd3Bzjj/AoOwY+Rg" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/prism.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/components/prism-json.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/themes/prism-tomorrow.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">
<script src="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.js"></script>
<link rel="stylesheet" href="/static/css/index.css">
<link rel="stylesheet" href="/static/css/tabs.css">
<script src="/static/js/send-message.js"></script>
<script language="JavaScript">
$(document).ready(function() {
init_messages();
});
</script>
</head>
<body>
<div class='ui text container'>
<h1 class='ui dividing header'>APRSD {{ version }}</h1>
</div>
<div class='ui grid text container'>
<div class='left floated ten wide column'>
<span style='color: green'>{{ callsign }}</span>
connected to
<span style='color: blue' id='aprsis'>NONE</span>
</div>
<div class='right floated four wide column'>
<span id='uptime'>NONE</span>
</div>
</div>
<h3 class="ui dividing header">Send Message Form</h3>
<form id="sendform" name="sendmsg" action="">
<p><label for="from_call">From Callsign:</label>
<input type="text" name="from_call" id="from" value="WB4BOR"></p>
<p><label for="from_call_password">Password:</label>
<input type="password" name="from_call_password" id='password' value="24496"></p>
<p><label for="to_call">To Callsign:</label>
<input type="text" name="to_call" id="to" value="WB4BOR-11"></p>
<p><label for="message">Message:</label>
<input type="text" name="message" id="message" value="ping"></p>
<p><label for="wait">Wait for Reply?</label>
<input type="checkbox" name="wait_reply" id="wait_reply" value="off" checked>
</p>
<input type="submit" name="submit" class="button" id="send_msg" value="Send" />
</form>
<h3 class="ui dividing header">Messages (<span id="msgs_count">0</span>)</h3>
<div class="ui styled fluid accordion" id="accordion">
<div id="msgsDiv" class="ui mini text">Messages</div>
</div>
</body>
</html>

View File

@ -1,12 +1,12 @@
# #
# This file is autogenerated by pip-compile with Python 3.9 # This file is autogenerated by pip-compile with Python 3.10
# by the following command: # by the following command:
# #
# pip-compile --annotation-style=line --resolver=backtracking dev-requirements.in # pip-compile --annotation-style=line --resolver=backtracking dev-requirements.in
# #
add-trailing-comma==2.4.0 # via gray add-trailing-comma==2.4.0 # via gray
alabaster==0.7.12 # via sphinx alabaster==0.7.12 # via sphinx
attrs==22.1.0 # via jsonschema, pytest attrs==22.2.0 # via jsonschema, pytest
autoflake==1.5.3 # via gray autoflake==1.5.3 # via gray
babel==2.11.0 # via sphinx babel==2.11.0 # via sphinx
black==22.12.0 # via gray black==22.12.0 # via gray
@ -20,21 +20,20 @@ click==8.1.3 # via black, pip-tools
colorama==0.4.6 # via tox colorama==0.4.6 # via tox
commonmark==0.9.1 # via rich commonmark==0.9.1 # via rich
configargparse==1.5.3 # via gray configargparse==1.5.3 # via gray
coverage[toml]==6.5.0 # via pytest-cov coverage[toml]==7.0.1 # via pytest-cov
distlib==0.3.6 # via virtualenv distlib==0.3.6 # via virtualenv
docutils==0.19 # via sphinx docutils==0.19 # via sphinx
exceptiongroup==1.0.4 # via pytest exceptiongroup==1.1.0 # via pytest
filelock==3.8.2 # via tox, virtualenv filelock==3.8.2 # via tox, virtualenv
fixit==0.1.4 # via gray fixit==0.1.4 # via gray
flake8==6.0.0 # via -r dev-requirements.in, fixit, pep8-naming flake8==6.0.0 # via -r dev-requirements.in, fixit, pep8-naming
gray==0.13.0 # via -r dev-requirements.in gray==0.13.0 # via -r dev-requirements.in
identify==2.5.9 # via pre-commit identify==2.5.11 # via pre-commit
idna==3.4 # via requests idna==3.4 # via requests
imagesize==1.4.1 # via sphinx imagesize==1.4.1 # via sphinx
importlib-metadata==5.1.0 # via sphinx
importlib-resources==5.10.1 # via fixit importlib-resources==5.10.1 # via fixit
iniconfig==1.1.1 # via pytest iniconfig==1.1.1 # via pytest
isort==5.11.2 # via -r dev-requirements.in, gray isort==5.11.4 # via -r dev-requirements.in, gray
jinja2==3.1.2 # via sphinx jinja2==3.1.2 # via sphinx
jsonschema==4.17.3 # via fixit jsonschema==4.17.3 # via fixit
libcst==0.4.9 # via fixit libcst==0.4.9 # via fixit
@ -46,11 +45,11 @@ nodeenv==1.7.0 # via pre-commit
packaging==22.0 # via build, pyproject-api, pytest, sphinx, tox packaging==22.0 # via build, pyproject-api, pytest, sphinx, tox
pathspec==0.10.3 # via black pathspec==0.10.3 # via black
pep517==0.13.0 # via build pep517==0.13.0 # via build
pep8-naming==0.13.2 # via -r dev-requirements.in pep8-naming==0.13.3 # via -r dev-requirements.in
pip-tools==6.12.0 # via -r dev-requirements.in pip-tools==6.12.1 # via -r dev-requirements.in
platformdirs==2.6.0 # via black, tox, virtualenv platformdirs==2.6.0 # via black, tox, virtualenv
pluggy==1.0.0 # via pytest, tox pluggy==1.0.0 # via pytest, tox
pre-commit==2.20.0 # via -r dev-requirements.in pre-commit==2.21.0 # via -r dev-requirements.in
pycodestyle==2.10.0 # via flake8 pycodestyle==2.10.0 # via flake8
pyflakes==3.0.1 # via autoflake, flake8 pyflakes==3.0.1 # via autoflake, flake8
pygments==2.13.0 # via rich, sphinx pygments==2.13.0 # via rich, sphinx
@ -58,7 +57,7 @@ pyproject-api==1.2.1 # via tox
pyrsistent==0.19.2 # via jsonschema pyrsistent==0.19.2 # via jsonschema
pytest==7.2.0 # via -r dev-requirements.in, pytest-cov pytest==7.2.0 # via -r dev-requirements.in, pytest-cov
pytest-cov==4.0.0 # via -r dev-requirements.in pytest-cov==4.0.0 # via -r dev-requirements.in
pytz==2022.6 # via babel pytz==2022.7 # via babel
pyupgrade==3.3.1 # via gray pyupgrade==3.3.1 # via gray
pyyaml==6.0 # via fixit, libcst, pre-commit pyyaml==6.0 # via fixit, libcst, pre-commit
requests==2.28.1 # via sphinx requests==2.28.1 # via sphinx
@ -72,17 +71,16 @@ sphinxcontrib-jsmath==1.0.1 # via sphinx
sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-qthelp==1.0.3 # via sphinx
sphinxcontrib-serializinghtml==1.1.5 # via sphinx sphinxcontrib-serializinghtml==1.1.5 # via sphinx
tokenize-rt==5.0.0 # via add-trailing-comma, pyupgrade tokenize-rt==5.0.0 # via add-trailing-comma, pyupgrade
toml==0.10.2 # via autoflake, pre-commit toml==0.10.2 # via autoflake
tomli==2.0.1 # via black, build, coverage, mypy, pep517, pyproject-api, pytest, tox tomli==2.0.1 # via black, build, coverage, mypy, pep517, pyproject-api, pytest, tox
tox==4.0.9 # via -r dev-requirements.in tox==4.0.18 # via -r dev-requirements.in
typing-extensions==4.4.0 # via black, libcst, mypy, typing-inspect typing-extensions==4.4.0 # via libcst, mypy, typing-inspect
typing-inspect==0.8.0 # via libcst typing-inspect==0.8.0 # via libcst
unify==0.5 # via gray unify==0.5 # via gray
untokenize==0.1.1 # via unify untokenize==0.1.1 # via unify
urllib3==1.26.13 # via requests urllib3==1.26.13 # via requests
virtualenv==20.17.1 # via pre-commit, tox virtualenv==20.17.1 # via pre-commit, tox
wheel==0.38.4 # via pip-tools wheel==0.38.4 # via pip-tools
zipp==3.11.0 # via importlib-metadata, importlib-resources
# The following packages are considered to be unsafe in a requirements file: # The following packages are considered to be unsafe in a requirements file:
# pip # pip

View File

@ -41,7 +41,7 @@ RUN pip install aprsd==$APRSD_PIP_VERSION
# Ensure /config is there with a default config file # Ensure /config is there with a default config file
USER root USER root
RUN mkdir -p /config RUN mkdir -p /config
RUN aprsd sample-config > /config/aprsd.yml RUN aprsd sample-config > /config/aprsd.conf
RUN chown -R $APRS_USER:$APRS_USER /config RUN chown -R $APRS_USER:$APRS_USER /config
# override this to run another configuration # override this to run another configuration
@ -53,4 +53,4 @@ ADD bin/run.sh /usr/local/bin
ENTRYPOINT ["/usr/local/bin/run.sh"] ENTRYPOINT ["/usr/local/bin/run.sh"]
HEALTHCHECK --interval=5m --timeout=12s --start-period=30s \ HEALTHCHECK --interval=5m --timeout=12s --start-period=30s \
CMD aprsd healthcheck --config /config/aprsd.yml --url http://localhost:8001/stats CMD aprsd healthcheck --config /config/aprsd.conf --url http://localhost:8001/stats

View File

@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
FROM ubuntu:focal as aprsd FROM ubuntu:22.04
# Dockerfile for building a container during aprsd development. # Dockerfile for building a container during aprsd development.
ARG BRANCH=master ARG BRANCH=master
@ -55,7 +55,7 @@ RUN ls -al /usr/local/bin
RUN ls -al /usr/bin RUN ls -al /usr/bin
RUN which aprsd RUN which aprsd
RUN mkdir -p /config RUN mkdir -p /config
RUN aprsd sample-config > /config/aprsd.yml RUN aprsd sample-config > /config/aprsd.conf
RUN chown -R $APRS_USER:$APRS_USER /config RUN chown -R $APRS_USER:$APRS_USER /config
# override this to run another configuration # override this to run another configuration
@ -67,4 +67,4 @@ ADD bin/run.sh $HOME/
ENTRYPOINT ["/home/aprs/run.sh"] ENTRYPOINT ["/home/aprs/run.sh"]
HEALTHCHECK --interval=5m --timeout=12s --start-period=30s \ HEALTHCHECK --interval=5m --timeout=12s --start-period=30s \
CMD aprsd healthcheck --config /config/aprsd.yml --url http://localhost:8001/stats CMD aprsd healthcheck --config /config/aprsd.conf --url http://localhost:8001/stats

View File

@ -52,14 +52,6 @@ aprsd.plugins.query module
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
aprsd.plugins.stock module
--------------------------
.. automodule:: aprsd.plugins.stock
:members:
:undoc-members:
:show-inheritance:
aprsd.plugins.time module aprsd.plugins.time module
------------------------- -------------------------

View File

@ -7,11 +7,35 @@ Subpackages
.. toctree:: .. toctree::
:maxdepth: 4 :maxdepth: 4
aprsd.clients
aprsd.cmds
aprsd.conf
aprsd.logging
aprsd.packets
aprsd.plugins aprsd.plugins
aprsd.threads
aprsd.utils
aprsd.web
Submodules Submodules
---------- ----------
aprsd.aprsd module
------------------
.. automodule:: aprsd.aprsd
:members:
:undoc-members:
:show-inheritance:
aprsd.cli\_helper module
------------------------
.. automodule:: aprsd.cli_helper
:members:
:undoc-members:
:show-inheritance:
aprsd.client module aprsd.client module
------------------- -------------------
@ -20,18 +44,10 @@ aprsd.client module
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
aprsd.dev module aprsd.exception module
---------------- ----------------------
.. automodule:: aprsd.dev .. automodule:: aprsd.exception
:members:
:undoc-members:
:show-inheritance:
aprsd.fake\_aprs module
-----------------------
.. automodule:: aprsd.fake_aprs
:members: :members:
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
@ -44,46 +60,6 @@ aprsd.flask module
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
aprsd.fuzzyclock module
-----------------------
.. automodule:: aprsd.fuzzyclock
:members:
:undoc-members:
:show-inheritance:
aprsd.healthcheck module
------------------------
.. automodule:: aprsd.healthcheck
:members:
:undoc-members:
:show-inheritance:
aprsd.kissclient module
-----------------------
.. automodule:: aprsd.kissclient
:members:
:undoc-members:
:show-inheritance:
aprsd.listen module
-------------------
.. automodule:: aprsd.listen
:members:
:undoc-members:
:show-inheritance:
aprsd.main module
-----------------
.. automodule:: aprsd.main
:members:
:undoc-members:
:show-inheritance:
aprsd.messaging module aprsd.messaging module
---------------------- ----------------------
@ -92,14 +68,6 @@ aprsd.messaging module
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
aprsd.packets module
--------------------
.. automodule:: aprsd.packets
:members:
:undoc-members:
:show-inheritance:
aprsd.plugin module aprsd.plugin module
------------------- -------------------
@ -116,6 +84,14 @@ aprsd.plugin\_utils module
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
aprsd.rpc\_server module
------------------------
.. automodule:: aprsd.rpc_server
:members:
:undoc-members:
:show-inheritance:
aprsd.stats module aprsd.stats module
------------------ ------------------
@ -124,30 +100,6 @@ aprsd.stats module
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
aprsd.threads module
--------------------
.. automodule:: aprsd.threads
:members:
:undoc-members:
:show-inheritance:
aprsd.trace module
------------------
.. automodule:: aprsd.trace
:members:
:undoc-members:
:show-inheritance:
aprsd.utils module
------------------
.. automodule:: aprsd.utils
:members:
:undoc-members:
:show-inheritance:
Module contents Module contents
--------------- ---------------

View File

@ -1,9 +1,339 @@
CHANGES CHANGES
======= =======
* Decouple admin web interface from server command
* Dockerfile now produces aprsd.conf
* Fix some unit tests and loading of CONF w/o file
* Added missing conf
* Removed references to old custom config
* Convert config to oslo\_config
* Added rain formatting unit tests to WeatherPacket
* Fix Rain reporting in WeatherPacket send
* Removed Packet.send()
* Removed watchlist plugins
* Fix PluginManager.get\_plugins
* Cleaned up PluginManager
* Cleaned up PluginManager
* Update routing for weatherpacket
* Fix some WeatherPacket formatting
* Fix pep8 violation
* Add packet filtering for aprsd listen
* Added WeatherPacket encoding
* Updated webchat and listen for queue based RX
* reworked collecting and reporting stats
* Removed unused threading code
* Change RX packet processing to enqueu
* Make tracking objectstores work w/o initializing
* Cleaned up packet transmit class attributes
* Fix packets timestamp to int
* More messaging -> packets cleanup
* Cleaned out all references to messaging
* Added contructing a GPSPacket for sending
* cleanup webchat
* Reworked all packet processing
* Updated plugins and plugin interfaces for Packet
* Started using dataclasses to describe packets
v2.6.1
------
* v2.6.1
* Fixed position report for webchat beacon
* Try and fix broken 32bit qemu builds on 64bit system
* Add unit tests for webchat
* remove armv7 build RUST sucks
* Fix for Collections change in 3.10
v2.6.0
------
* Update workflow again
* Update Dockerfile to 22.04
* Update Dockerfile and build.sh
* Update workflow
* Prep for 2.6.0 release
* Update requirements
* Removed Makefile comment
* Update Makefile for dev vs. run environments
* Added pyopenssl for https for webchat
* change from device-detector to user-agents
* Remove twine from dev-requirements
* Update to latest Makefile.venv
* Refactored threads a bit
* Mark packets as acked in MsgTracker
* remove dev setting for template
* Add GPS beacon to mobile page
* Allow werkzeug for admin interface
* Allow werkzeug for admin interface
* Add support for mobile browsers for webchat
* Ignore callsign case while processing packets
* remove linux/arm/v7 for official builds for now
* added workflow for building specific version
* Allow passing in version to the Dockerfile
* Send GPS Beacon from webchat interface
* specify Dockerfile-dev
* Fixed build.sh
* Build on the source not released aprsd
* Remove email validation
* Add support for building linux/arm/v7
* Remove python 3.7 from docker build github
* Fixed failing unit tests
* change github workflow
* Removed TimeOpenCageDataPlugin
* Dump config with aprsd dev test-plugin
* Updated requirements
* Got webchat working with KISS tcp
* Added click auto\_envvar\_prefix
* Update aprsd thread base class to use queue
* Update packets to use wrapt
* Add remving existing requirements
* Try sending raw APRSFrames to aioax25
* Use new aprsd.callsign as the main callsign
* Fixed access to threads refactor
* Added webchat command
* Moved log.py to logging
* Moved trace.py to utils
* Fixed pep8 errors
* Refactored threads.py
* Refactor utils to directory
* remove arm build for now
* Added rustc and cargo to Dockerfile
* remove linux/arm/v6 from docker platform build
* Only tag master build as master
* Remove docker build from test
* create master-build.yml
* Added container build action
* Update docs on using Docker
* Update dev-requirements pip-tools
* Fix typo in docker-compose.yml
* Fix PyPI scraping
* Allow web interface when running in Docker
* Fix typo on exception
* README formatting fixes
* Bump dependencies to fix python 3.10
* Fixed up config option checking for KISS
* Fix logging issue with log messages
* for 2.5.9
v2.5.9
------
* FIX: logging exceptions
* Updated build and run for rich lib
* update build for 2.5.8
v2.5.8
------
* For 2.5.8
* Removed debug code
* Updated list-plugins
* Renamed virtualenv dir to .aprsd-venv
* Added unit tests for dev test-plugin
* Send Message command defaults to config
v2.5.7
------
* Updated Changelog
* Fixed an KISS config disabled issue
* Fixed a bug with multiple notify plugins enabled
* Unify the logging to file and stdout
* Added new feature to list-plugins command
* more README.rst cleanup
* Updated README examples
v2.5.6
------
* Changelog
* Tightened up the packet logging
* Added unit tests for USWeatherPlugin, USMetarPlugin
* Added test\_location to test LocationPlugin
* Updated pytest output
* Added py39 to tox for tests
* Added NotifyPlugin unit tests and more
* Small cleanup on packet logging
* Reduced the APRSIS connection reset to 2 minutes
* Fixed the NotifyPlugin
* Fixed some pep8 errors
* Add tracing for dev command
* Added python rich library based logging
* Added LOG\_LEVEL env variable for the docker
v2.5.5
------
* Update requirements to use aprslib 0.7.0
* fixed the failure during loading for objectstore
* updated docker build
v2.5.4
------
* Updated Changelog
* Fixed dev command missing initialization
v2.5.3
------
* Fix admin logging tab
v2.5.2
------
* Added new list-plugins command
* Don't require check-version command to have a config
* Healthcheck command doesn't need the aprsd.yml config
* Fix test failures
* Removed requirement for aprs.fi key
* Updated Changelog
v2.5.1
------
* Removed stock plugin
* Removed the stock plugin
v2.5.0
------
* Updated for v2.5.0
* Updated Dockerfile's and build script for docker
* Cleaned up some verbose output & colorized output
* Reworked all the common arguments
* Fixed test-plugin
* Ensure common params are honored
* pep8
* Added healthcheck to the cmds
* Removed the need for FROMCALL in dev test-plugin
* Pep8 failures
* Refactor the cli
* Updated Changelog for 4.2.3
* Fixed a problem with send-message command
v2.4.2
------
* Updated Changelog
* Be more careful picking data to/from disk
* Updated Changelog
v2.4.1
------
* Ensure plugins are last to be loaded
* Fixed email connecting to smtp server
v2.4.0
------
* Updated Changelog for 2.4.0 release
* Converted MsgTrack to ObjectStoreMixin
* Fixed unit tests
* Make sure SeenList update has a from in packet
* Ensure PacketList is initialized
* Added SIGTERM to signal\_handler
* Enable configuring where to save the objectstore data
* PEP8 cleanup
* Added objectstore Mixin
* Added -num option to aprsd-dev test-plugin
* Only call stop\_threads if it exists
* Added new SeenList
* Added plugin version to stats reporting
* Added new HelpPlugin
* Updated aprsd-dev to use config for logfile format
* Updated build.sh
* removed usage of config.check\_config\_option
* Fixed send-message after config/client rework
* Fixed issue with flask config
* Added some server startup info logs
* Increase email delay to +10
* Updated dev to use plugin manager
* Fixed notify plugins
* Added new Config object
* Fixed email plugin's use of globals
* Refactored client classes
* Refactor utils usage
* 2.3.1 Changelog
v2.3.1
------
* Fixed issue of aprs-is missing keepalive
* Fixed packet processing issue with aprsd send-message
v2.3.0
------
* Prep 2.3.0
* Enable plugins to return message object
* Added enabled flag for every plugin object
* Ensure plugin threads are valid
* Updated Dockerfile to use v2.3.0
* Removed fixed size on logging queue
* Added Logfile tab in Admin ui
* Updated Makefile clean target
* Added self creating Makefile help target
* Update dev.py
* Allow passing in aprsis\_client
* Fixed a problem with the AVWX plugin not working
* Remove some noisy trace in email plugin
* Fixed issue at startup with notify plugin
* Fixed email validation
* Removed values from forms
* Added send-message to the main admin UI
* Updated requirements
* Cleaned up some pep8 failures
* Upgraded the send-message POC to use websockets
* New Admin ui send message page working
* Send Message via admin Web interface
* Updated Admin UI to show KISS connections
* Got TX/RX working with aioax25+direwolf over TCP
* Rebased from master
* Added the ability to use direwolf KISS socket
* Update Dockerfile to use 2.2.1
v2.2.1
------
* Update Changelog for 2.2.1
* Silence some log noise
v2.2.0
------
* Updated Changelog for v2.2.0
* Updated overview image
* Removed Black code style reference
* Removed TXThread
* Added days to uptime string formatting
* Updated select timeouts
* Rebase from master and run gray
* Added tracking plugin processing
* Added threads functions to APRSDPluginBase
* Refactor Message processing and MORE
* Use Gray instead of Black for code formatting
* Updated tox.ini
* Fixed LOG.debug issue in weather plugin
* Updated slack channel link
* Cleanup of the README.rst
* Fixed aprsd-dev
v2.1.0
------
* Prep for v2.1.0
* Enable multiple replies for plugins
* Put in a fix for aprslib parse exceptions
* Fixed time plugin
* Updated the charts Added the packets chart
* Added showing symbol images to watch list
v2.0.0 v2.0.0
------ ------
* Updated docs for 2.0.0
* Reworked the notification threads and admin ui * Reworked the notification threads and admin ui
* Fixed small bug with packets get\_packet\_type * Fixed small bug with packets get\_packet\_type
* Updated overview images * Updated overview images

View File

@ -64,7 +64,7 @@ master_doc = "index"
# #
# This is also used if you do content translation via gettext catalogs. # This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases. # Usually you set "language" from the command line for these cases.
language = None language = "en"
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.

View File

@ -15,10 +15,11 @@ a sample config file in the default location of ~/.config/aprsd/aprsd.yml
.. code-block:: shell .. code-block:: shell
└─[$] -> aprsd server └─> aprsd server
Load config 12/28/2022 04:26:31 PM MainThread ERROR No config file found!! run 'aprsd sample-config' cli_helper.py:90
/home/aprsd/.config/aprsd/aprsd.yml is missing, creating config file 12/28/2022 04:26:31 PM MainThread ERROR Config aprs_network.password not set. client.py:105
Default config file created at /home/aprsd/.config/aprsd/aprsd.yml. Please edit with your settings. 12/28/2022 04:26:31 PM MainThread ERROR Option 'aprs_network.password is not set.' was not in config file client.py:268
12/28/2022 04:26:31 PM MainThread ERROR APRS client is not properly configured in config file. server.py:58
You can see the sample config file output You can see the sample config file output
@ -27,43 +28,310 @@ Sample config file
.. code-block:: shell .. code-block:: shell
└─[$] -> cat ~/.config/aprsd/aprsd.yml └─> aprsd sample-config
aprs: [DEFAULT]
host: rotate.aprs.net
logfile: /tmp/arsd.log
login: someusername
password: somepassword
port: 14580
aprsd:
enabled_plugins:
- aprsd.plugins.email.EmailPlugin
- aprsd.plugins.fortune.FortunePlugin
- aprsd.plugins.location.LocationPlugin
- aprsd.plugins.ping.PingPlugin
- aprsd.plugins.query.QueryPlugin
- aprsd.plugins.time.TimePlugin
- aprsd.plugins.weather.WeatherPlugin
- aprsd.plugins.version.VersionPlugin
plugin_dir: ~/.config/aprsd/plugins
ham:
callsign: KFART
imap:
host: imap.gmail.com
login: imapuser
password: something here too
port: 993
use_ssl: true
shortcuts:
aa: 5551239999@vtext.com
cl: craiglamparter@somedomain.org
wb: 555309@vtext.com
smtp:
host: imap.gmail.com
login: something
password: some lame password
port: 465
use_ssl: false
#
# From aprsd.conf
#
# Callsign to use for messages sent by APRSD (string value)
#callsign = <None>
# Enable saving of watch list, packet tracker between restarts.
# (boolean value)
#enable_save = true
# Save location for packet tracking files. (string value)
#save_location = ~/.config/aprsd
# Enable code tracing (boolean value)
#trace_enabled = false
# Units for display, imperial or metric (string value)
#units = imperial
# Comma separated list of enabled plugins for APRSD.To enable
# installed external plugins add them here.The full python path to the
# class name must be used (list value)
#enabled_plugins = aprsd.plugins.email.EmailPlugin,aprsd.plugins.fortune.FortunePlugin,aprsd.plugins.location.LocationPlugin,aprsd.plugins.ping.PingPlugin,aprsd.plugins.query.QueryPlugin,aprsd.plugins.time.TimePlugin,aprsd.plugins.weather.OWMWeatherPlugin,aprsd.plugins.version.VersionPlugin
[admin]
#
# From aprsd.conf
#
# Enable the Admin Web Interface (boolean value)
#web_enabled = false
# The ip address to listen on (IP address value)
#web_ip = 0.0.0.0
# The port to listen on (port value)
# Minimum value: 0
# Maximum value: 65535
#web_port = 8001
# The admin user for the admin web interface (string value)
#user = admin
# Admin interface password (string value)
#password = <None>
[aprs_fi]
#
# From aprsd.conf
#
# Get the apiKey from your aprs.fi account here:http://aprs.fi/account
# (string value)
#apiKey = <None>
[aprs_network]
#
# From aprsd.conf
#
# Set enabled to False if there is no internet connectivity.This is
# useful for a direwolf KISS aprs connection only. (boolean value)
#enabled = true
# APRS Username (string value)
#login = NOCALL
# APRS Password Get the passcode for your callsign here:
# https://apps.magicbug.co.uk/passcode (string value)
#password = <None>
# The APRS-IS hostname (hostname value)
#host = noam.aprs2.net
# APRS-IS port (port value)
# Minimum value: 0
# Maximum value: 65535
#port = 14580
[aprsd_weewx_plugin]
#
# From aprsd_weewx_plugin.conf
#
# Latitude of the station you want to report as (floating point value)
#latitude = <None>
# Longitude of the station you want to report as (floating point
# value)
#longitude = <None>
# How long (in seconds) in between weather reports (integer value)
#report_interval = 60
[avwx_plugin]
#
# From aprsd.conf
#
# avwx-api is an opensource project that hasa hosted service here:
# https://avwx.rest/You can launch your own avwx-api in a containerby
# cloning the githug repo here:https://github.com/avwx-rest/AVWX-API
# (string value)
#apiKey = <None>
# The base url for the avwx API. If you are hosting your ownHere is
# where you change the url to point to yours. (string value)
#base_url = https://avwx.rest
[email_plugin]
#
# From aprsd.conf
#
# (Required) Callsign to validate for doing email commands.Only this
# callsign can check email. This is also where the email notifications
# for new emails will be sent. (string value)
#callsign = <None>
# Enable the Email plugin? (boolean value)
#enabled = false
# Enable the Email plugin Debugging? (boolean value)
#debug = false
# Login username/email for IMAP server (string value)
#imap_login = <None>
# Login password for IMAP server (string value)
#imap_password = <None>
# Hostname/IP of the IMAP server (hostname value)
#imap_host = <None>
# Port to use for IMAP server (port value)
# Minimum value: 0
# Maximum value: 65535
#imap_port = 993
# Use SSL for connection to IMAP Server (boolean value)
#imap_use_ssl = true
# Login username/email for SMTP server (string value)
#smtp_login = <None>
# Login password for SMTP server (string value)
#smtp_password = <None>
# Hostname/IP of the SMTP server (hostname value)
#smtp_host = <None>
# Port to use for SMTP server (port value)
# Minimum value: 0
# Maximum value: 65535
#smtp_port = 465
# Use SSL for connection to SMTP Server (boolean value)
#smtp_use_ssl = true
# List of email shortcuts for checking/sending email For Exmaple:
# wb=walt@walt.com,cl=cl@cl.com
# Means use 'wb' to send an email to walt@walt.com (list value)
#email_shortcuts = <None>
[kiss_serial]
#
# From aprsd.conf
#
# Enable Serial KISS interface connection. (boolean value)
#enabled = false
# Serial Device file to use. /dev/ttyS0 (string value)
#device = <None>
# The Serial device baud rate for communication (integer value)
#baudrate = 9600
[kiss_tcp]
#
# From aprsd.conf
#
# Enable Serial KISS interface connection. (boolean value)
#enabled = false
# The KISS TCP Host to connect to. (hostname value)
#host = <None>
# The KISS TCP/IP network port (port value)
# Minimum value: 0
# Maximum value: 65535
#port = 8001
[logging]
#
# From aprsd.conf
#
# Date format for log entries (string value)
#date_format = %m/%d/%Y %I:%M:%S %p
# Enable Rich logging (boolean value)
#rich_logging = true
# File to log to (string value)
#logfile = <None>
# Log file format, unless rich_logging enabled. (string value)
#logformat = [%(asctime)s] [%(threadName)-20.20s] [%(levelname)-5.5s] %(message)s - [%(pathname)s:%(lineno)d]
[owm_weather_plugin]
#
# From aprsd.conf
#
# OWMWeatherPlugin api key to OpenWeatherMap's API.This plugin uses
# the openweathermap API to fetchlocation and weather information.To
# use this plugin you need to get an openweathermapaccount and
# apikey.https://home.openweathermap.org/api_keys (string value)
#apiKey = <None>
[query_plugin]
#
# From aprsd.conf
#
# The Ham callsign to allow access to the query plugin from RF.
# (string value)
#callsign = <None>
[rpc_settings]
#
# From aprsd.conf
#
# Enable RPC calls (boolean value)
#enabled = true
# The ip address to listen on (string value)
#ip = localhost
# The port to listen on (port value)
# Minimum value: 0
# Maximum value: 65535
#port = 18861
# Magic word to authenticate requests between client/server (string
# value)
#magic_word = CHANGEME!!!
[watch_list]
#
# From aprsd.conf
#
# Enable the watch list feature. Still have to enable the correct
# plugin. Built-in plugin to use is aprsd.plugins.notify.NotifyPlugin
# (boolean value)
#enabled = false
# Callsigns to watch for messsages (list value)
#callsigns = <None>
# The Ham Callsign to send messages to for watch list alerts. (string
# value)
#alert_callsign = <None>
# The number of packets to store. (integer value)
#packet_keep_count = 10
# Time to wait before alert is sent on new message for users in
# callsigns. (integer value)
#alert_time_seconds = 3600
Note, You must edit the config file and change the ham callsign to your Note, You must edit the config file and change the ham callsign to your
legal FCC HAM callsign, or aprsd server will not start. legal FCC HAM callsign, or aprsd server will not start.

View File

@ -41,7 +41,7 @@ aprsd/examples/plugins/example_plugin.py
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
class HelloPlugin(plugin.APRSDPluginBase): class HelloPlugin(plugin.APRSDRegexCommandPluginBase):
"""Hello World.""" """Hello World."""
version = "1.0" version = "1.0"
@ -49,7 +49,7 @@ aprsd/examples/plugins/example_plugin.py
command_regex = "^[hH]" command_regex = "^[hH]"
command_name = "hello" command_name = "hello"
def command(self, fromcall, message, ack): def process(self, packet):
LOG.info("HelloPlugin") LOG.info("HelloPlugin")
reply = "Hello '{}'".format(fromcall) reply = "Hello '{}'".format(packet.from_call)
return reply return reply

View File

@ -1,28 +1,12 @@
===== ===============================================
APRSD APRSD - Ham radio APRS-IS Message plugin server
===== ===============================================
by KM6LYW and WB4BOR
.. image:: https://badge.fury.io/py/aprsd.svg KM6LYW and WB4BOR
:target: https://badge.fury.io/py/aprsd ____________________
.. image:: https://github.com/craigerl/aprsd/workflows/python/badge.svg |pypi| |pytest| |versions| |slack| |issues| |commit| |imports| |down|
:target: https://github.com/craigerl/aprsd/actions
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://black.readthedocs.io/en/stable/
.. image:: https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336
:target: https://timothycrosley.github.io/isort/
.. image:: https://img.shields.io/github/issues/craigerl/aprsd
.. image:: https://img.shields.io/github/last-commit/craigerl/aprsd
.. image:: https://static.pepy.tech/personalized-badge/aprsd?period=month&units=international_system&left_color=black&right_color=orange&left_text=Downloads
:target: https://pepy.tech/project/aprsd
.. contents:: :local:
`APRSD <http://github.com/craigerl/aprsd>`_ is a Ham radio `APRS <http://aprs.org>`_ message command gateway built on python. `APRSD <http://github.com/craigerl/aprsd>`_ is a Ham radio `APRS <http://aprs.org>`_ message command gateway built on python.
@ -37,11 +21,14 @@ provide responding to messages to check email, get location, ping,
time of day, get weather, and fortune telling as well as version information time of day, get weather, and fortune telling as well as version information
of aprsd itself. of aprsd itself.
Documentation: https://aprsd.readthedocs.io Please `read the docs`_ to learn more!
.. contents:: :local:
APRSD Overview Diagram APRSD Overview Diagram
---------------------- ======================
.. image:: https://raw.githubusercontent.com/craigerl/aprsd/master/docs/_static/aprsd_overview.svg?sanitize=true .. image:: https://raw.githubusercontent.com/craigerl/aprsd/master/docs/_static/aprsd_overview.svg?sanitize=true
@ -50,7 +37,7 @@ Typical use case
================ ================
Ham radio operator using an APRS enabled HAM radio sends a message to check Ham radio operator using an APRS enabled HAM radio sends a message to check
the weather. an APRS message is sent, and then picked up by APRSD. The the weather. An APRS message is sent, and then picked up by APRSD. The
APRS packet is decoded, and the message is sent through the list of plugins APRS packet is decoded, and the message is sent through the list of plugins
for processing. For example, the WeatherPlugin picks up the message, fetches the weather for processing. For example, the WeatherPlugin picks up the message, fetches the weather
for the area around the user who sent the request, and then responds with for the area around the user who sent the request, and then responds with
@ -59,103 +46,91 @@ callsigns to look out for. The watch list can notify you when a HAM callsign
in the list is seen and now available to message on the APRS network. in the list is seen and now available to message on the APRS network.
APRSD Capabilities Current list of built-in plugins
==================
* server - The main aprsd server processor. Send/Rx APRS messages to HAM callsign
* send-message - use aprsd to send a command/message to aprsd server. Used for development testing
* sample-config - generate a sample aprsd.yml config file for use/editing
* bash completion generation. Uses python click bash completion to generate completion code for your .bashrc/.zshrc
List of core server plugins
===========================
Plugins function by specifying a regex that is searched for in the APRS message.
If it matches, the plugin runs. IF the regex doesn't match, the plugin is skipped.
* EmailPlugin - Check email and reply with contents. Have to configure IMAP and SMTP settings in aprs.yml
* FortunePlugin - Replies with old unix fortune random fortune!
* LocationPlugin - Checks location of ham operator
* PingPlugin - Sends pong with timestamp
* QueryPlugin - Allows querying the list of delayed messages that were not ACK'd by radio
* TimePlugin - Current time of day
* WeatherPlugin - Get weather conditions for current location of HAM callsign
* VersionPlugin - Reports the version information for aprsd
List of core notification plugins
=================================
These plugins see all APRS messages from ham callsigns in the config's watch
list.
* NotifySeenPlugin - Send a message when a message is seen from a callsign in
the watch list. This is helpful when you want to know
when a friend is online in the ARPS network, but haven't
been seen in a while.
Current messages this will respond to:
====================================== ======================================
:: ::
APRS messages: └─> aprsd list-plugins
l(ocation) [callsign] = descriptive current location of your radio 🐍 APRSD Built-in Plugins 🐍
8 Miles E Auburn CA 1673' 39.92150,-120.93950 0.1h ago ┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
w(eather) = weather forecast for your radio's current position ┃ Plugin Name ┃ Info ┃ Type ┃ Plugin Path ┃
58F(58F/46F) Partly Cloudy. Tonight, Heavy Rain. ┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
t(ime) = respond with the current time │ AVWXWeatherPlugin │ AVWX weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.AVWXWeatherPlugin │
f(ortune) = respond with a short fortune │ EmailPlugin │ Send and Receive email │ RegexCommand │ aprsd.plugins.email.EmailPlugin │
-email_addr email text = send an email, say "mapme" to send a current position/map │ FortunePlugin │ Give me a fortune │ RegexCommand │ aprsd.plugins.fortune.FortunePlugin │
-2 = resend the last 2 emails from your imap inbox to this radio │ LocationPlugin │ Where in the world is a CALLSIGN's last GPS beacon? │ RegexCommand │ aprsd.plugins.location.LocationPlugin │
p(ing) = respond with Pong!/time │ NotifySeenPlugin │ Notify me when a CALLSIGN is recently seen on APRS-IS │ WatchList │ aprsd.plugins.notify.NotifySeenPlugin │
v(ersion) = Respond with current APRSD Version string │ OWMWeatherPlugin │ OpenWeatherMap weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.OWMWeatherPlugin │
anything else = respond with usage │ PingPlugin │ reply with a Pong! │ RegexCommand │ aprsd.plugins.ping.PingPlugin │
│ QueryPlugin │ APRSD Owner command to query messages in the MsgTrack │ RegexCommand │ aprsd.plugins.query.QueryPlugin │
│ TimeOWMPlugin │ Current time of GPS beacon's timezone. Uses OpenWeatherMap │ RegexCommand │ aprsd.plugins.time.TimeOWMPlugin │
│ TimePlugin │ What is the current local time. │ RegexCommand │ aprsd.plugins.time.TimePlugin │
│ USMetarPlugin │ USA only METAR of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USMetarPlugin │
│ USWeatherPlugin │ Provide USA only weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USWeatherPlugin │
│ VersionPlugin │ What is the APRSD Version │ RegexCommand │ aprsd.plugins.version.VersionPlugin │
└───────────────────┴────────────────────────────────────────────────────────────┴──────────────┴─────────────────────────────────────────┘
Meanwhile this code will monitor a single imap mailbox and forward email Pypi.org APRSD Installable Plugin Packages
to your BASECALLSIGN over the air. Only radios using the BASECALLSIGN are allowed
to send email, so consider this security risk before using this (or Amatuer radio in
general). Email is single user at this time.
There are additional parameters in the code (sorry), so be sure to set your Install any of the following plugins with 'pip install <Plugin Package Name>'
email server, and associated logins, passwords. search for "yourdomain", ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
"password". Search for "shortcuts" to setup email aliases as well. ┃ Plugin Package Name ┃ Description ┃ Version ┃ Released ┃ Installed? ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ 📂 aprsd-stock-plugin │ Ham Radio APRSD Plugin for fetching stock quotes │ 0.1.3 │ Dec 2, 2022 │ No │
│ 📂 aprsd-sentry-plugin │ Ham radio APRSD plugin that does.... │ 0.1.2 │ Dec 2, 2022 │ No │
│ 📂 aprsd-timeopencage-plugin │ APRSD plugin for fetching time based on GPS location │ 0.1.0 │ Dec 2, 2022 │ No │
│ 📂 aprsd-weewx-plugin │ HAM Radio APRSD that reports weather from a weewx weather station. │ 0.1.4 │ Dec 7, 2021 │ Yes │
│ 📂 aprsd-repeat-plugins │ APRSD Plugins for the REPEAT service │ 1.0.12 │ Dec 2, 2022 │ No │
│ 📂 aprsd-telegram-plugin │ Ham Radio APRS APRSD plugin for Telegram IM service │ 0.1.3 │ Dec 2, 2022 │ No │
│ 📂 aprsd-twitter-plugin │ Python APRSD plugin to send tweets │ 0.3.0 │ Dec 7, 2021 │ No │
│ 📂 aprsd-slack-plugin │ Amateur radio APRS daemon which listens for messages and responds │ 1.0.5 │ Dec 18, 2022 │ No │
└──────────────────────────────┴────────────────────────────────────────────────────────────────────┴─────────┴──────────────┴────────────┘
Installation: 🐍 APRSD Installed 3rd party Plugins 🐍
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Package Name ┃ Plugin Name ┃ Version ┃ Type ┃ Plugin Path ┃
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ aprsd-weewx-plugin │ WeewxMQTTPlugin │ 1.0 │ RegexCommand │ aprsd_weewx_plugin.weewx.WeewxMQTTPlugin │
└────────────────────┴─────────────────┴─────────┴──────────────┴──────────────────────────────────────────┘
Installation
============= =============
pip install aprsd To install ``aprsd``, use Pip:
Example usage: ``pip install aprsd``
Example usage
============== ==============
aprsd -h ``aprsd -h``
Help Help
==== ====
:: ::
└─[$] > aprsd -h └─> aprsd -h
Usage: aprsd [OPTIONS] COMMAND [ARGS]... Usage: aprsd [OPTIONS] COMMAND [ARGS]...
Shell completion for click-completion-command Available shell types:
bash Bourne again shell fish Friendly interactive shell
powershell Windows PowerShell zsh Z shell Default type: auto
Options: Options:
--version Show the version and exit. --version Show the version and exit.
-h, --help Show this message and exit. -h, --help Show this message and exit.
Commands: Commands:
install Install the click-completion-command completion check-version Check this version against the latest in pypi.org.
sample-config This dumps the config to stdout. completion Click Completion subcommands
dev Development type subcommands
healthcheck Check the health of the running aprsd server.
list-plugins List the built in plugins available to APRSD.
listen Listen to packets on the APRS-IS Network based on FILTER.
sample-config Generate a sample Config file from aprsd and all...
send-message Send a message to a callsign via APRS_IS. send-message Send a message to a callsign via APRS_IS.
server Start the aprsd server process. server Start the aprsd server gateway process.
show Show the click-completion-command completion code version Show the APRSD version.
webchat Web based HAM Radio chat program!
@ -165,90 +140,14 @@ Commands
Configuration Configuration
============= =============
This command outputs a sample config yml formatted block that you can edit This command outputs a sample config yml formatted block that you can edit
and use to pass in to aprsd with -c. By default aprsd looks in ~/.config/aprsd/aprsd.yml and use to pass in to ``aprsd`` with ``-c``. By default aprsd looks in ``~/.config/aprsd/aprsd.yml``
aprsd sample-config ``aprsd sample-config``
Output
======
:: ::
└─> aprsd sample-config └─> aprsd sample-config
aprs: ...
# Get the passcode for your callsign here:
# https://apps.magicbug.co.uk/passcode
host: rotate.aprs2.net
login: CALLSIGN
password: '00000'
port: 14580
aprsd:
dateformat: '%m/%d/%Y %I:%M:%S %p'
email:
enabled: true
imap:
debug: false
host: imap.gmail.com
login: IMAP_USERNAME
password: IMAP_PASSWORD
port: 993
use_ssl: true
shortcuts:
aa: 5551239999@vtext.com
cl: craiglamparter@somedomain.org
wb: 555309@vtext.com
smtp:
debug: false
host: smtp.gmail.com
login: SMTP_USERNAME
password: SMTP_PASSWORD
port: 465
use_ssl: false
enabled_plugins:
- aprsd.plugins.email.EmailPlugin
- aprsd.plugins.fortune.FortunePlugin
- aprsd.plugins.location.LocationPlugin
- aprsd.plugins.ping.PingPlugin
- aprsd.plugins.query.QueryPlugin
- aprsd.plugins.stock.StockPlugin
- aprsd.plugins.time.TimePlugin
- aprsd.plugins.weather.USWeatherPlugin
- aprsd.plugins.version.VersionPlugin
logfile: /tmp/aprsd.log
logformat: '[%(asctime)s] [%(threadName)-12s] [%(levelname)-5.5s] %(message)s - [%(pathname)s:%(lineno)d]'
trace: false
units: imperial
web:
enabled: true
host: 0.0.0.0
logging_enabled: true
port: 8001
users:
admin: aprsd
ham:
callsign: CALLSIGN
services:
aprs.fi:
# Get the apiKey from your aprs.fi account here:
# http://aprs.fi/account
apiKey: APIKEYVALUE
avwx:
# (Optional for AVWXWeatherPlugin)
# Use hosted avwx-api here: https://avwx.rest
# or deploy your own from here:
# https://github.com/avwx-rest/avwx-api
apiKey: APIKEYVALUE
base_url: http://host:port
opencagedata:
# (Optional for TimeOpenCageDataPlugin)
# Get the apiKey from your opencagedata account here:
# https://opencagedata.com/dashboard#api-keys
apiKey: APIKEYVALUE
openweathermap:
# (Optional for OWMWeatherPlugin)
# Get the apiKey from your
# openweathermap account here:
# https://home.openweathermap.org/api_keys
apiKey: APIKEYVALUE
server server
====== ======
@ -259,35 +158,35 @@ look for incomming commands to the callsign configured in the config file
:: ::
└─[$] > aprsd server --help └─[$] > aprsd server --help
Usage: aprsd server [OPTIONS] Usage: aprsd server [OPTIONS]
Start the aprsd server process. Start the aprsd server gateway process.
Options: Options:
--loglevel [CRITICAL|ERROR|WARNING|INFO|DEBUG] --loglevel [CRITICAL|ERROR|WARNING|INFO|DEBUG]
The log level to use for aprsd.log The log level to use for aprsd.log
[default: INFO] [default: INFO]
-c, --config TEXT The aprsd config file to use for options.
[default:
/Users/i530566/.config/aprsd/aprsd.yml]
--quiet Don't log to stdout
-f, --flush Flush out all old aged messages on disk.
[default: False]
-h, --help Show this message and exit.
--quiet Don't log to stdout └─> aprsd server
--disable-validation Disable email shortcut validation. Bad
email addresses can result in broken email
responses!!
-c, --config TEXT The aprsd config file to use for options.
[default:
/home/waboring/.config/aprsd/aprsd.yml]
-f, --flush Flush out all old aged messages on disk.
[default: False]
-h, --help Show this message and exit.
$ aprsd server
Load config Load config
[02/13/2021 09:22:09 AM] [MainThread ] [INFO ] APRSD Started version: 1.6.0 12/07/2021 03:16:17 PM MainThread INFO APRSD is up to date server.py:51
[02/13/2021 09:22:09 AM] [MainThread ] [INFO ] Checking IMAP configuration 12/07/2021 03:16:17 PM MainThread INFO APRSD Started version: 2.5.6 server.py:52
[02/13/2021 09:22:09 AM] [MainThread ] [INFO ] Checking SMTP configuration 12/07/2021 03:16:17 PM MainThread INFO Using CONFIG values: server.py:55
[02/13/2021 09:22:10 AM] [MainThread ] [INFO ] Validating 2 Email shortcuts. This can take up to 10 seconds per shortcut 12/07/2021 03:16:17 PM MainThread INFO ham.callsign = WB4BOR server.py:60
12/07/2021 03:16:17 PM MainThread INFO aprs.login = WB4BOR-12 server.py:60
12/07/2021 03:16:17 PM MainThread INFO aprs.password = XXXXXXXXXXXXXXXXXXX server.py:58
12/07/2021 03:16:17 PM MainThread INFO aprs.host = noam.aprs2.net server.py:60
12/07/2021 03:16:17 PM MainThread INFO aprs.port = 14580 server.py:60
12/07/2021 03:16:17 PM MainThread INFO aprs.logfile = /tmp/aprsd.log server.py:60
send-message send-message
@ -299,32 +198,30 @@ test messages
:: ::
└─[$] > aprsd send-message -h └─[$] > aprsd send-message -h
Usage: aprsd send-message [OPTIONS] TOCALLSIGN [COMMAND]... Usage: aprsd send-message [OPTIONS] TOCALLSIGN COMMAND...
Send a message to a callsign via APRS_IS. Send a message to a callsign via APRS_IS.
Options: Options:
--loglevel [CRITICAL|ERROR|WARNING|INFO|DEBUG] --loglevel [CRITICAL|ERROR|WARNING|INFO|DEBUG]
The log level to use for aprsd.log The log level to use for aprsd.log
[default: DEBUG] [default: INFO]
--quiet Don't log to stdout
-c, --config TEXT The aprsd config file to use for options. -c, --config TEXT The aprsd config file to use for options.
[default: ~/.config/aprsd/aprsd.yml] [default:
/Users/i530566/.config/aprsd/aprsd.yml]
--quiet Don't log to stdout
--aprs-login TEXT What callsign to send the message from. --aprs-login TEXT What callsign to send the message from.
[env var: APRS_LOGIN] [env var: APRS_LOGIN]
--aprs-password TEXT the APRS-IS password for APRS_LOGIN [env --aprs-password TEXT the APRS-IS password for APRS_LOGIN [env
var: APRS_PASSWORD] var: APRS_PASSWORD]
-n, --no-ack Don't wait for an ack, just sent it to APRS-
IS and bail. [default: False]
-w, --wait-response Wait for a response to the message?
[default: False]
--raw TEXT Send a raw message. Implies --no-ack
-h, --help Show this message and exit. -h, --help Show this message and exit.
Example output:
===============
SEND EMAIL (radio to smtp server) SEND EMAIL (radio to smtp server)
================================= =================================
@ -395,25 +292,35 @@ AND... ping, fortune, time.....
Development Development
=========== ===========
* git clone git@github.com:craigerl/aprsd.git * ``git clone git@github.com:craigerl/aprsd.git``
* cd aprsd * ``cd aprsd``
* make * ``make``
Workflow Workflow
======== ========
While working aprsd, The workflow is as follows While working aprsd, The workflow is as follows:
* Checkout a new branch to work on by running
``git checkout -b mybranch``
* Make your changes to the code
* Run Tox with the following options:
- ``tox -epep8``
- ``tox -efmt``
- ``tox -p``
* Commit your changes. This will run the pre-commit hooks which does checks too
``git commit``
* checkout a new branch to work on
* git checkout -b mybranch
* Edit code
* run tox -epep8
* run tox -efmt
* run tox -p
* git commit ( This will run the pre-commit hooks which does checks too )
* Once you are done with all of your commits, then push up the branch to * Once you are done with all of your commits, then push up the branch to
github github with:
* git push -u origin mybranch
``git push -u origin mybranch``
* Create a pull request from your branch so github tests can run and we can do * Create a pull request from your branch so github tests can run and we can do
a code review. a code review.
@ -423,21 +330,21 @@ Release
To do release to pypi: To do release to pypi:
* Tag release with * Tag release with:
git tag -v1.XX -m "New release" ``git tag -v1.XX -m "New release"``
* push release tag up * Push release tag:
git push origin master --tags ``git push origin master --tags``
* Do a test build and verify build is valid * Do a test build and verify build is valid by running:
make build ``make build``
* Once twine is happy, upload release to pypi * Once twine is happy, upload release to pypi:
make upload ``make upload``
Docker Container Docker Container
@ -455,24 +362,62 @@ the repo.
Official Build Official Build
============== ==============
docker build -t hemna6969/aprsd:latest . ``docker build -t hemna6969/aprsd:latest .``
Development Build Development Build
================= =================
docker build -t hemna6969/aprsd:latest -f Dockerfile-dev . ``docker build -t hemna6969/aprsd:latest -f Dockerfile-dev .``
Running the container Running the container
===================== =====================
There is a docker-compose.yml file that can be used to run your container. There is a ``docker-compose.yml`` file in the ``docker/`` directory
There are 2 volumes defined that can be used to store your configuration that can be used to run your container. To provide the container
and the plugins directory: /config and /plugins an ``aprsd.conf`` configuration file, change your
``docker-compose.yml`` as shown below:
If you want to install plugins at container start time, then use the ::
environment var in docker-compose.yml specified as APRS_PLUGINS
Provide a csv list of pypi installable plugins. Then make sure the plugin volumes:
python file is in your /plugins volume and the plugin will be installed at - $HOME/.config/aprsd:/config
container startup. The plugin may have dependencies that are required.
The plugin file should be copied to /plugins for loading by aprsd To install plugins at container start time, pass in a list of
comma-separated list of plugins on PyPI using the ``APRSD_PLUGINS``
environment variable in the ``docker-compose.yml`` file. Note that
version constraints may also be provided. For example:
::
environment:
- APRSD_PLUGINS=aprsd-slack-plugin>=1.0.2,aprsd-twitter-plugin
.. badges
.. |pypi| image:: https://badge.fury.io/py/aprsd.svg
:target: https://badge.fury.io/py/aprsd
.. |pytest| image:: https://github.com/craigerl/aprsd/workflows/python/badge.svg
:target: https://github.com/craigerl/aprsd/actions
.. |versions| image:: https://img.shields.io/pypi/pyversions/aprsd.svg
:target: https://pypi.org/pypi/aprsd
.. |slack| image:: https://img.shields.io/badge/slack-@hemna/aprsd-blue.svg?logo=slack
:target: https://hemna.slack.com/app_redirect?channel=C01KQSCP5RP
.. |imports| image:: https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336
:target: https://timothycrosley.github.io/isort/
.. |issues| image:: https://img.shields.io/github/issues/craigerl/aprsd
.. |commit| image:: https://img.shields.io/github/last-commit/craigerl/aprsd
.. |down| image:: https://static.pepy.tech/personalized-badge/aprsd?period=month&units=international_system&left_color=black&right_color=orange&left_text=Downloads
:target: https://pepy.tech/project/aprsd
.. links
.. _read the docs:
https://aprsd.readthedocs.io

View File

@ -23,9 +23,11 @@ beautifulsoup4
wrapt wrapt
# kiss3 uses attrs # kiss3 uses attrs
kiss3 kiss3
attrs==22.1.0 attrs
# for mobile checking # for mobile checking
user-agents user-agents
pyopenssl pyopenssl
dataclasses dataclasses
dacite2 dacite2
oslo.config
rpyc

View File

@ -1,15 +1,15 @@
# #
# This file is autogenerated by pip-compile with Python 3.9 # This file is autogenerated by pip-compile with Python 3.10
# by the following command: # by the following command:
# #
# pip-compile --annotation-style=line --resolver=backtracking requirements.in # pip-compile --annotation-style=line --resolver=backtracking requirements.in
# #
aprslib==0.7.2 # via -r requirements.in aprslib==0.7.2 # via -r requirements.in
attrs==22.1.0 # via -r requirements.in, ax253, kiss3 attrs==22.2.0 # via -r requirements.in, ax253, kiss3
ax253==0.1.5.post1 # via kiss3 ax253==0.1.5.post1 # via kiss3
beautifulsoup4==4.11.1 # via -r requirements.in beautifulsoup4==4.11.1 # via -r requirements.in
bidict==0.22.0 # via python-socketio bidict==0.22.0 # via python-socketio
bitarray==2.6.0 # via ax253, kiss3 bitarray==2.6.1 # via ax253, kiss3
certifi==2022.12.7 # via requests certifi==2022.12.7 # via requests
cffi==1.15.1 # via cryptography cffi==1.15.1 # via cryptography
charset-normalizer==2.1.1 # via requests charset-normalizer==2.1.1 # via requests
@ -19,6 +19,7 @@ commonmark==0.9.1 # via rich
cryptography==38.0.4 # via pyopenssl cryptography==38.0.4 # via pyopenssl
dacite2==2.0.0 # via -r requirements.in dacite2==2.0.0 # via -r requirements.in
dataclasses==0.6 # via -r requirements.in dataclasses==0.6 # via -r requirements.in
debtcollector==2.5.0 # via oslo-config
dnspython==2.2.1 # via eventlet dnspython==2.2.1 # via eventlet
eventlet==0.33.2 # via -r requirements.in eventlet==0.33.2 # via -r requirements.in
flask==2.1.2 # via -r requirements.in, flask-classful, flask-httpauth, flask-socketio flask==2.1.2 # via -r requirements.in, flask-classful, flask-httpauth, flask-socketio
@ -28,13 +29,17 @@ flask-socketio==5.3.2 # via -r requirements.in
greenlet==2.0.1 # via eventlet greenlet==2.0.1 # via eventlet
idna==3.4 # via requests idna==3.4 # via requests
imapclient==2.3.1 # via -r requirements.in imapclient==2.3.1 # via -r requirements.in
importlib-metadata==5.1.0 # via ax253, flask, kiss3 importlib-metadata==5.2.0 # via ax253, kiss3
itsdangerous==2.1.2 # via flask itsdangerous==2.1.2 # via flask
jinja2==3.1.2 # via click-completion, flask jinja2==3.1.2 # via click-completion, flask
kiss3==8.0.0 # via -r requirements.in kiss3==8.0.0 # via -r requirements.in
markupsafe==2.1.1 # via jinja2 markupsafe==2.1.1 # via jinja2
pbr==5.11.0 # via -r requirements.in netaddr==0.8.0 # via oslo-config
oslo-config==9.0.0 # via -r requirements.in
oslo-i18n==5.1.0 # via oslo-config
pbr==5.11.0 # via -r requirements.in, oslo-i18n, stevedore
pluggy==1.0.0 # via -r requirements.in pluggy==1.0.0 # via -r requirements.in
plumbum==1.8.0 # via rpyc
pycparser==2.21 # via cffi pycparser==2.21 # via cffi
pygments==2.13.0 # via rich pygments==2.13.0 # via rich
pyopenssl==22.1.0 # via -r requirements.in pyopenssl==22.1.0 # via -r requirements.in
@ -42,13 +47,16 @@ pyserial==3.5 # via pyserial-asyncio
pyserial-asyncio==0.6 # via kiss3 pyserial-asyncio==0.6 # via kiss3
python-engineio==4.3.4 # via python-socketio python-engineio==4.3.4 # via python-socketio
python-socketio==5.7.2 # via flask-socketio python-socketio==5.7.2 # via flask-socketio
pytz==2022.6 # via -r requirements.in pytz==2022.7 # via -r requirements.in
pyyaml==6.0 # via -r requirements.in pyyaml==6.0 # via -r requirements.in, oslo-config
requests==2.28.1 # via -r requirements.in, update-checker requests==2.28.1 # via -r requirements.in, oslo-config, update-checker
rfc3986==2.0.0 # via oslo-config
rich==12.6.0 # via -r requirements.in rich==12.6.0 # via -r requirements.in
rpyc==5.3.0 # via -r requirements.in
shellingham==1.5.0 # via click-completion shellingham==1.5.0 # via click-completion
six==1.16.0 # via -r requirements.in, click-completion, eventlet, imapclient six==1.16.0 # via -r requirements.in, click-completion, eventlet, imapclient
soupsieve==2.3.2.post1 # via beautifulsoup4 soupsieve==2.3.2.post1 # via beautifulsoup4
stevedore==4.1.1 # via oslo-config
tabulate==0.9.0 # via -r requirements.in tabulate==0.9.0 # via -r requirements.in
thesmuggler==1.0.1 # via -r requirements.in thesmuggler==1.0.1 # via -r requirements.in
ua-parser==0.16.1 # via user-agents ua-parser==0.16.1 # via user-agents
@ -56,5 +64,5 @@ update-checker==0.18.0 # via -r requirements.in
urllib3==1.26.13 # via requests urllib3==1.26.13 # via requests
user-agents==2.2.0 # via -r requirements.in user-agents==2.2.0 # via -r requirements.in
werkzeug==2.1.2 # via -r requirements.in, flask werkzeug==2.1.2 # via -r requirements.in, flask
wrapt==1.14.1 # via -r requirements.in wrapt==1.14.1 # via -r requirements.in, debtcollector
zipp==3.11.0 # via importlib-metadata zipp==3.11.0 # via importlib-metadata

View File

@ -34,6 +34,10 @@ packages =
[entry_points] [entry_points]
console_scripts = console_scripts =
aprsd = aprsd.aprsd:main aprsd = aprsd.aprsd:main
oslo.config.opts =
aprsd.conf = aprsd.conf.opts:list_opts
oslo.config.opts.defaults =
aprsd.conf = aprsd.conf:set_lib_defaults
[build_sphinx] [build_sphinx]
source-dir = docs source-dir = docs

View File

@ -1,68 +0,0 @@
import typing as t
import unittest
from unittest import mock
from click.testing import CliRunner
from aprsd import config as aprsd_config
from aprsd.aprsd import cli
from aprsd.cmds import dev # noqa
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
class TestDevTestPluginCommand(unittest.TestCase):
def _build_config(self, login=None, password=None):
config = {
"aprs": {},
"aprsd": {
"trace": False,
"watch_list": {},
},
}
if login:
config["aprs"]["login"] = login
if password:
config["aprs"]["password"] = password
return aprsd_config.Config(config)
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.logging.log.setup_logging")
def test_no_login(self, mock_logging, mock_parse_config):
"""Make sure we get an error if there is no login and config."""
runner = CliRunner()
mock_parse_config.return_value = self._build_config()
result = runner.invoke(
cli, [
"dev", "test-plugin",
"-p", "aprsd.plugins.version.VersionPlugin",
"bogus command",
],
catch_exceptions=False,
)
# rich.print(f"EXIT CODE {result.exit_code}")
# rich.print(f"Exception {result.exception}")
# rich.print(f"OUTPUT {result.output}")
assert result.exit_code == -1
assert "Must set --aprs_login or APRS_LOGIN" in result.output
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.logging.log.setup_logging")
def test_no_plugin_arg(self, mock_logging, mock_parse_config):
"""Make sure we get an error if there is no login and config."""
runner = CliRunner()
mock_parse_config.return_value = self._build_config(login="something")
result = runner.invoke(
cli, ["dev", "test-plugin", "bogus command"],
catch_exceptions=False,
)
assert result.exit_code == 2
assert "Failed to provide -p option to test a plugin" in result.output

View File

@ -3,78 +3,42 @@ import unittest
from unittest import mock from unittest import mock
from click.testing import CliRunner from click.testing import CliRunner
from oslo_config import cfg
from aprsd import config as aprsd_config from aprsd import conf # noqa : F401
from aprsd.aprsd import cli from aprsd.aprsd import cli
from aprsd.cmds import send_message # noqa from aprsd.cmds import send_message # noqa
from .. import fake
CONF = cfg.CONF
F = t.TypeVar("F", bound=t.Callable[..., t.Any]) F = t.TypeVar("F", bound=t.Callable[..., t.Any])
class TestSendMessageCommand(unittest.TestCase): class TestSendMessageCommand(unittest.TestCase):
def _build_config(self, login=None, password=None): def config_and_init(self, login=None, password=None):
config = { CONF.callsign = fake.FAKE_TO_CALLSIGN
"aprs": {}, CONF.trace_enabled = False
"aprsd": { CONF.watch_list.packet_keep_count = 1
"trace": False,
"watch_list": {},
},
}
if login: if login:
config["aprs"]["login"] = login CONF.aprs_network.login = login
if password: if password:
config["aprs"]["password"] = password CONF.aprs_network.password = password
return aprsd_config.Config(config) CONF.admin.user = "admin"
CONF.admin.password = "password"
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.logging.log.setup_logging") @mock.patch("aprsd.logging.log.setup_logging")
def test_no_login(self, mock_logging, mock_parse_config): def test_no_tocallsign(self, mock_logging):
"""Make sure we get an error if there is no login and config."""
return
runner = CliRunner()
mock_parse_config.return_value = self._build_config()
result = runner.invoke(
cli, ["send-message", "WB4BOR", "wx"],
catch_exceptions=False,
)
# rich.print(f"EXIT CODE {result.exit_code}")
# rich.print(f"Exception {result.exception}")
# rich.print(f"OUTPUT {result.output}")
assert result.exit_code == -1
assert "Must set --aprs_login or APRS_LOGIN" in result.output
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.logging.log.setup_logging")
def test_no_password(self, mock_logging, mock_parse_config):
"""Make sure we get an error if there is no password and config."""
return
runner = CliRunner()
mock_parse_config.return_value = self._build_config(login="something")
result = runner.invoke(
cli, ["send-message", "WB4BOR", "wx"],
catch_exceptions=False,
)
assert result.exit_code == -1
assert "Must set --aprs-password or APRS_PASSWORD" in result.output
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.logging.log.setup_logging")
def test_no_tocallsign(self, mock_logging, mock_parse_config):
"""Make sure we get an error if there is no tocallsign.""" """Make sure we get an error if there is no tocallsign."""
runner = CliRunner() self.config_and_init(
mock_parse_config.return_value = self._build_config(
login="something", login="something",
password="another", password="another",
) )
runner = CliRunner()
result = runner.invoke( result = runner.invoke(
cli, ["send-message"], cli, ["send-message"],
@ -83,16 +47,15 @@ class TestSendMessageCommand(unittest.TestCase):
assert result.exit_code == 2 assert result.exit_code == 2
assert "Error: Missing argument 'TOCALLSIGN'" in result.output assert "Error: Missing argument 'TOCALLSIGN'" in result.output
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.logging.log.setup_logging") @mock.patch("aprsd.logging.log.setup_logging")
def test_no_command(self, mock_logging, mock_parse_config): def test_no_command(self, mock_logging):
"""Make sure we get an error if there is no command.""" """Make sure we get an error if there is no command."""
runner = CliRunner() self.config_and_init(
mock_parse_config.return_value = self._build_config(
login="something", login="something",
password="another", password="another",
) )
runner = CliRunner()
result = runner.invoke( result = runner.invoke(
cli, ["send-message", "WB4BOR"], cli, ["send-message", "WB4BOR"],

View File

@ -5,112 +5,81 @@ from unittest import mock
from click.testing import CliRunner from click.testing import CliRunner
import flask import flask
import flask_socketio import flask_socketio
from oslo_config import cfg
from aprsd import config as aprsd_config from aprsd import conf # noqa: F401
from aprsd import packets
from aprsd.cmds import webchat # noqa from aprsd.cmds import webchat # noqa
from aprsd.packets import core from aprsd.packets import core
from .. import fake from .. import fake
CONF = cfg.CONF
F = t.TypeVar("F", bound=t.Callable[..., t.Any]) F = t.TypeVar("F", bound=t.Callable[..., t.Any])
class TestSendMessageCommand(unittest.TestCase): class TestSendMessageCommand(unittest.TestCase):
def _build_config(self, login=None, password=None): def config_and_init(self, login=None, password=None):
config = { CONF.callsign = fake.FAKE_TO_CALLSIGN
"aprs": {}, CONF.trace_enabled = False
"aprsd": { CONF.watch_list.packet_keep_count = 1
"trace": False,
"web": {
"users": {"admin": "password"},
},
"watch_list": {"packet_keep_count": 1},
},
}
if login: if login:
config["aprs"]["login"] = login CONF.aprs_network.login = login
if password: if password:
config["aprs"]["password"] = password CONF.aprs_network.password = password
return aprsd_config.Config(config) CONF.admin.user = "admin"
CONF.admin.password = "password"
@mock.patch("aprsd.config.parse_config")
def test_missing_config(self, mock_parse_config):
CliRunner()
cfg = self._build_config()
del cfg["aprsd"]["web"]["users"]
mock_parse_config.return_value = cfg
server = webchat.WebChatFlask()
self.assertRaises(
KeyError,
server.set_config, cfg,
)
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.logging.log.setup_logging") @mock.patch("aprsd.logging.log.setup_logging")
def test_init_flask(self, mock_logging, mock_parse_config): def test_init_flask(self, mock_logging):
"""Make sure we get an error if there is no login and config.""" """Make sure we get an error if there is no login and config."""
CliRunner() CliRunner()
cfg = self._build_config() self.config_and_init()
mock_parse_config.return_value = cfg
socketio, flask_app = webchat.init_flask(cfg, "DEBUG", False) socketio, flask_app = webchat.init_flask("DEBUG", False)
self.assertIsInstance(socketio, flask_socketio.SocketIO) self.assertIsInstance(socketio, flask_socketio.SocketIO)
self.assertIsInstance(flask_app, flask.Flask) self.assertIsInstance(flask_app, flask.Flask)
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.packets.tracker.PacketTrack.remove") @mock.patch("aprsd.packets.tracker.PacketTrack.remove")
@mock.patch("aprsd.cmds.webchat.socketio.emit") @mock.patch("aprsd.cmds.webchat.socketio")
def test_process_ack_packet( def test_process_ack_packet(
self, mock_parse_config, self,
mock_remove, mock_emit, mock_remove, mock_socketio,
): ):
config = self._build_config() self.config_and_init()
mock_parse_config.return_value = config mock_socketio.emit = mock.MagicMock()
packet = fake.fake_packet( packet = fake.fake_packet(
message="blah", message="blah",
msg_number=1, msg_number=1,
message_format=core.PACKET_TYPE_ACK, message_format=core.PACKET_TYPE_ACK,
) )
socketio = mock.MagicMock() socketio = mock.MagicMock()
packets.PacketList(config=config) wcp = webchat.WebChatProcessPacketThread(packet, socketio)
packets.PacketTrack(config=config)
packets.WatchList(config=config)
packets.SeenList(config=config)
wcp = webchat.WebChatProcessPacketThread(config, packet, socketio)
wcp.process_ack_packet(packet) wcp.process_ack_packet(packet)
mock_remove.called_once() mock_remove.called_once()
mock_emit.called_once() mock_socketio.called_once()
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.packets.PacketList.rx") @mock.patch("aprsd.packets.PacketList.rx")
@mock.patch("aprsd.cmds.webchat.socketio.emit") @mock.patch("aprsd.cmds.webchat.socketio")
def test_process_our_message_packet( def test_process_our_message_packet(
self, mock_parse_config, self,
mock_packet_add, mock_packet_add,
mock_emit, mock_socketio,
): ):
config = self._build_config() self.config_and_init()
mock_parse_config.return_value = config mock_socketio.emit = mock.MagicMock()
packet = fake.fake_packet( packet = fake.fake_packet(
message="blah", message="blah",
msg_number=1, msg_number=1,
message_format=core.PACKET_TYPE_MESSAGE, message_format=core.PACKET_TYPE_MESSAGE,
) )
socketio = mock.MagicMock() socketio = mock.MagicMock()
packets.PacketList(config=config) wcp = webchat.WebChatProcessPacketThread(packet, socketio)
packets.PacketTrack(config=config)
packets.WatchList(config=config)
packets.SeenList(config=config)
wcp = webchat.WebChatProcessPacketThread(config, packet, socketio)
wcp.process_our_message_packet(packet) wcp.process_our_message_packet(packet)
mock_packet_add.called_once() mock_packet_add.called_once()
mock_emit.called_once() mock_socketio.called_once()

View File

@ -1,15 +1,21 @@
from unittest import mock from unittest import mock
from oslo_config import cfg
from aprsd import conf # noqa: F401
from aprsd.plugins import fortune as fortune_plugin from aprsd.plugins import fortune as fortune_plugin
from .. import fake, test_plugin from .. import fake, test_plugin
CONF = cfg.CONF
class TestFortunePlugin(test_plugin.TestPlugin): class TestFortunePlugin(test_plugin.TestPlugin):
@mock.patch("shutil.which") @mock.patch("shutil.which")
def test_fortune_fail(self, mock_which): def test_fortune_fail(self, mock_which):
mock_which.return_value = None mock_which.return_value = None
fortune = fortune_plugin.FortunePlugin(self.config) fortune = fortune_plugin.FortunePlugin()
expected = "FortunePlugin isn't enabled" expected = "FortunePlugin isn't enabled"
packet = fake.fake_packet(message="fortune") packet = fake.fake_packet(message="fortune")
actual = fortune.filter(packet) actual = fortune.filter(packet)
@ -20,7 +26,8 @@ class TestFortunePlugin(test_plugin.TestPlugin):
def test_fortune_success(self, mock_which, mock_output): def test_fortune_success(self, mock_which, mock_output):
mock_which.return_value = "/usr/bin/games/fortune" mock_which.return_value = "/usr/bin/games/fortune"
mock_output.return_value = "Funny fortune" mock_output.return_value = "Funny fortune"
fortune = fortune_plugin.FortunePlugin(self.config) CONF.callsign = fake.FAKE_TO_CALLSIGN
fortune = fortune_plugin.FortunePlugin()
expected = "Funny fortune" expected = "Funny fortune"
packet = fake.fake_packet(message="fortune") packet = fake.fake_packet(message="fortune")

View File

@ -1,18 +1,24 @@
from unittest import mock from unittest import mock
from oslo_config import cfg
from aprsd import conf # noqa: F401
from aprsd.plugins import location as location_plugin from aprsd.plugins import location as location_plugin
from .. import fake, test_plugin from .. import fake, test_plugin
CONF = cfg.CONF
class TestLocationPlugin(test_plugin.TestPlugin): class TestLocationPlugin(test_plugin.TestPlugin):
@mock.patch("aprsd.config.Config.check_option") def test_location_not_enabled_missing_aprs_fi_key(self):
def test_location_not_enabled_missing_aprs_fi_key(self, mock_check):
# When the aprs.fi api key isn't set, then # When the aprs.fi api key isn't set, then
# the LocationPlugin will be disabled. # the LocationPlugin will be disabled.
mock_check.side_effect = Exception CONF.callsign = fake.FAKE_TO_CALLSIGN
fortune = location_plugin.LocationPlugin(self.config) CONF.aprs_fi.apiKey = None
fortune = location_plugin.LocationPlugin()
expected = "LocationPlugin isn't enabled" expected = "LocationPlugin isn't enabled"
packet = fake.fake_packet(message="location") packet = fake.fake_packet(message="location")
actual = fortune.filter(packet) actual = fortune.filter(packet)
@ -23,7 +29,8 @@ class TestLocationPlugin(test_plugin.TestPlugin):
# When the aprs.fi api key isn't set, then # When the aprs.fi api key isn't set, then
# the LocationPlugin will be disabled. # the LocationPlugin will be disabled.
mock_check.side_effect = Exception mock_check.side_effect = Exception
fortune = location_plugin.LocationPlugin(self.config) CONF.callsign = fake.FAKE_TO_CALLSIGN
fortune = location_plugin.LocationPlugin()
expected = "Failed to fetch aprs.fi location" expected = "Failed to fetch aprs.fi location"
packet = fake.fake_packet(message="location") packet = fake.fake_packet(message="location")
actual = fortune.filter(packet) actual = fortune.filter(packet)
@ -34,7 +41,8 @@ class TestLocationPlugin(test_plugin.TestPlugin):
# When the aprs.fi api key isn't set, then # When the aprs.fi api key isn't set, then
# the LocationPlugin will be disabled. # the LocationPlugin will be disabled.
mock_check.return_value = {"entries": []} mock_check.return_value = {"entries": []}
fortune = location_plugin.LocationPlugin(self.config) CONF.callsign = fake.FAKE_TO_CALLSIGN
fortune = location_plugin.LocationPlugin()
expected = "Failed to fetch aprs.fi location" expected = "Failed to fetch aprs.fi location"
packet = fake.fake_packet(message="location") packet = fake.fake_packet(message="location")
actual = fortune.filter(packet) actual = fortune.filter(packet)
@ -57,7 +65,8 @@ class TestLocationPlugin(test_plugin.TestPlugin):
} }
mock_weather.side_effect = Exception mock_weather.side_effect = Exception
mock_time.return_value = 10 mock_time.return_value = 10
fortune = location_plugin.LocationPlugin(self.config) CONF.callsign = fake.FAKE_TO_CALLSIGN
fortune = location_plugin.LocationPlugin()
expected = "KFAKE: Unknown Location 0' 10,11 0.0h ago" expected = "KFAKE: Unknown Location 0' 10,11 0.0h ago"
packet = fake.fake_packet(message="location") packet = fake.fake_packet(message="location")
actual = fortune.filter(packet) actual = fortune.filter(packet)
@ -82,7 +91,8 @@ class TestLocationPlugin(test_plugin.TestPlugin):
wx_data = {"location": {"areaDescription": expected_town}} wx_data = {"location": {"areaDescription": expected_town}}
mock_weather.return_value = wx_data mock_weather.return_value = wx_data
mock_time.return_value = 10 mock_time.return_value = 10
fortune = location_plugin.LocationPlugin(self.config) CONF.callsign = fake.FAKE_TO_CALLSIGN
fortune = location_plugin.LocationPlugin()
expected = f"KFAKE: {expected_town} 0' 10,11 0.0h ago" expected = f"KFAKE: {expected_town} 0' 10,11 0.0h ago"
packet = fake.fake_packet(message="location") packet = fake.fake_packet(message="location")
actual = fortune.filter(packet) actual = fortune.filter(packet)

View File

@ -1,14 +1,16 @@
from unittest import mock from unittest import mock
from aprsd import client from oslo_config import cfg
from aprsd import config as aprsd_config
from aprsd import packets from aprsd import client, packets
from aprsd import conf # noqa: F401
from aprsd.plugins import notify as notify_plugin from aprsd.plugins import notify as notify_plugin
from .. import fake, test_plugin from .. import fake, test_plugin
DEFAULT_WATCHLIST_CALLSIGNS = [fake.FAKE_FROM_CALLSIGN] CONF = cfg.CONF
DEFAULT_WATCHLIST_CALLSIGNS = fake.FAKE_FROM_CALLSIGN
class TestWatchListPlugin(test_plugin.TestPlugin): class TestWatchListPlugin(test_plugin.TestPlugin):
@ -16,7 +18,7 @@ class TestWatchListPlugin(test_plugin.TestPlugin):
self.fromcall = fake.FAKE_FROM_CALLSIGN self.fromcall = fake.FAKE_FROM_CALLSIGN
self.ack = 1 self.ack = 1
def _config( def config_and_init(
self, self,
watchlist_enabled=True, watchlist_enabled=True,
watchlist_alert_callsign=None, watchlist_alert_callsign=None,
@ -24,39 +26,33 @@ class TestWatchListPlugin(test_plugin.TestPlugin):
watchlist_packet_keep_count=None, watchlist_packet_keep_count=None,
watchlist_callsigns=DEFAULT_WATCHLIST_CALLSIGNS, watchlist_callsigns=DEFAULT_WATCHLIST_CALLSIGNS,
): ):
_config = aprsd_config.Config(aprsd_config.DEFAULT_CONFIG_DICT) CONF.callsign = self.fromcall
default_wl = aprsd_config.DEFAULT_CONFIG_DICT["aprsd"]["watch_list"] CONF.aprs_network.login = self.fromcall
CONF.aprs_fi.apiKey = "something"
_config["ham"]["callsign"] = self.fromcall
_config["aprsd"]["callsign"] = self.fromcall
_config["aprs"]["login"] = self.fromcall
_config["services"]["aprs.fi"]["apiKey"] = "something"
# Set the watchlist specific config options # Set the watchlist specific config options
CONF.watch_list.enabled = watchlist_enabled
_config["aprsd"]["watch_list"]["enabled"] = watchlist_enabled
if not watchlist_alert_callsign: if not watchlist_alert_callsign:
watchlist_alert_callsign = fake.FAKE_TO_CALLSIGN watchlist_alert_callsign = fake.FAKE_TO_CALLSIGN
_config["aprsd"]["watch_list"]["alert_callsign"] = watchlist_alert_callsign CONF.watch_list.alert_callsign = watchlist_alert_callsign
if not watchlist_alert_time_seconds: if not watchlist_alert_time_seconds:
watchlist_alert_time_seconds = default_wl["alert_time_seconds"] watchlist_alert_time_seconds = CONF.watch_list.alert_time_seconds
_config["aprsd"]["watch_list"]["alert_time_seconds"] = watchlist_alert_time_seconds CONF.watch_list.alert_time_seconds = watchlist_alert_time_seconds
if not watchlist_packet_keep_count: if not watchlist_packet_keep_count:
watchlist_packet_keep_count = default_wl["packet_keep_count"] watchlist_packet_keep_count = CONF.watch_list.packet_keep_count
_config["aprsd"]["watch_list"]["packet_keep_count"] = watchlist_packet_keep_count CONF.watch_list.packet_keep_count = watchlist_packet_keep_count
_config["aprsd"]["watch_list"]["callsigns"] = watchlist_callsigns CONF.watch_list.callsigns = watchlist_callsigns
return _config
class TestAPRSDWatchListPluginBase(TestWatchListPlugin): class TestAPRSDWatchListPluginBase(TestWatchListPlugin):
def test_watchlist_not_enabled(self): def test_watchlist_not_enabled(self):
config = self._config(watchlist_enabled=False) self.config_and_init(watchlist_enabled=False)
self.config_and_init(config=config) plugin = fake.FakeWatchListPlugin()
plugin = fake.FakeWatchListPlugin(self.config)
packet = fake.fake_packet( packet = fake.fake_packet(
message="version", message="version",
@ -69,9 +65,8 @@ class TestAPRSDWatchListPluginBase(TestWatchListPlugin):
@mock.patch("aprsd.client.ClientFactory", autospec=True) @mock.patch("aprsd.client.ClientFactory", autospec=True)
def test_watchlist_not_in_watchlist(self, mock_factory): def test_watchlist_not_in_watchlist(self, mock_factory):
client.factory = mock_factory client.factory = mock_factory
config = self._config() self.config_and_init()
self.config_and_init(config=config) plugin = fake.FakeWatchListPlugin()
plugin = fake.FakeWatchListPlugin(self.config)
packet = fake.fake_packet( packet = fake.fake_packet(
fromcall="FAKE", fromcall="FAKE",
@ -86,9 +81,8 @@ class TestAPRSDWatchListPluginBase(TestWatchListPlugin):
class TestNotifySeenPlugin(TestWatchListPlugin): class TestNotifySeenPlugin(TestWatchListPlugin):
def test_disabled(self): def test_disabled(self):
config = self._config(watchlist_enabled=False) self.config_and_init(watchlist_enabled=False)
self.config_and_init(config=config) plugin = notify_plugin.NotifySeenPlugin()
plugin = notify_plugin.NotifySeenPlugin(self.config)
packet = fake.fake_packet( packet = fake.fake_packet(
message="version", message="version",
@ -101,9 +95,8 @@ class TestNotifySeenPlugin(TestWatchListPlugin):
@mock.patch("aprsd.client.ClientFactory", autospec=True) @mock.patch("aprsd.client.ClientFactory", autospec=True)
def test_callsign_not_in_watchlist(self, mock_factory): def test_callsign_not_in_watchlist(self, mock_factory):
client.factory = mock_factory client.factory = mock_factory
config = self._config(watchlist_enabled=False) self.config_and_init(watchlist_enabled=False)
self.config_and_init(config=config) plugin = notify_plugin.NotifySeenPlugin()
plugin = notify_plugin.NotifySeenPlugin(self.config)
packet = fake.fake_packet( packet = fake.fake_packet(
message="version", message="version",
@ -118,12 +111,11 @@ class TestNotifySeenPlugin(TestWatchListPlugin):
def test_callsign_in_watchlist_not_old(self, mock_is_old, mock_factory): def test_callsign_in_watchlist_not_old(self, mock_is_old, mock_factory):
client.factory = mock_factory client.factory = mock_factory
mock_is_old.return_value = False mock_is_old.return_value = False
config = self._config( self.config_and_init(
watchlist_enabled=True, watchlist_enabled=True,
watchlist_callsigns=["WB4BOR"], watchlist_callsigns=["WB4BOR"],
) )
self.config_and_init(config=config) plugin = notify_plugin.NotifySeenPlugin()
plugin = notify_plugin.NotifySeenPlugin(self.config)
packet = fake.fake_packet( packet = fake.fake_packet(
fromcall="WB4BOR", fromcall="WB4BOR",
@ -139,13 +131,12 @@ class TestNotifySeenPlugin(TestWatchListPlugin):
def test_callsign_in_watchlist_old_same_alert_callsign(self, mock_is_old, mock_factory): def test_callsign_in_watchlist_old_same_alert_callsign(self, mock_is_old, mock_factory):
client.factory = mock_factory client.factory = mock_factory
mock_is_old.return_value = True mock_is_old.return_value = True
config = self._config( self.config_and_init(
watchlist_enabled=True, watchlist_enabled=True,
watchlist_alert_callsign="WB4BOR", watchlist_alert_callsign="WB4BOR",
watchlist_callsigns=["WB4BOR"], watchlist_callsigns=["WB4BOR"],
) )
self.config_and_init(config=config) plugin = notify_plugin.NotifySeenPlugin()
plugin = notify_plugin.NotifySeenPlugin(self.config)
packet = fake.fake_packet( packet = fake.fake_packet(
fromcall="WB4BOR", fromcall="WB4BOR",
@ -163,13 +154,12 @@ class TestNotifySeenPlugin(TestWatchListPlugin):
mock_is_old.return_value = True mock_is_old.return_value = True
notify_callsign = fake.FAKE_TO_CALLSIGN notify_callsign = fake.FAKE_TO_CALLSIGN
fromcall = "WB4BOR" fromcall = "WB4BOR"
config = self._config( self.config_and_init(
watchlist_enabled=True, watchlist_enabled=True,
watchlist_alert_callsign=notify_callsign, watchlist_alert_callsign=notify_callsign,
watchlist_callsigns=["WB4BOR"], watchlist_callsigns=["WB4BOR"],
) )
self.config_and_init(config=config) plugin = notify_plugin.NotifySeenPlugin()
plugin = notify_plugin.NotifySeenPlugin(self.config)
packet = fake.fake_packet( packet = fake.fake_packet(
fromcall=fromcall, fromcall=fromcall,

View File

@ -1,10 +1,16 @@
from unittest import mock from unittest import mock
from oslo_config import cfg
from aprsd import conf # noqa: F401
from aprsd.plugins import ping as ping_plugin from aprsd.plugins import ping as ping_plugin
from .. import fake, test_plugin from .. import fake, test_plugin
CONF = cfg.CONF
class TestPingPlugin(test_plugin.TestPlugin): class TestPingPlugin(test_plugin.TestPlugin):
@mock.patch("time.localtime") @mock.patch("time.localtime")
def test_ping(self, mock_time): def test_ping(self, mock_time):
@ -14,7 +20,8 @@ class TestPingPlugin(test_plugin.TestPlugin):
s = fake_time.tm_sec = 55 s = fake_time.tm_sec = 55
mock_time.return_value = fake_time mock_time.return_value = fake_time
ping = ping_plugin.PingPlugin(self.config) CONF.callsign = fake.FAKE_TO_CALLSIGN
ping = ping_plugin.PingPlugin()
packet = fake.fake_packet( packet = fake.fake_packet(
message="location", message="location",

View File

@ -1,5 +1,7 @@
from unittest import mock from unittest import mock
from oslo_config import cfg
from aprsd import packets from aprsd import packets
from aprsd.packets import tracker from aprsd.packets import tracker
from aprsd.plugins import query as query_plugin from aprsd.plugins import query as query_plugin
@ -7,11 +9,18 @@ from aprsd.plugins import query as query_plugin
from .. import fake, test_plugin from .. import fake, test_plugin
CONF = cfg.CONF
class TestQueryPlugin(test_plugin.TestPlugin): class TestQueryPlugin(test_plugin.TestPlugin):
@mock.patch("aprsd.packets.tracker.PacketTrack.flush") @mock.patch("aprsd.packets.tracker.PacketTrack.flush")
def test_query_flush(self, mock_flush): def test_query_flush(self, mock_flush):
packet = fake.fake_packet(message="!delete") packet = fake.fake_packet(message="!delete")
query = query_plugin.QueryPlugin(self.config) CONF.callsign = fake.FAKE_TO_CALLSIGN
CONF.save_enabled = True
CONF.query_plugin.callsign = fake.FAKE_FROM_CALLSIGN
query = query_plugin.QueryPlugin()
query.enabled = True
expected = "Deleted ALL pending msgs." expected = "Deleted ALL pending msgs."
actual = query.filter(packet) actual = query.filter(packet)
@ -20,10 +29,13 @@ class TestQueryPlugin(test_plugin.TestPlugin):
@mock.patch("aprsd.packets.tracker.PacketTrack.restart_delayed") @mock.patch("aprsd.packets.tracker.PacketTrack.restart_delayed")
def test_query_restart_delayed(self, mock_restart): def test_query_restart_delayed(self, mock_restart):
CONF.callsign = fake.FAKE_TO_CALLSIGN
CONF.save_enabled = True
CONF.query_plugin.callsign = fake.FAKE_FROM_CALLSIGN
track = tracker.PacketTrack() track = tracker.PacketTrack()
track.data = {} track.data = {}
packet = fake.fake_packet(message="!4") packet = fake.fake_packet(message="!4")
query = query_plugin.QueryPlugin(self.config) query = query_plugin.QueryPlugin()
expected = "No pending msgs to resend" expected = "No pending msgs to resend"
actual = query.filter(packet) actual = query.filter(packet)

View File

@ -1,5 +1,6 @@
from unittest import mock from unittest import mock
from oslo_config import cfg
import pytz import pytz
from aprsd.plugins import time as time_plugin from aprsd.plugins import time as time_plugin
@ -8,6 +9,9 @@ from aprsd.utils import fuzzy
from .. import fake, test_plugin from .. import fake, test_plugin
CONF = cfg.CONF
class TestTimePlugins(test_plugin.TestPlugin): class TestTimePlugins(test_plugin.TestPlugin):
@mock.patch("aprsd.plugins.time.TimePlugin._get_local_tz") @mock.patch("aprsd.plugins.time.TimePlugin._get_local_tz")
@ -25,7 +29,8 @@ class TestTimePlugins(test_plugin.TestPlugin):
h = int(local_t.strftime("%H")) h = int(local_t.strftime("%H"))
m = int(local_t.strftime("%M")) m = int(local_t.strftime("%M"))
fake_time.tm_sec = 13 fake_time.tm_sec = 13
time = time_plugin.TimePlugin(self.config) CONF.callsign = fake.FAKE_TO_CALLSIGN
time = time_plugin.TimePlugin()
packet = fake.fake_packet( packet = fake.fake_packet(
message="location", message="location",

View File

@ -1,4 +1,4 @@
from unittest import mock from oslo_config import cfg
import aprsd import aprsd
from aprsd.plugins import version as version_plugin from aprsd.plugins import version as version_plugin
@ -6,11 +6,16 @@ from aprsd.plugins import version as version_plugin
from .. import fake, test_plugin from .. import fake, test_plugin
CONF = cfg.CONF
class TestVersionPlugin(test_plugin.TestPlugin): class TestVersionPlugin(test_plugin.TestPlugin):
@mock.patch("aprsd.plugin.PluginManager.get_plugins")
def test_version(self, mock_get_plugins): def test_version(self):
expected = f"APRSD ver:{aprsd.__version__} uptime:00:00:00" expected = f"APRSD ver:{aprsd.__version__} uptime:00:00:00"
version = version_plugin.VersionPlugin(self.config) CONF.callsign = fake.FAKE_TO_CALLSIGN
version = version_plugin.VersionPlugin()
version.enabled = True
packet = fake.fake_packet( packet = fake.fake_packet(
message="No", message="No",

View File

@ -1,18 +1,24 @@
from unittest import mock from unittest import mock
from oslo_config import cfg
from aprsd import conf # noqa: F401
from aprsd.plugins import weather as weather_plugin from aprsd.plugins import weather as weather_plugin
from .. import fake, test_plugin from .. import fake, test_plugin
CONF = cfg.CONF
class TestUSWeatherPluginPlugin(test_plugin.TestPlugin): class TestUSWeatherPluginPlugin(test_plugin.TestPlugin):
@mock.patch("aprsd.config.Config.check_option") def test_not_enabled_missing_aprs_fi_key(self):
def test_not_enabled_missing_aprs_fi_key(self, mock_check):
# When the aprs.fi api key isn't set, then # When the aprs.fi api key isn't set, then
# the LocationPlugin will be disabled. # the LocationPlugin will be disabled.
mock_check.side_effect = Exception CONF.aprs_fi.apiKey = None
wx = weather_plugin.USWeatherPlugin(self.config) CONF.callsign = fake.FAKE_TO_CALLSIGN
wx = weather_plugin.USWeatherPlugin()
expected = "USWeatherPlugin isn't enabled" expected = "USWeatherPlugin isn't enabled"
packet = fake.fake_packet(message="weather") packet = fake.fake_packet(message="weather")
actual = wx.filter(packet) actual = wx.filter(packet)
@ -23,7 +29,9 @@ class TestUSWeatherPluginPlugin(test_plugin.TestPlugin):
# When the aprs.fi api key isn't set, then # When the aprs.fi api key isn't set, then
# the Plugin will be disabled. # the Plugin will be disabled.
mock_check.side_effect = Exception mock_check.side_effect = Exception
wx = weather_plugin.USWeatherPlugin(self.config) CONF.aprs_fi.apiKey = "abc123"
CONF.callsign = fake.FAKE_TO_CALLSIGN
wx = weather_plugin.USWeatherPlugin()
expected = "Failed to fetch aprs.fi location" expected = "Failed to fetch aprs.fi location"
packet = fake.fake_packet(message="weather") packet = fake.fake_packet(message="weather")
actual = wx.filter(packet) actual = wx.filter(packet)
@ -34,7 +42,10 @@ class TestUSWeatherPluginPlugin(test_plugin.TestPlugin):
# When the aprs.fi api key isn't set, then # When the aprs.fi api key isn't set, then
# the Plugin will be disabled. # the Plugin will be disabled.
mock_check.return_value = {"entries": []} mock_check.return_value = {"entries": []}
wx = weather_plugin.USWeatherPlugin(self.config) CONF.aprs_fi.apiKey = "abc123"
CONF.callsign = fake.FAKE_TO_CALLSIGN
wx = weather_plugin.USWeatherPlugin()
wx.enabled = True
expected = "Failed to fetch aprs.fi location" expected = "Failed to fetch aprs.fi location"
packet = fake.fake_packet(message="weather") packet = fake.fake_packet(message="weather")
actual = wx.filter(packet) actual = wx.filter(packet)
@ -55,7 +66,10 @@ class TestUSWeatherPluginPlugin(test_plugin.TestPlugin):
], ],
} }
mock_weather.side_effect = Exception mock_weather.side_effect = Exception
wx = weather_plugin.USWeatherPlugin(self.config) CONF.aprs_fi.apiKey = "abc123"
CONF.callsign = fake.FAKE_TO_CALLSIGN
wx = weather_plugin.USWeatherPlugin()
wx.enabled = True
expected = "Unable to get weather" expected = "Unable to get weather"
packet = fake.fake_packet(message="weather") packet = fake.fake_packet(message="weather")
actual = wx.filter(packet) actual = wx.filter(packet)
@ -83,7 +97,10 @@ class TestUSWeatherPluginPlugin(test_plugin.TestPlugin):
}, },
"time": {"startPeriodName": ["ignored", "sometime"]}, "time": {"startPeriodName": ["ignored", "sometime"]},
} }
wx = weather_plugin.USWeatherPlugin(self.config) CONF.aprs_fi.apiKey = "abc123"
CONF.callsign = fake.FAKE_TO_CALLSIGN
wx = weather_plugin.USWeatherPlugin()
wx.enabled = True
expected = "400F(10F/11F) test. sometime, another." expected = "400F(10F/11F) test. sometime, another."
packet = fake.fake_packet(message="weather") packet = fake.fake_packet(message="weather")
actual = wx.filter(packet) actual = wx.filter(packet)
@ -92,12 +109,11 @@ class TestUSWeatherPluginPlugin(test_plugin.TestPlugin):
class TestUSMetarPlugin(test_plugin.TestPlugin): class TestUSMetarPlugin(test_plugin.TestPlugin):
@mock.patch("aprsd.config.Config.check_option") def test_not_enabled_missing_aprs_fi_key(self):
def test_not_enabled_missing_aprs_fi_key(self, mock_check):
# When the aprs.fi api key isn't set, then # When the aprs.fi api key isn't set, then
# the LocationPlugin will be disabled. # the LocationPlugin will be disabled.
mock_check.side_effect = Exception CONF.aprs_fi.apiKey = None
wx = weather_plugin.USMetarPlugin(self.config) wx = weather_plugin.USMetarPlugin()
expected = "USMetarPlugin isn't enabled" expected = "USMetarPlugin isn't enabled"
packet = fake.fake_packet(message="metar") packet = fake.fake_packet(message="metar")
actual = wx.filter(packet) actual = wx.filter(packet)
@ -108,7 +124,10 @@ class TestUSMetarPlugin(test_plugin.TestPlugin):
# When the aprs.fi api key isn't set, then # When the aprs.fi api key isn't set, then
# the Plugin will be disabled. # the Plugin will be disabled.
mock_check.side_effect = Exception mock_check.side_effect = Exception
wx = weather_plugin.USMetarPlugin(self.config) CONF.aprs_fi.apiKey = "abc123"
CONF.callsign = fake.FAKE_TO_CALLSIGN
wx = weather_plugin.USMetarPlugin()
wx.enabled = True
expected = "Failed to fetch aprs.fi location" expected = "Failed to fetch aprs.fi location"
packet = fake.fake_packet(message="metar") packet = fake.fake_packet(message="metar")
actual = wx.filter(packet) actual = wx.filter(packet)
@ -119,7 +138,10 @@ class TestUSMetarPlugin(test_plugin.TestPlugin):
# When the aprs.fi api key isn't set, then # When the aprs.fi api key isn't set, then
# the Plugin will be disabled. # the Plugin will be disabled.
mock_check.return_value = {"entries": []} mock_check.return_value = {"entries": []}
wx = weather_plugin.USMetarPlugin(self.config) CONF.aprs_fi.apiKey = "abc123"
CONF.callsign = fake.FAKE_TO_CALLSIGN
wx = weather_plugin.USMetarPlugin()
wx.enabled = True
expected = "Failed to fetch aprs.fi location" expected = "Failed to fetch aprs.fi location"
packet = fake.fake_packet(message="metar") packet = fake.fake_packet(message="metar")
actual = wx.filter(packet) actual = wx.filter(packet)
@ -128,7 +150,10 @@ class TestUSMetarPlugin(test_plugin.TestPlugin):
@mock.patch("aprsd.plugin_utils.get_weather_gov_metar") @mock.patch("aprsd.plugin_utils.get_weather_gov_metar")
def test_gov_metar_fetch_fails(self, mock_metar): def test_gov_metar_fetch_fails(self, mock_metar):
mock_metar.side_effect = Exception mock_metar.side_effect = Exception
wx = weather_plugin.USMetarPlugin(self.config) CONF.aprs_fi.apiKey = "abc123"
CONF.callsign = fake.FAKE_TO_CALLSIGN
wx = weather_plugin.USMetarPlugin()
wx.enabled = True
expected = "Unable to find station METAR" expected = "Unable to find station METAR"
packet = fake.fake_packet(message="metar KPAO") packet = fake.fake_packet(message="metar KPAO")
actual = wx.filter(packet) actual = wx.filter(packet)
@ -141,7 +166,10 @@ class TestUSMetarPlugin(test_plugin.TestPlugin):
text = '{"properties": {"rawMessage": "BOGUSMETAR"}}' text = '{"properties": {"rawMessage": "BOGUSMETAR"}}'
mock_metar.return_value = Response() mock_metar.return_value = Response()
wx = weather_plugin.USMetarPlugin(self.config) CONF.aprs_fi.apiKey = "abc123"
CONF.callsign = fake.FAKE_TO_CALLSIGN
wx = weather_plugin.USMetarPlugin()
wx.enabled = True
expected = "BOGUSMETAR" expected = "BOGUSMETAR"
packet = fake.fake_packet(message="metar KPAO") packet = fake.fake_packet(message="metar KPAO")
actual = wx.filter(packet) actual = wx.filter(packet)
@ -169,7 +197,10 @@ class TestUSMetarPlugin(test_plugin.TestPlugin):
} }
mock_metar.return_value = Response() mock_metar.return_value = Response()
wx = weather_plugin.USMetarPlugin(self.config) CONF.aprs_fi.apiKey = "abc123"
CONF.callsign = fake.FAKE_TO_CALLSIGN
wx = weather_plugin.USMetarPlugin()
wx.enabled = True
expected = "BOGUSMETAR" expected = "BOGUSMETAR"
packet = fake.fake_packet(message="metar") packet = fake.fake_packet(message="metar")
actual = wx.filter(packet) actual = wx.filter(packet)

View File

@ -1,25 +1,32 @@
import unittest import unittest
from oslo_config import cfg
from aprsd import conf # noqa: F401
from aprsd.plugins import email from aprsd.plugins import email
CONF = cfg.CONF
class TestEmail(unittest.TestCase): class TestEmail(unittest.TestCase):
def test_get_email_from_shortcut(self): def test_get_email_from_shortcut(self):
config = {"aprsd": {"email": {"shortcuts": {}}}} CONF.email_plugin.shortcuts = None
email_address = "something@something.com" email_address = "something@something.com"
addr = f"-{email_address}" addr = f"-{email_address}"
actual = email.get_email_from_shortcut(config, addr) actual = email.get_email_from_shortcut(addr)
self.assertEqual(addr, actual) self.assertEqual(addr, actual)
config = {"aprsd": {"email": {"nothing": "nothing"}}} CONF.email_plugin.shortcuts = None
actual = email.get_email_from_shortcut(config, addr) actual = email.get_email_from_shortcut(addr)
self.assertEqual(addr, actual) self.assertEqual(addr, actual)
config = {"aprsd": {"email": {"shortcuts": {"not_used": "empty"}}}} CONF.email_plugin.shortcuts = None
actual = email.get_email_from_shortcut(config, addr) actual = email.get_email_from_shortcut(addr)
self.assertEqual(addr, actual) self.assertEqual(addr, actual)
config = {"aprsd": {"email": {"shortcuts": {"-wb": email_address}}}} CONF.email_plugin.email_shortcuts = ["wb=something@something.com"]
short = "-wb" email.shortcuts_dict = None
actual = email.get_email_from_shortcut(config, short) short = "wb"
actual = email.get_email_from_shortcut(short)
self.assertEqual(email_address, actual) self.assertEqual(email_address, actual)

View File

@ -17,6 +17,6 @@ class TestMain(unittest.TestCase):
"""Test to make sure we fail.""" """Test to make sure we fail."""
imap_mock.return_value = None imap_mock.return_value = None
smtp_mock.return_value = {"smaiof": "fire"} smtp_mock.return_value = {"smaiof": "fire"}
config = mock.MagicMock() mock.MagicMock()
email.validate_email_config(config, True) email.validate_email_config(True)

View File

@ -1,7 +1,9 @@
import unittest import unittest
from unittest import mock from unittest import mock
from aprsd import config as aprsd_config from oslo_config import cfg
from aprsd import conf # noqa: F401
from aprsd import packets from aprsd import packets
from aprsd import plugin as aprsd_plugin from aprsd import plugin as aprsd_plugin
from aprsd import plugins, stats from aprsd import plugins, stats
@ -10,6 +12,9 @@ from aprsd.packets import core
from . import fake from . import fake
CONF = cfg.CONF
class TestPluginManager(unittest.TestCase): class TestPluginManager(unittest.TestCase):
def setUp(self) -> None: def setUp(self) -> None:
@ -21,34 +26,26 @@ class TestPluginManager(unittest.TestCase):
aprsd_plugin.PluginManager._instance = None aprsd_plugin.PluginManager._instance = None
def config_and_init(self): def config_and_init(self):
self.config = aprsd_config.Config(aprsd_config.DEFAULT_CONFIG_DICT) CONF.callsign = self.fromcall
self.config["ham"]["callsign"] = self.fromcall CONF.aprs_network.login = fake.FAKE_TO_CALLSIGN
self.config["aprs"]["login"] = fake.FAKE_TO_CALLSIGN CONF.aprs_fi.apiKey = "something"
self.config["services"]["aprs.fi"]["apiKey"] = "something" CONF.enabled_plugins = "aprsd.plugins.ping.PingPlugin"
self.config["aprsd"]["enabled_plugins"] = [ CONF.enable_save = False
"aprsd.plugins.ping.PingPlugin",
]
print(self.config)
def test_init_no_config(self):
pm = aprsd_plugin.PluginManager()
self.assertEqual(None, pm.config)
def test_init_with_config(self):
pm = aprsd_plugin.PluginManager(self.config)
self.assertEqual(self.config, pm.config)
def test_get_plugins_no_plugins(self): def test_get_plugins_no_plugins(self):
pm = aprsd_plugin.PluginManager(self.config) CONF.enabled_plugins = []
pm = aprsd_plugin.PluginManager()
plugin_list = pm.get_plugins() plugin_list = pm.get_plugins()
self.assertEqual([], plugin_list) self.assertEqual([], plugin_list)
def test_get_plugins_with_plugins(self): def test_get_plugins_with_plugins(self):
pm = aprsd_plugin.PluginManager(self.config) CONF.enabled_plugins = ["aprsd.plugins.ping.PingPlugin"]
pm = aprsd_plugin.PluginManager()
plugin_list = pm.get_plugins() plugin_list = pm.get_plugins()
self.assertEqual([], plugin_list) self.assertEqual([], plugin_list)
pm.setup_plugins() pm.setup_plugins()
plugin_list = pm.get_plugins() plugin_list = pm.get_plugins()
print(plugin_list)
self.assertIsInstance(plugin_list, list) self.assertIsInstance(plugin_list, list)
self.assertIsInstance( self.assertIsInstance(
plugin_list[0], plugin_list[0],
@ -59,7 +56,7 @@ class TestPluginManager(unittest.TestCase):
) )
def test_get_watchlist_plugins(self): def test_get_watchlist_plugins(self):
pm = aprsd_plugin.PluginManager(self.config) pm = aprsd_plugin.PluginManager()
plugin_list = pm.get_plugins() plugin_list = pm.get_plugins()
self.assertEqual([], plugin_list) self.assertEqual([], plugin_list)
pm.setup_plugins() pm.setup_plugins()
@ -68,7 +65,8 @@ class TestPluginManager(unittest.TestCase):
self.assertEqual(0, len(plugin_list)) self.assertEqual(0, len(plugin_list))
def test_get_message_plugins(self): def test_get_message_plugins(self):
pm = aprsd_plugin.PluginManager(self.config) CONF.enabled_plugins = ["aprsd.plugins.ping.PingPlugin"]
pm = aprsd_plugin.PluginManager()
plugin_list = pm.get_plugins() plugin_list = pm.get_plugins()
self.assertEqual([], plugin_list) self.assertEqual([], plugin_list)
pm.setup_plugins() pm.setup_plugins()
@ -98,27 +96,19 @@ class TestPlugin(unittest.TestCase):
packets.PacketTrack._instance = None packets.PacketTrack._instance = None
self.config = None self.config = None
def config_and_init(self, config=None): def config_and_init(self):
if not config: CONF.callsign = self.fromcall
self.config = aprsd_config.Config(aprsd_config.DEFAULT_CONFIG_DICT) CONF.aprs_network.login = fake.FAKE_TO_CALLSIGN
self.config["ham"]["callsign"] = self.fromcall CONF.aprs_fi.apiKey = "something"
self.config["aprs"]["login"] = fake.FAKE_TO_CALLSIGN CONF.enabled_plugins = "aprsd.plugins.ping.PingPlugin"
self.config["services"]["aprs.fi"]["apiKey"] = "something" CONF.enable_save = False
else:
self.config = config
# Inintialize the stats object with the config
stats.APRSDStats(self.config)
packets.WatchList(config=self.config)
packets.SeenList(config=self.config)
packets.PacketTrack(config=self.config)
class TestPluginBase(TestPlugin): class TestPluginBase(TestPlugin):
@mock.patch.object(fake.FakeBaseNoThreadsPlugin, "process") @mock.patch.object(fake.FakeBaseNoThreadsPlugin, "process")
def test_base_plugin_no_threads(self, mock_process): def test_base_plugin_no_threads(self, mock_process):
p = fake.FakeBaseNoThreadsPlugin(self.config) p = fake.FakeBaseNoThreadsPlugin()
expected = [] expected = []
actual = p.create_threads() actual = p.create_threads()
@ -139,19 +129,20 @@ class TestPluginBase(TestPlugin):
@mock.patch.object(fake.FakeBaseThreadsPlugin, "create_threads") @mock.patch.object(fake.FakeBaseThreadsPlugin, "create_threads")
def test_base_plugin_threads_created(self, mock_create): def test_base_plugin_threads_created(self, mock_create):
p = fake.FakeBaseThreadsPlugin(self.config) p = fake.FakeBaseThreadsPlugin()
mock_create.assert_called_once() mock_create.assert_called_once()
p.stop_threads() p.stop_threads()
def test_base_plugin_threads(self): def test_base_plugin_threads(self):
p = fake.FakeBaseThreadsPlugin(self.config) p = fake.FakeBaseThreadsPlugin()
actual = p.create_threads() actual = p.create_threads()
self.assertTrue(isinstance(actual, fake.FakeThread)) self.assertTrue(isinstance(actual, fake.FakeThread))
p.stop_threads() p.stop_threads()
@mock.patch.object(fake.FakeRegexCommandPlugin, "process") @mock.patch.object(fake.FakeRegexCommandPlugin, "process")
def test_regex_base_not_called(self, mock_process): def test_regex_base_not_called(self, mock_process):
p = fake.FakeRegexCommandPlugin(self.config) CONF.callsign = fake.FAKE_TO_CALLSIGN
p = fake.FakeRegexCommandPlugin()
packet = fake.fake_packet(message="a") packet = fake.fake_packet(message="a")
expected = None expected = None
actual = p.filter(packet) actual = p.filter(packet)
@ -165,32 +156,32 @@ class TestPluginBase(TestPlugin):
mock_process.assert_not_called() mock_process.assert_not_called()
packet = fake.fake_packet( packet = fake.fake_packet(
message="F",
message_format=core.PACKET_TYPE_MICE, message_format=core.PACKET_TYPE_MICE,
) )
expected = None expected = packets.NULL_MESSAGE
actual = p.filter(packet) actual = p.filter(packet)
self.assertEqual(expected, actual) self.assertEqual(expected, actual)
mock_process.assert_not_called() mock_process.assert_not_called()
packet = fake.fake_packet( packet = fake.fake_packet(
message="f",
message_format=core.PACKET_TYPE_ACK, message_format=core.PACKET_TYPE_ACK,
) )
expected = None expected = packets.NULL_MESSAGE
actual = p.filter(packet) actual = p.filter(packet)
self.assertEqual(expected, actual) self.assertEqual(expected, actual)
mock_process.assert_not_called() mock_process.assert_not_called()
@mock.patch.object(fake.FakeRegexCommandPlugin, "process") @mock.patch.object(fake.FakeRegexCommandPlugin, "process")
def test_regex_base_assert_called(self, mock_process): def test_regex_base_assert_called(self, mock_process):
p = fake.FakeRegexCommandPlugin(self.config) CONF.callsign = fake.FAKE_TO_CALLSIGN
p = fake.FakeRegexCommandPlugin()
packet = fake.fake_packet(message="f") packet = fake.fake_packet(message="f")
p.filter(packet) p.filter(packet)
mock_process.assert_called_once() mock_process.assert_called_once()
def test_regex_base_process_called(self): def test_regex_base_process_called(self):
p = fake.FakeRegexCommandPlugin(self.config) CONF.callsign = fake.FAKE_TO_CALLSIGN
p = fake.FakeRegexCommandPlugin()
packet = fake.fake_packet(message="f") packet = fake.fake_packet(message="f")
expected = fake.FAKE_MESSAGE_TEXT expected = fake.FAKE_MESSAGE_TEXT