1
0
mirror of https://github.com/craigerl/aprsd.git synced 2026-08-18 17:44:02 -04:00

Compare commits

..

71 Commits

Author SHA1 Message Date
hemna b2f95b0f4e Updated Changelog for 3.1.2 2023-08-15 15:25:01 -04:00
hemna ae9e4d31ad Added support for ThirdParty packet types
The kiss clients now detect if the incomming packet is a third party
packet and then sends up the subpacket instead of the encapsulated
packet up to the consumer.
2023-08-15 14:24:03 -04:00
hemna 65a5a90458 Disable the Send GPS Beacon button
This patch disables the 'Send GPS Beacon' button for the webchat
command if the browser isn't connected over https
2023-08-14 18:45:13 -04:00
hemna 182887c20a Removed adhoc ssl support in webchat
This immediately breaks the beacon button.
This patch removes the dep for pyopenssl and cryptography
so that aprsd install on the rpi.

Unfortunately in order for the web page to get the Lat/Lon, the
browser must be connected over SSL.  Will have to create a workaround
for this later.
2023-08-14 18:34:25 -04:00
hemna f228144f4b Updated Changelog for v3.1.1 2023-08-07 13:01:52 -04:00
hemna db9e1d23d1 Fixed pep8 failures 2023-08-07 11:07:01 -04:00
hemna 986df391b2 re-enable USWeatherPlugin to use mapClick
The old MApClick.php api seems to work...re-enabling
2023-07-31 21:53:02 -04:00
hemna 3994235380 Merge pull request #128 from craigerl/fix_kiss
Fix sending packets over KISS interface
2023-07-28 18:08:54 -04:00
hemna 9ebf2f9a30 Fix sending packets over KISS interface
The KISS client sends the path as part of the headers, so we had
to strip out the path from the payload of each message so the path
wouldn't get listed twice.
2023-07-28 17:25:06 -04:00
hemna 011cfc55e1 Use config web_ip for running admin ui from module
When running the web admin interface with
'python -m aprsd.wsgi' the Flask app global now uses
the web_ip config entry for listening.  Also disabled
debug output.
2023-07-26 08:47:22 -04:00
hemna e0c3c5cbbf remove loop log 2023-07-25 20:45:55 -04:00
hemna 26f354b3a9 Max out the client reconnect backoff to 5
This patch adjusts the backoff mechanism for aprs client
reconnect to a max backoff sleep of 5 seconds.   This prevents
an exponential backoff when connection retrying.
2023-07-24 17:03:29 -04:00
hemna 922a6dbb35 Merge pull request #125 from craigerl/update-Dockerfile
Update the Dockerfile
2023-07-24 14:34:06 -04:00
hemna d03c4fc096 Update the Dockerfile
This updates the main Dockerfile to be the same as the
Dockerfile-dev other than using the official pypi package for
aprsd.
2023-07-24 11:36:28 -04:00
hemna dfd3688d8f Changelog updates for v3.1.0
This patch is an update to the Changelog for the
3.1.0 release.
2023-07-24 11:22:53 -04:00
hemna c7d629f88a Use CONF.admin.web_port for single launch web admin
This patch changes the non uwsgi launch of the admin page
to use the config for the web port
2023-07-24 09:39:16 -04:00
hemna 099b87e250 Fixed sio namespace registration 2023-07-23 20:22:48 -04:00
hemna 1ab9c3fee4 Update Dockerfile-dev to include uwsgi 2023-07-24 00:13:28 +00:00
hemna 8891cd3002 Merge pull request #124 from craigerl/wsgi-rework
replacement of flask-socketio with python-socketio
2023-07-23 19:45:51 -04:00
hemna 4664ead9e7 Fixed pep8 2023-07-23 19:34:02 -04:00
hemna e51a501544 change port to 8000 2023-07-23 19:19:55 -04:00
hemna 89576a3c43 replacement of flask-socketio with python-socketio
This patch starts the work to replace flask-socketio with
python-socketio so that uwsgi can be used instead of gunicorn.
uwsgi can support websockets.

Have to rework webchat command next
2023-07-23 18:54:23 -04:00
hemna 5383b698ea Change how fetch-stats gets it's defaults
The defaults come from the aprsd.conf CONF attributes now.
2023-07-22 17:05:11 -04:00
hemna cbef93b327 Ensure fetch-stats ip is a string 2023-07-22 16:41:54 -04:00
hemna 6ae55fc9a1 Add info logging for rpc server calls 2023-07-20 16:43:31 -04:00
hemna 588e140a7f updated wsgi config default /config/aprsd.conf
This patch changes wsgi.py to default to /config/aprsd.conf

It's assumed that this will be used as a docker container
2023-07-20 15:59:44 -04:00
hemna d251a2727a Merge pull request #123 from craigerl/flask-update
Remove flask pinning
2023-07-20 15:10:07 -04:00
hemna d3a93b735d Added timing after each thread loop
This is to help keep track of which non-blocking threads are still
alive.

The RPC Server thread blocks, so the time will always increase.
2023-07-20 14:44:46 -04:00
hemna fa452cc773 Update docker bin/admin.sh
This patch uses the wsgi.py instead of admin_Web.py
2023-07-20 14:34:32 -04:00
hemna 6a6e854caf Removed flask-classful from webchat
This patch removed the dependency on flask-classful.  This required
making all of the flask web routing non class based.

This patch also changes the aprsis class to allow retries for failed
connections when the aprsis servers are full and not responding to
login requests.
2023-07-20 14:34:31 -04:00
hemna e1183a7e30 Remove flask pinning
Also removed need for flask-classful. Created new
aprsd/wsgi.py for the web admin interface.
2023-07-20 14:34:31 -04:00
hemna 5723e3a77b removed linux/arm/v8 2023-07-20 14:33:59 -04:00
hemna dee73c1060 Update master build to include linux/arm/v8 2023-07-17 22:08:00 +00:00
hemna d8318f2ae2 Update Dockerfile-dev to fix plugin permissions
This patch changes the user creation to include creating
a home directory so the plugin install installs those plugins
as a --user option.
2023-07-17 21:57:39 +00:00
hemna 2825cac446 Merge pull request #122 from craigerl/crypto-upgrade
Update requirements for upgraded cryptography
2023-07-17 16:56:30 -04:00
hemna fa6e738a20 update manual build github 2023-07-17 13:05:31 -04:00
hemna 0c179005ee Update requirements for upgraded cryptography
This patch updates the requirements.in to remove the
pinning to cryptography 38.0.1.  Lets see if the docker
images build.
2023-07-17 11:15:50 -04:00
hemna ad004633de Added more libs for Dockerfile-dev 2023-07-17 14:09:37 +00:00
hemna ccd564a52e Replace Dockerfile-dev with python3 slim 2023-07-17 13:28:34 +00:00
hemna 35d41582ee Moved logging to log for wsgi.py
Added wsgi.py to be used with gunicorn to start aprsd's web admin
interface.

gunicorn -b :8080 "aprsd.wsgi:app"
2023-07-16 16:32:39 -04:00
hemna 565ffe3f72 Changed weather plugin regex pattern
The weather plugins used to match on w, but now require wx
2023-07-15 18:22:24 -04:00
hemna 0bd11d05c6 Limit the float values to 3 decimal places 2023-07-14 11:35:32 -04:00
hemna 62eff8645d Merge pull request #119 from craigerl/wx-fixes
Fixed rain numbers from aprslib
2023-07-14 11:18:11 -04:00
hemna aa547cbef5 Fixed rain numbers from aprslib 2023-07-14 10:42:36 -04:00
hemna 7f2aba702a Fixed rpc client initialization 2023-07-13 14:58:12 -04:00
hemna 63bf82aab5 Fix in for aprslib issue #80
aprslib incorrectly decodes weather packets and doesn't provide
wind_speed or wind_direction from the CSE/SPD 7 bytes in the APRS
packet.  This patch puts a temporary fix in place until the
aprslib pull request lands and is released.

https://github.com/rossengeorgiev/aprs-python/issues/80

https://github.com/rossengeorgiev/aprs-python/pull/81
2023-07-13 14:35:55 -04:00
hemna bba7b68112 Try and fix Dockerfile-dev 2023-07-10 16:34:14 +00:00
hemna 005675cb46 Fixed pep8 errors 2023-07-10 11:01:41 -04:00
hemna 191e1ff552 Populate stats object with threads info
This patch adds the thread names and state to the stats object
so the aprsd fetch-stats command can show it.
2023-07-10 10:44:24 -04:00
hemna 0a14b07fae added counts to the fetch-stats table 2023-07-09 21:29:29 -04:00
hemna b2e621da4b Added the fetch-stats command
You can now fetch and view the stats of a live running aprsd server
if it has enabled the rpc server in the config file's rpc_settings
block.
You just have to match the magic word as specified in the config file to
authorize against the rpc server.

aprsd fetch-stats --ip-address <ip of aprsd> --port <port> --magic-word
<magic word>
2023-07-09 21:06:57 -04:00
hemna fe0d71de4d Replace ratelimiter with rush
This patch replaces the ratelimiter library with rush for rate limiting
as the ratelimiter package doesn't work with python 3.11.

This patch also refactors the flask.pu to admin_web.py and
aprsd.py to main.py
2023-07-08 17:30:22 -04:00
hemna 9b944142bd Added some utilities to Dockerfile-dev
This patch adds telnet, sudo and vim to the development
Dockerfile-dev file for testing aprsd in a container
2023-06-22 15:51:45 -04:00
hemna b172c692a1 add arm64 for manual github build 2023-06-22 10:44:08 -04:00
hemna 311cebaf27 Added manual master build 2023-06-22 10:08:28 -04:00
hemna f4d60357ee Update master-build.yml
undo
2023-06-22 10:04:59 -04:00
hemna 09a0c4cb02 Add github manual trigger for master build 2023-06-22 10:03:39 -04:00
hemna 80b85e648f Fixed unit tests for Location plugin 2023-06-22 09:06:55 -04:00
hemna 9931c8a6c5 USe new tox and update githubworkflows
This patch updates tox to the latest and updates the github workflows
to use tox-gh, which is claimed to work with github parallel tox runs
2023-06-22 07:58:35 -04:00
hemna 319969cc08 Updated requirements 2023-06-21 19:12:38 -04:00
hemna da20ff038b force tox to 4.3.5 2023-06-21 19:09:26 -04:00
hemna 15bf3710d2 Update github workflows
removed building for arm64
2023-06-21 18:55:32 -04:00
hemna 5bc589f21f Fixed pep8 violation
This patch fixes a pep8 violation in the location plugin
2023-06-21 18:51:53 -04:00
hemna 8b73372b6e Added rpc server for listen
Added the ability to start the rpc server for fetching stats from the
listen command.  If the rpc server is enabled in config, the rpc
server will now start.
2023-06-21 18:48:08 -04:00
hemna 26c1e7afbb Update location plugin and reworked requirements
Added geopy as a dependency for the location plugin.
The us weather service API is now broken upstream.

Reworked the requirements.txt and dev-requirements.txt files
2023-06-15 16:08:28 -04:00
hemna c99d5b859e Merge pull request #116 from jhmartin/fix-example-plugin
Example plugin wrong function
2023-06-14 11:26:38 -04:00
hemna cad22e1744 Fixed .readthedocs.yaml format 2023-06-14 09:31:19 -04:00
hemna 43d6b62760 Add .readthedocs.yaml
Read the docs service is now requiring the config file
.readthedocs.yaml for it to be able to build the online documentation
for a project.
2023-06-14 09:11:39 -04:00
Jason Martin 96fa4330ba Example plugin wrong function
The example plugin, used verbatim, complains about an abstract class. The interface requires 'process'  not 'command'.
2023-05-19 22:53:15 +00:00
hemna 4e99e30f16 Ensure conf is imported for threads/tx
Import the conf for threads/tx.py to ensure that the
msg_rate_limit_period is defined prior to the conf entry
being referenced.
2023-05-05 11:07:23 -04:00
hemna 00f1c3a2ba Update Dockerfile to help build cryptography 2023-04-26 14:31:50 +00:00
57 changed files with 1465 additions and 505 deletions
+52
View File
@@ -0,0 +1,52 @@
name: Manual Build docker container
on:
workflow_dispatch:
inputs:
logLevel:
description: 'Log level'
required: true
default: 'warning'
type: choice
options:
- info
- warning
- debug
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Get Branch Name
id: branch-name
uses: tj-actions/branch-names@v7
- name: Extract Branch
id: extract_branch
run: |
echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
- name: What is the selected branch?
run: |
echo "Selected Branch '${{ steps.extract_branch.outputs.branch }}'"
- name: Setup QEMU
uses: docker/setup-qemu-action@v2
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker HUB
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build the Docker image
uses: docker/build-push-action@v3
with:
context: "{{defaultContext}}:docker"
platforms: linux/amd64,linux/arm64
file: ./Dockerfile-dev
build-args: |
BRANCH=${{ steps.extract_branch.outputs.branch }}
BUILDX_QEMU_ENV=true
push: true
tags: |
hemna6969/aprsd:${{ steps.extract_branch.outputs.branch }}
+2 -2
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: ["3.8", "3.9", "3.10"] python-version: ["3.8", "3.9", "3.10", "3.11"]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
@@ -27,7 +27,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install tox tox-gh-actions pip install tox tox-gh>=1.2
- name: Test with tox - name: Test with tox
run: tox run: tox
+1 -1
View File
@@ -17,6 +17,6 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install tox tox-gh-actions pip install tox tox-gh>=1.2
- name: Test with tox - name: Test with tox
run: tox run: tox
+1 -1
View File
@@ -39,7 +39,7 @@ jobs:
uses: docker/build-push-action@v3 uses: docker/build-push-action@v3
with: with:
context: "{{defaultContext}}:docker" context: "{{defaultContext}}:docker"
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64
file: ./Dockerfile file: ./Dockerfile
build-args: | build-args: |
VERSION=${{ inputs.aprsd_version }} VERSION=${{ inputs.aprsd_version }}
+23
View File
@@ -0,0 +1,23 @@
---
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the version of Python and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.11"
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py
# We recommend specifying your dependencies to enable reproducible builds:
# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: dev-requirements.txt
+76
View File
@@ -1,9 +1,85 @@
CHANGES CHANGES
======= =======
v3.1.2
------
* Added support for ThirdParty packet types
* Disable the Send GPS Beacon button
* Removed adhoc ssl support in webchat
v3.1.1
------
* Updated Changelog for v3.1.1
* Fixed pep8 failures
* re-enable USWeatherPlugin to use mapClick
* Fix sending packets over KISS interface
* Use config web\_ip for running admin ui from module
* remove loop log
* Max out the client reconnect backoff to 5
* Update the Dockerfile
v3.1.0
------
* Changelog updates for v3.1.0
* Use CONF.admin.web\_port for single launch web admin
* Fixed sio namespace registration
* Update Dockerfile-dev to include uwsgi
* Fixed pep8
* change port to 8000
* replacement of flask-socketio with python-socketio
* Change how fetch-stats gets it's defaults
* Ensure fetch-stats ip is a string
* Add info logging for rpc server calls
* updated wsgi config default /config/aprsd.conf
* Added timing after each thread loop
* Update docker bin/admin.sh
* Removed flask-classful from webchat
* Remove flask pinning
* removed linux/arm/v8
* Update master build to include linux/arm/v8
* Update Dockerfile-dev to fix plugin permissions
* update manual build github
* Update requirements for upgraded cryptography
* Added more libs for Dockerfile-dev
* Replace Dockerfile-dev with python3 slim
* Moved logging to log for wsgi.py
* Changed weather plugin regex pattern
* Limit the float values to 3 decimal places
* Fixed rain numbers from aprslib
* Fixed rpc client initialization
* Fix in for aprslib issue #80
* Try and fix Dockerfile-dev
* Fixed pep8 errors
* Populate stats object with threads info
* added counts to the fetch-stats table
* Added the fetch-stats command
* Replace ratelimiter with rush
* Added some utilities to Dockerfile-dev
* add arm64 for manual github build
* Added manual master build
* Update master-build.yml
* Add github manual trigger for master build
* Fixed unit tests for Location plugin
* USe new tox and update githubworkflows
* Updated requirements
* force tox to 4.3.5
* Update github workflows
* Fixed pep8 violation
* Added rpc server for listen
* Update location plugin and reworked requirements
* Fixed .readthedocs.yaml format
* Add .readthedocs.yaml
* Example plugin wrong function
* Ensure conf is imported for threads/tx
* Update Dockerfile to help build cryptography
v3.0.3 v3.0.3
------ ------
* Update Changelog to 3.0.3
* cleanup some debug messages * cleanup some debug messages
* Fixed loading of plugins for server * Fixed loading of plugins for server
* Don't load help plugin for listen command * Don't load help plugin for listen command
+2 -2
View File
@@ -9,8 +9,8 @@ include Makefile.venv
Makefile.venv: Makefile.venv:
curl \ curl \
-o Makefile.fetched \ -o Makefile.fetched \
-L "https://github.com/sio/Makefile.venv/raw/v2022.07.20/Makefile.venv" -L "https://raw.githubusercontent.com/sio/Makefile.venv/master/Makefile.venv"
echo "147b164f0cbbbe4a2740dcca6c9adb6e9d8d15b895be3998697aa6a821a277d8 *Makefile.fetched" \ echo " fb48375ed1fd19e41e0cdcf51a4a0c6d1010dfe03b672ffc4c26a91878544f82 *Makefile.fetched" \
| sha256sum --check - \ | sha256sum --check - \
&& mv Makefile.fetched Makefile.venv && mv Makefile.fetched Makefile.venv
+14
View File
@@ -289,6 +289,20 @@ LOCATION
AND... ping, fortune, time..... AND... ping, fortune, time.....
Web Admin Interface
===================
To start the web admin interface, You have to install gunicorn in your virtualenv that already has aprsd installed.
::
source <path to APRSD's virtualenv>/bin/activate
pip install gunicorn
gunicorn --bind 0.0.0.0:8080 "aprsd.wsgi:app"
The web admin interface will be running on port 8080 on the local machine. http://localhost:8080
Development Development
=========== ===========
+23 -10
View File
@@ -14,7 +14,7 @@ from werkzeug.security import check_password_hash, generate_password_hash
import aprsd import aprsd
from aprsd import cli_helper, client, conf, packets, plugin, threads from aprsd import cli_helper, client, conf, packets, plugin, threads
from aprsd.logging import rich as aprsd_logging from aprsd.log import rich as aprsd_logging
from aprsd.rpc import client as aprsd_rpc_client from aprsd.rpc import client as aprsd_rpc_client
@@ -335,13 +335,26 @@ def init_flask(loglevel, quiet):
return socketio, flask_app return socketio, flask_app
def create_app(config_file=None, log_level=None):
global socketio
global app
default_config_file = cli_helper.DEFAULT_CONFIG_FILE
if not config_file:
config_file = default_config_file
CONF(
[], project="aprsd", version=aprsd.__version__,
default_config_files=[config_file],
)
if not log_level:
log_level = CONF.logging.log_level
socketio, app = init_flask(log_level, False)
setup_logging(app, log_level, False)
return app
if __name__ == "aprsd.flask": if __name__ == "aprsd.flask":
try: sio, app = create_app()
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)
+1 -1
View File
@@ -8,7 +8,7 @@ from oslo_config import cfg
import aprsd import aprsd
from aprsd import conf # noqa: F401 from aprsd import conf # noqa: F401
from aprsd.logging import log from aprsd.log import log
from aprsd.utils import trace from aprsd.utils import trace
+17 -4
View File
@@ -49,8 +49,10 @@ class Client:
@property @property
def client(self): def client(self):
if not self._client: if not self._client:
LOG.info("Creating APRS client")
self._client = self.setup_connection() self._client = self.setup_connection()
if self.filter: if self.filter:
LOG.info("Creating APRS client filter")
self._client.set_filter(self.filter) self._client.set_filter(self.filter)
return self._client return self._client
@@ -62,6 +64,8 @@ class Client:
"""Call this to force a rebuild/reconnect.""" """Call this to force a rebuild/reconnect."""
if self._client: if self._client:
del self._client del self._client
else:
LOG.warning("Client not initialized, nothing to reset.")
@abc.abstractmethod @abc.abstractmethod
def setup_connection(self): def setup_connection(self):
@@ -144,7 +148,7 @@ class APRSISClient(Client):
try: try:
LOG.info("Creating aprslib client") LOG.info("Creating aprslib client")
aprs_client = aprsis.Aprsdis(user, passwd=password, host=host, port=port) aprs_client = aprsis.Aprsdis(user, passwd=password, host=host, port=port)
# Force the logging to be the same # Force the log to be the same
aprs_client.logger = LOG aprs_client.logger = LOG
aprs_client.connect() aprs_client.connect()
connected = True connected = True
@@ -152,11 +156,16 @@ class APRSISClient(Client):
except LoginError as e: except LoginError as e:
LOG.error(f"Failed to login to APRS-IS Server '{e}'") LOG.error(f"Failed to login to APRS-IS Server '{e}'")
connected = False connected = False
raise e time.sleep(backoff)
except Exception as e: except Exception as e:
LOG.error(f"Unable to connect to APRS-IS server. '{e}' ") LOG.error(f"Unable to connect to APRS-IS server. '{e}' ")
connected = False
time.sleep(backoff) time.sleep(backoff)
backoff = backoff * 2 # Don't allow the backoff to go to inifinity.
if backoff > 5:
backoff = 5
else:
backoff += 1
continue continue
LOG.debug(f"Logging in to APRS-IS with user '{user}'") LOG.debug(f"Logging in to APRS-IS with user '{user}'")
self._client = aprs_client self._client = aprs_client
@@ -226,7 +235,11 @@ class KISSClient(Client):
# LOG.debug(f"Decoding {msg}") # LOG.debug(f"Decoding {msg}")
raw = aprslib.parse(str(frame)) raw = aprslib.parse(str(frame))
return core.Packet.factory(raw) packet = core.Packet.factory(raw)
if isinstance(packet, core.ThirdParty):
return packet.subpacket
else:
return packet
@trace.trace @trace.trace
def setup_connection(self): def setup_connection(self):
+11 -6
View File
@@ -112,23 +112,23 @@ class Aprsdis(aprslib.IS):
self._sendall(login_str) self._sendall(login_str)
self.sock.settimeout(5) self.sock.settimeout(5)
test = self.sock.recv(len(login_str) + 100) test = self.sock.recv(len(login_str) + 100)
self.logger.debug("Server: '%s'", test)
if is_py3: if is_py3:
test = test.decode("latin-1") test = test.decode("latin-1")
test = test.rstrip() test = test.rstrip()
self.logger.debug("Server: %s", test) self.logger.debug("Server: '%s'", test)
a, b, callsign, status, e = test.split(" ", 4) if not test:
raise LoginError(f"Server Response Empty: '{test}'")
_, _, callsign, status, e = test.split(" ", 4)
s = e.split(",") s = e.split(",")
if len(s): if len(s):
server_string = s[0].replace("server ", "") server_string = s[0].replace("server ", "")
else: else:
server_string = e.replace("server ", "") server_string = e.replace("server ", "")
self.logger.info(f"Connected to {server_string}")
self.server_string = server_string
stats.APRSDStats().set_aprsis_server(server_string)
if callsign == "": if callsign == "":
raise LoginError("Server responded with empty callsign???") raise LoginError("Server responded with empty callsign???")
if callsign != self.callsign: if callsign != self.callsign:
@@ -141,6 +141,10 @@ class Aprsdis(aprslib.IS):
else: else:
self.logger.info("Login successful") self.logger.info("Login successful")
self.logger.info(f"Connected to {server_string}")
self.server_string = server_string
stats.APRSDStats().set_aprsis_server(server_string)
except LoginError as e: except LoginError as e:
self.logger.error(str(e)) self.logger.error(str(e))
self.close() self.close()
@@ -148,6 +152,7 @@ class Aprsdis(aprslib.IS):
except Exception as e: except Exception as e:
self.close() self.close()
self.logger.error(f"Failed to login '{e}'") self.logger.error(f"Failed to login '{e}'")
self.logger.exception(e)
raise LoginError("Failed to login") raise LoginError("Failed to login")
def consumer(self, callback, blocking=True, immortal=False, raw=False): def consumer(self, callback, blocking=True, immortal=False, raw=False):
+9 -15
View File
@@ -81,27 +81,18 @@ class KISS3Client:
LOG.debug("Start blocking KISS consumer") LOG.debug("Start blocking KISS consumer")
self._parse_callback = callback self._parse_callback = callback
self.kiss.read(callback=self.parse_frame, min_frames=None) self.kiss.read(callback=self.parse_frame, min_frames=None)
LOG.debug("END blocking KISS consumer") LOG.debug(f"END blocking KISS consumer {self.kiss}")
def send(self, packet): def send(self, packet):
"""Send an APRS Message object.""" """Send an APRS Message object."""
# payload = (':%-9s:%s' % (
# msg.tocall,
# payload
# )).encode('US-ASCII'),
# payload = str(msg).encode('US-ASCII')
payload = None payload = None
path = ["WIDE1-1", "WIDE2-1"] path = ["WIDE1-1", "WIDE2-1"]
if isinstance(packet, core.AckPacket): if isinstance(packet, core.Packet):
msg_payload = f"ack{packet.msgNo}" packet.prepare()
elif isinstance(packet, core.Packet): payload = packet.payload.encode("US-ASCII")
payload = packet.raw.encode("US-ASCII")
path = ["WIDE2-1"]
else: else:
msg_payload = f"{packet.raw}{{{str(packet.msgNo)}" msg_payload = f"{packet.raw}{{{str(packet.msgNo)}"
if not payload:
payload = ( payload = (
":{:<9}:{}".format( ":{:<9}:{}".format(
packet.to_call, packet.to_call,
@@ -109,9 +100,12 @@ class KISS3Client:
) )
).encode("US-ASCII") ).encode("US-ASCII")
LOG.debug(f"Send '{payload}' TO KISS") LOG.debug(
f"KISS Send '{payload}' TO '{packet.to_call}' From "
f"'{packet.from_call}' with PATH '{path}'",
)
frame = Frame.ui( frame = Frame.ui(
destination=packet.to_call, destination="APZ100",
source=packet.from_call, source=packet.from_call,
path=path, path=path,
info=payload, info=payload,
+1 -1
View File
@@ -1,7 +1,7 @@
import click import click
import click_completion import click_completion
from aprsd.aprsd import cli from aprsd.main import cli
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
+1 -1
View File
@@ -10,7 +10,7 @@ from oslo_config import cfg
# local imports here # local imports here
from aprsd import cli_helper, client, conf, packets, plugin from aprsd import cli_helper, client, conf, packets, plugin
from aprsd.aprsd import cli from aprsd.main import cli
from aprsd.utils import trace from aprsd.utils import trace
+156
View File
@@ -0,0 +1,156 @@
# Fetch active stats from a remote running instance of aprsd server
# This uses the RPC server to fetch the stats from the remote server.
import logging
import click
from oslo_config import cfg
from rich.console import Console
from rich.table import Table
# local imports here
import aprsd
from aprsd import cli_helper
from aprsd.main import cli
from aprsd.rpc import client as rpc_client
# setup the global logger
# log.basicConfig(level=log.DEBUG) # level=10
LOG = logging.getLogger("APRSD")
CONF = cfg.CONF
@cli.command()
@cli_helper.add_options(cli_helper.common_options)
@click.option(
"--host", type=str,
default=None,
help="IP address of the remote aprsd server to fetch stats from.",
)
@click.option(
"--port", type=int,
default=None,
help="Port of the remote aprsd server rpc port to fetch stats from.",
)
@click.option(
"--magic-word", type=str,
default=None,
help="Magic word of the remote aprsd server rpc port to fetch stats from.",
)
@click.pass_context
@cli_helper.process_standard_options
def fetch_stats(ctx, host, port, magic_word):
"""Fetch stats from a remote running instance of aprsd server."""
LOG.info(f"APRSD Fetch-Stats started version: {aprsd.__version__}")
CONF.log_opt_values(LOG, logging.DEBUG)
if not host:
host = CONF.rpc_settings.ip
if not port:
port = CONF.rpc_settings.port
if not magic_word:
magic_word = CONF.rpc_settings.magic_word
msg = f"Fetching stats from {host}:{port} with magic word '{magic_word}'"
console = Console()
console.print(msg)
with console.status(msg):
client = rpc_client.RPCClient(host, port, magic_word)
stats = client.get_stats_dict()
console.print_json(data=stats)
aprsd_title = (
"APRSD "
f"[bold cyan]v{stats['aprsd']['version']}[/] "
f"Callsign [bold green]{stats['aprsd']['callsign']}[/] "
f"Uptime [bold yellow]{stats['aprsd']['uptime']}[/]"
)
console.rule(f"Stats from {host}:{port} with magic word '{magic_word}'")
console.print("\n\n")
console.rule(aprsd_title)
# Show the connection to APRS
# It can be a connection to an APRS-IS server or a local TNC via KISS or KISSTCP
if "aprs-is" in stats:
title = f"APRS-IS Connection {stats['aprs-is']['server']}"
table = Table(title=title)
table.add_column("Key")
table.add_column("Value")
for key, value in stats["aprs-is"].items():
table.add_row(key, value)
console.print(table)
threads_table = Table(title="Threads")
threads_table.add_column("Name")
threads_table.add_column("Alive?")
for name, alive in stats["aprsd"]["threads"].items():
threads_table.add_row(name, str(alive))
console.print(threads_table)
msgs_table = Table(title="Messages")
msgs_table.add_column("Key")
msgs_table.add_column("Value")
for key, value in stats["messages"].items():
msgs_table.add_row(key, str(value))
console.print(msgs_table)
packet_totals = Table(title="Packet Totals")
packet_totals.add_column("Key")
packet_totals.add_column("Value")
packet_totals.add_row("Total Received", str(stats["packets"]["total_received"]))
packet_totals.add_row("Total Sent", str(stats["packets"]["total_sent"]))
packet_totals.add_row("Total Tracked", str(stats["packets"]["total_tracked"]))
console.print(packet_totals)
# Show each of the packet types
packets_table = Table(title="Packets By Type")
packets_table.add_column("Packet Type")
packets_table.add_column("TX")
packets_table.add_column("RX")
for key, value in stats["packets"]["by_type"].items():
packets_table.add_row(key, str(value["tx"]), str(value["rx"]))
console.print(packets_table)
if "plugins" in stats:
count = len(stats["plugins"])
plugins_table = Table(title=f"Plugins ({count})")
plugins_table.add_column("Plugin")
plugins_table.add_column("Enabled")
plugins_table.add_column("Version")
plugins_table.add_column("TX")
plugins_table.add_column("RX")
for key, value in stats["plugins"].items():
plugins_table.add_row(
key,
str(stats["plugins"][key]["enabled"]),
stats["plugins"][key]["version"],
str(stats["plugins"][key]["tx"]),
str(stats["plugins"][key]["rx"]),
)
console.print(plugins_table)
if "seen_list" in stats["aprsd"]:
count = len(stats["aprsd"]["seen_list"])
seen_table = Table(title=f"Seen List ({count})")
seen_table.add_column("Callsign")
seen_table.add_column("Message Count")
seen_table.add_column("Last Heard")
for key, value in stats["aprsd"]["seen_list"].items():
seen_table.add_row(key, str(value["count"]), value["last"])
console.print(seen_table)
if "watch_list" in stats["aprsd"]:
count = len(stats["aprsd"]["watch_list"])
watch_table = Table(title=f"Watch List ({count})")
watch_table.add_column("Callsign")
watch_table.add_column("Last Heard")
for key, value in stats["aprsd"]["watch_list"].items():
watch_table.add_row(key, value["last"])
console.print(watch_table)
+2 -2
View File
@@ -16,12 +16,12 @@ import aprsd
from aprsd import cli_helper, utils from aprsd import cli_helper, utils
from aprsd import conf # noqa from aprsd import conf # noqa
# local imports here # local imports here
from aprsd.aprsd import cli from aprsd.main import cli
from aprsd.rpc import client as aprsd_rpc_client from aprsd.rpc import client as aprsd_rpc_client
# setup the global logger # setup the global logger
# logging.basicConfig(level=logging.DEBUG) # level=10 # log.basicConfig(level=log.DEBUG) # level=10
CONF = cfg.CONF CONF = cfg.CONF
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
console = Console() console = Console()
+1 -1
View File
@@ -19,7 +19,7 @@ from thesmuggler import smuggle
from aprsd import cli_helper from aprsd import cli_helper
from aprsd import plugin as aprsd_plugin from aprsd import plugin as aprsd_plugin
from aprsd.aprsd import cli from aprsd.main import cli
from aprsd.plugins import ( from aprsd.plugins import (
email, fortune, location, notify, ping, query, time, version, weather, email, fortune, location, notify, ping, query, time, version, weather,
) )
+10 -2
View File
@@ -16,12 +16,13 @@ from rich.console import Console
# local imports here # local imports here
import aprsd import aprsd
from aprsd import cli_helper, client, packets, plugin, stats, threads from aprsd import cli_helper, client, packets, plugin, stats, threads
from aprsd.aprsd import cli from aprsd.main import cli
from aprsd.rpc import server as rpc_server
from aprsd.threads import rx from aprsd.threads import rx
# setup the global logger # setup the global logger
# logging.basicConfig(level=logging.DEBUG) # level=10 # log.basicConfig(level=log.DEBUG) # level=10
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
CONF = cfg.CONF CONF = cfg.CONF
console = Console() console = Console()
@@ -181,6 +182,10 @@ def listen(
keepalive = threads.KeepAliveThread() keepalive = threads.KeepAliveThread()
keepalive.start() keepalive.start()
if CONF.rpc_settings.enabled:
rpc = rpc_server.APRSDRPCThread()
rpc.start()
pm = None pm = None
pm = plugin.PluginManager() pm = plugin.PluginManager()
if load_plugins: if load_plugins:
@@ -204,3 +209,6 @@ def listen(
keepalive.join() keepalive.join()
LOG.debug("listen_thread Join") LOG.debug("listen_thread Join")
listen_thread.join() listen_thread.join()
if CONF.rpc_settings.enabled:
rpc.join()
+1 -1
View File
@@ -10,7 +10,7 @@ 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 import conf # noqa : F401
from aprsd.aprsd import cli from aprsd.main import cli
from aprsd.threads import tx from aprsd.threads import tx
+4 -3
View File
@@ -6,9 +6,10 @@ import click
from oslo_config import cfg from oslo_config import cfg
import aprsd import aprsd
from aprsd import aprsd as aprsd_main from aprsd import cli_helper, client
from aprsd import cli_helper, client, packets, plugin, threads, utils from aprsd import main as aprsd_main
from aprsd.aprsd import cli from aprsd import packets, plugin, threads, utils
from aprsd.main import cli
from aprsd.rpc import server as rpc_server from aprsd.rpc import server as rpc_server
from aprsd.threads import rx from aprsd.threads import rx
+114 -110
View File
@@ -12,7 +12,6 @@ import click
import flask import flask
from flask import request from flask import request
from flask.logging import default_handler from flask.logging import default_handler
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 oslo_config import cfg
@@ -22,8 +21,8 @@ import wrapt
import aprsd import aprsd
from aprsd import cli_helper, client, conf, packets, stats, threads, utils from aprsd import cli_helper, client, conf, packets, stats, threads, utils
from aprsd.aprsd import cli from aprsd.log import rich as aprsd_logging
from aprsd.logging import rich as aprsd_logging from aprsd.main import cli
from aprsd.threads import rx, tx from aprsd.threads import rx, tx
from aprsd.utils import objectstore, trace from aprsd.utils import objectstore, trace
@@ -31,9 +30,16 @@ from aprsd.utils import objectstore, trace
CONF = cfg.CONF CONF = cfg.CONF
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
auth = HTTPBasicAuth() auth = HTTPBasicAuth()
users = None users = {}
socketio = None socketio = None
flask_app = flask.Flask(
"aprsd",
static_url_path="/static",
static_folder="web/chat/static",
template_folder="web/chat/templates",
)
def signal_handler(sig, frame): def signal_handler(sig, frame):
@@ -174,106 +180,112 @@ class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
) )
class WebChatFlask(flask_classful.FlaskView): def set_config():
global users
def set_config(self):
global users
self.users = {}
user = CONF.admin.user
self.users[user] = generate_password_hash(CONF.admin.password)
users = self.users
def _get_transport(self, stats): def _get_transport(stats):
if CONF.aprs_network.enabled: 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' >"
"{}</a>".format(stats["stats"]["aprs-is"]["server"]) "{}</a>".format(stats["stats"]["aprs-is"]["server"])
)
else:
# We might be connected to a KISS socket?
if client.KISSClient.is_enabled():
transport = client.KISSClient.transport()
if transport == client.TRANSPORT_TCPKISS:
aprs_connection = (
"TCPKISS://{}:{}".format(
CONF.kiss_tcp.host,
CONF.kiss_tcp.port,
)
)
elif transport == client.TRANSPORT_SERIALKISS:
# for pep8 violation
aprs_connection = (
"SerialKISS://{}@{} baud".format(
CONF.kiss_serial.device,
CONF.kiss_serial.baudrate,
),
)
return transport, aprs_connection
@auth.login_required
def index(self):
ua_str = request.headers.get("User-Agent")
# this takes about 2 seconds :(
user_agent = ua_parse(ua_str)
LOG.debug(f"Is mobile? {user_agent.is_mobile}")
stats = self._stats()
if user_agent.is_mobile:
html_template = "mobile.html"
else:
html_template = "index.html"
# For development
# html_template = "mobile.html"
LOG.debug(f"Template {html_template}")
transport, aprs_connection = self._get_transport(stats)
LOG.debug(f"transport {transport} aprs_connection {aprs_connection}")
stats["transport"] = transport
stats["aprs_connection"] = aprs_connection
LOG.debug(f"initial stats = {stats}")
return flask.render_template(
html_template,
initial_stats=stats,
aprs_connection=aprs_connection,
callsign=CONF.callsign,
version=aprsd.__version__,
) )
else:
# We might be connected to a KISS socket?
if client.KISSClient.is_enabled():
transport = client.KISSClient.transport()
if transport == client.TRANSPORT_TCPKISS:
aprs_connection = (
"TCPKISS://{}:{}".format(
CONF.kiss_tcp.host,
CONF.kiss_tcp.port,
)
)
elif transport == client.TRANSPORT_SERIALKISS:
# for pep8 violation
aprs_connection = (
"SerialKISS://{}@{} baud".format(
CONF.kiss_serial.device,
CONF.kiss_serial.baudrate,
),
)
@auth.login_required return transport, aprs_connection
def send_message_status(self):
LOG.debug(request)
msgs = SentMessages()
info = msgs.get_all()
return json.dumps(info)
def _stats(self):
stats_obj = stats.APRSDStats()
now = datetime.datetime.now()
time_format = "%m-%d-%Y %H:%M:%S" @auth.login_required
stats_dict = stats_obj.stats() @flask_app.route("/")
# Webchat doesnt need these def index():
ua_str = request.headers.get("User-Agent")
# this takes about 2 seconds :(
user_agent = ua_parse(ua_str)
LOG.debug(f"Is mobile? {user_agent.is_mobile}")
stats = _stats()
if user_agent.is_mobile:
html_template = "mobile.html"
else:
html_template = "index.html"
# For development
# html_template = "mobile.html"
LOG.debug(f"Template {html_template}")
transport, aprs_connection = _get_transport(stats)
LOG.debug(f"transport {transport} aprs_connection {aprs_connection}")
stats["transport"] = transport
stats["aprs_connection"] = aprs_connection
LOG.debug(f"initial stats = {stats}")
return flask.render_template(
html_template,
initial_stats=stats,
aprs_connection=aprs_connection,
callsign=CONF.callsign,
version=aprsd.__version__,
)
@auth.login_required
@flask_app.route("//send-message-status")
def send_message_status():
LOG.debug(request)
msgs = SentMessages()
info = msgs.get_all()
return json.dumps(info)
def _stats():
stats_obj = stats.APRSDStats()
now = datetime.datetime.now()
time_format = "%m-%d-%Y %H:%M:%S"
stats_dict = stats_obj.stats()
# Webchat doesnt need these
if "watch_list" in stats_dict["aprsd"]:
del stats_dict["aprsd"]["watch_list"] del stats_dict["aprsd"]["watch_list"]
if "seen_list" in stats_dict["aprsd"]:
del stats_dict["aprsd"]["seen_list"] del stats_dict["aprsd"]["seen_list"]
# del stats_dict["email"] if "threads" in stats_dict["aprsd"]:
# del stats_dict["plugins"] del stats_dict["aprsd"]["threads"]
# del stats_dict["messages"] # del stats_dict["email"]
# del stats_dict["plugins"]
# del stats_dict["messages"]
result = { result = {
"time": now.strftime(time_format), "time": now.strftime(time_format),
"stats": stats_dict, "stats": stats_dict,
} }
return result return result
def stats(self):
return json.dumps(self._stats()) @flask_app.route("/stats")
def get_stats():
return json.dumps(_stats())
class SendMessageNamespace(Namespace): class SendMessageNamespace(Namespace):
@@ -377,21 +389,9 @@ def setup_logging(flask_app, loglevel, quiet):
@trace.trace @trace.trace
def init_flask(loglevel, quiet): def init_flask(loglevel, quiet):
global socketio global socketio, flask_app
flask_app = flask.Flask(
"aprsd",
static_url_path="/static",
static_folder="web/chat/static",
template_folder="web/chat/templates",
)
setup_logging(flask_app, loglevel, quiet) setup_logging(flask_app, loglevel, quiet)
server = WebChatFlask()
server.set_config()
flask_app.route("/", methods=["GET"])(server.index)
flask_app.route("/stats", methods=["GET"])(server.stats)
# flask_app.route("/send-message", methods=["GET"])(server.send_message)
flask_app.route("/send-message-status", methods=["GET"])(server.send_message_status)
socketio = SocketIO( socketio = SocketIO(
flask_app, logger=False, engineio_logger=False, flask_app, logger=False, engineio_logger=False,
@@ -407,7 +407,7 @@ def init_flask(loglevel, quiet):
"/sendmsg", "/sendmsg",
), ),
) )
return socketio, flask_app return socketio
# main() ### # main() ###
@@ -448,6 +448,8 @@ def webchat(ctx, flush, port):
LOG.info(f"APRSD Started version: {aprsd.__version__}") LOG.info(f"APRSD Started version: {aprsd.__version__}")
CONF.log_opt_values(LOG, logging.DEBUG) CONF.log_opt_values(LOG, logging.DEBUG)
user = CONF.admin.user
users[user] = generate_password_hash(CONF.admin.password)
# 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
@@ -466,7 +468,7 @@ def webchat(ctx, flush, port):
packets.WatchList() packets.WatchList()
packets.SeenList() packets.SeenList()
(socketio, app) = init_flask(loglevel, quiet) socketio = init_flask(loglevel, quiet)
rx_thread = rx.APRSDPluginRXThread( rx_thread = rx.APRSDPluginRXThread(
packet_queue=threads.packet_queue, packet_queue=threads.packet_queue,
) )
@@ -482,8 +484,10 @@ def webchat(ctx, flush, port):
keepalive.start() keepalive.start()
LOG.info("Start socketio.run()") LOG.info("Start socketio.run()")
socketio.run( socketio.run(
app, flask_app,
ssl_context="adhoc", # This is broken for now after removing cryptography
# and pyopenssl
# ssl_context="adhoc",
host=CONF.admin.web_ip, host=CONF.admin.web_ip,
port=port, port=port,
) )
+1 -1
View File
@@ -28,7 +28,7 @@ def set_lib_defaults():
def set_log_defaults(): def set_log_defaults():
# logging.set_defaults(default_log_levels=logging.get_default_log_levels()) # log.set_defaults(default_log_levels=log.get_default_log_levels())
pass pass
+1 -1
View File
@@ -1,5 +1,5 @@
""" """
The options for logging setup The options for log setup
""" """
from oslo_config import cfg from oslo_config import cfg
+8 -2
View File
@@ -1,5 +1,5 @@
""" """
The options for logging setup The options for log setup
""" """
import logging import logging
@@ -33,7 +33,7 @@ logging_opts = [
cfg.BoolOpt( cfg.BoolOpt(
"rich_logging", "rich_logging",
default=True, default=True,
help="Enable Rich logging", help="Enable Rich log",
), ),
cfg.StrOpt( cfg.StrOpt(
"logfile", "logfile",
@@ -45,6 +45,12 @@ logging_opts = [
default=DEFAULT_LOG_FORMAT, default=DEFAULT_LOG_FORMAT,
help="Log file format, unless rich_logging enabled.", help="Log file format, unless rich_logging enabled.",
), ),
cfg.StrOpt(
"log_level",
default="INFO",
choices=LOG_LEVELS.keys(),
help="Log level for logging of events.",
),
] ]
+3 -3
View File
@@ -7,7 +7,7 @@ import sys
from oslo_config import cfg from oslo_config import cfg
from aprsd import conf from aprsd import conf
from aprsd.logging import rich as aprsd_logging from aprsd.log import rich as aprsd_logging
CONF = cfg.CONF CONF = cfg.CONF
@@ -15,8 +15,8 @@ LOG = logging.getLogger("APRSD")
logging_queue = queue.Queue() logging_queue = queue.Queue()
# Setup the logging faciility # Setup the log faciility
# to disable logging to stdout, but still log to file # to disable log to stdout, but still log to file
# use the --quiet option on the cmdln # use the --quiet option on the cmdln
def setup_logging(loglevel, quiet): def setup_logging(loglevel, quiet):
log_level = conf.log.LOG_LEVELS[loglevel] log_level = conf.log.LOG_LEVELS[loglevel]
+1 -1
View File
@@ -130,7 +130,7 @@ class APRSDRichHandler(RichHandler):
"""Render log for display. """Render log for display.
Args: Args:
record (LogRecord): logging Record. record (LogRecord): log Record.
traceback (Optional[Traceback]): Traceback instance or None for no Traceback. traceback (Optional[Traceback]): Traceback instance or None for no Traceback.
message_renderable (ConsoleRenderable): Renderable (typically Text) containing log message contents. message_renderable (ConsoleRenderable): Renderable (typically Text) containing log message contents.
+3 -3
View File
@@ -39,7 +39,7 @@ from aprsd import cli_helper, packets, stats, threads, utils
# setup the global logger # setup the global logger
# logging.basicConfig(level=logging.DEBUG) # level=10 # log.basicConfig(level=log.DEBUG) # level=10
CONF = cfg.CONF 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"])
@@ -70,8 +70,8 @@ def main():
# First import all the possible commands for the CLI # First import all the possible commands for the CLI
# The commands themselves live in the cmds directory # The commands themselves live in the cmds directory
from .cmds import ( # noqa from .cmds import ( # noqa
completion, dev, healthcheck, list_plugins, listen, send_message, completion, dev, fetch_stats, healthcheck, list_plugins, listen,
server, webchat, send_message, server, webchat,
) )
cli(auto_envvar_prefix="APRSD") cli(auto_envvar_prefix="APRSD")
+141 -48
View File
@@ -25,6 +25,7 @@ PACKET_TYPE_OBJECT = "object"
PACKET_TYPE_UNKNOWN = "unknown" PACKET_TYPE_UNKNOWN = "unknown"
PACKET_TYPE_STATUS = "status" PACKET_TYPE_STATUS = "status"
PACKET_TYPE_BEACON = "beacon" PACKET_TYPE_BEACON = "beacon"
PACKET_TYPE_THIRDPARTY = "thirdparty"
PACKET_TYPE_UNCOMPRESSED = "uncompressed" PACKET_TYPE_UNCOMPRESSED = "uncompressed"
@@ -57,6 +58,8 @@ class Packet(metaclass=abc.ABCMeta):
# or holds the raw string from input packet # or holds the raw string from input packet
raw: str = None raw: str = None
raw_dict: dict = field(repr=False, default_factory=lambda: {}) raw_dict: dict = field(repr=False, default_factory=lambda: {})
# Built by calling prepare(). raw needs this built first.
payload: str = None
# Fields related to sending packets out # Fields related to sending packets out
send_count: int = field(repr=False, default=0) send_count: int = field(repr=False, default=0)
@@ -92,14 +95,28 @@ class Packet(metaclass=abc.ABCMeta):
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
self._build_payload()
self._build_raw() self._build_raw()
def _build_payload(self):
"""The payload is the non headers portion of the packet."""
msg = self._filter_for_send().rstrip("\n")
self.payload = (
f":{self.to_call.ljust(9)}"
f":{msg}"
)
def _build_raw(self): def _build_raw(self):
"""Build the self.raw string which is what is sent over the air.""" """Build the self.raw which is what is sent over the air."""
self.raw = self._filter_for_send().rstrip("\n") self.raw = "{}>APZ100:{}".format(
self.from_call,
self.payload,
)
LOG.debug(f"_build_raw: payload '{self.payload}' raw '{self.raw}'")
@staticmethod @staticmethod
def factory(raw_packet): def factory(raw_packet):
"""Factory method to create a packet from a raw packet string."""
raw = raw_packet raw = raw_packet
raw["raw_dict"] = raw.copy() raw["raw_dict"] = raw.copy()
translate_fields = { translate_fields = {
@@ -118,6 +135,19 @@ class Packet(metaclass=abc.ABCMeta):
packet_type = get_packet_type(raw) packet_type = get_packet_type(raw)
raw["packet_type"] = packet_type raw["packet_type"] = packet_type
class_name = TYPE_LOOKUP[packet_type] class_name = TYPE_LOOKUP[packet_type]
if packet_type == PACKET_TYPE_THIRDPARTY:
# We have an encapsulated packet!
# So we need to decode it and return the inner packet
# as the packet we are going to process.
# This is a recursive call to the factory
subpacket_raw = raw["subpacket"]
subpacket = Packet.factory(subpacket_raw)
del raw["subpacket"]
# raw["subpacket"] = subpacket
packet = dacite.from_dict(data_class=class_name, data=raw)
packet.subpacket = subpacket
return packet
if packet_type == PACKET_TYPE_UNKNOWN: if packet_type == PACKET_TYPE_UNKNOWN:
# Try and figure it out here # Try and figure it out here
if "latitude" in raw: if "latitude" in raw:
@@ -126,9 +156,43 @@ class Packet(metaclass=abc.ABCMeta):
if packet_type == PACKET_TYPE_WX: if packet_type == PACKET_TYPE_WX:
# the weather information is in a dict # the weather information is in a dict
# this brings those values out to the outer dict # this brings those values out to the outer dict
for key in raw["weather"]: for key in raw["weather"]:
raw[key] = raw["weather"][key] raw[key] = raw["weather"][key]
# If we have the broken aprslib, then we need to
# Convert the course and speed to wind_speed and wind_direction
# aprslib issue #80
# https://github.com/rossengeorgiev/aprs-python/issues/80
# Wind speed and course is option in the SPEC.
# For some reason aprslib multiplies the speed by 1.852.
if "wind_speed" not in raw and "wind_direction" not in raw:
# Most likely this is the broken aprslib
# So we need to convert the wind_gust speed
raw["wind_gust"] = round(raw.get("wind_gust", 0) / 0.44704, 3)
if "wind_speed" not in raw:
wind_speed = raw.get("speed")
if wind_speed:
raw["wind_speed"] = round(wind_speed / 1.852, 3)
raw["weather"]["wind_speed"] = raw["wind_speed"]
if "speed" in raw:
del raw["speed"]
# Let's adjust the rain numbers as well, since it's wrong
raw["rain_1h"] = round((raw.get("rain_1h", 0) / .254) * .01, 3)
raw["weather"]["rain_1h"] = raw["rain_1h"]
raw["rain_24h"] = round((raw.get("rain_24h", 0) / .254) * .01, 3)
raw["weather"]["rain_24h"] = raw["rain_24h"]
raw["rain_since_midnight"] = round((raw.get("rain_since_midnight", 0) / .254) * .01, 3)
raw["weather"]["rain_since_midnight"] = raw["rain_since_midnight"]
if "wind_direction" not in raw:
wind_direction = raw.get("course")
if wind_direction:
raw["wind_direction"] = wind_direction
raw["weather"]["wind_direction"] = raw["wind_direction"]
if "course" in raw:
del raw["course"]
return dacite.from_dict(data_class=class_name, data=raw) return dacite.from_dict(data_class=class_name, data=raw)
def log(self, header=None): def log(self, header=None):
@@ -199,7 +263,7 @@ class PathPacket(Packet):
path: List[str] = field(default_factory=list) path: List[str] = field(default_factory=list)
via: str = None via: str = None
def _build_raw(self): def _build_payload(self):
raise NotImplementedError raise NotImplementedError
@@ -211,13 +275,8 @@ class AckPacket(PathPacket):
if self.response: if self.response:
LOG.warning("Response set!") LOG.warning("Response set!")
def _build_raw(self): def _build_payload(self):
"""Build the self.raw which is what is sent over the air.""" self.payload = f":{self.to_call.ljust(9)}:ack{self.msgNo}"
self.raw = "{}>APZ100::{}:ack{}".format(
self.from_call,
self.to_call.ljust(9),
self.msgNo,
)
@dataclass @dataclass
@@ -228,13 +287,8 @@ class RejectPacket(PathPacket):
if self.response: if self.response:
LOG.warning("Response set!") LOG.warning("Response set!")
def _build_raw(self): def _build_payload(self):
"""Build the self.raw which is what is sent over the air.""" self.payload = f":{self.to_call.ljust(9)} :rej{self.msgNo}"
self.raw = "{}>APZ100::{} :rej{}".format(
self.from_call,
self.to_call.ljust(9),
self.msgNo,
)
@dataclass @dataclass
@@ -251,10 +305,8 @@ class MessagePacket(PathPacket):
# We all miss George Carlin # We all miss George Carlin
return re.sub("fuck|shit|cunt|piss|cock|bitch", "****", message) return re.sub("fuck|shit|cunt|piss|cock|bitch", "****", message)
def _build_raw(self): def _build_payload(self):
"""Build the self.raw which is what is sent over the air.""" self.payload = ":{}:{}{{{}".format(
self.raw = "{}>APZ100::{}:{}{{{}".format(
self.from_call,
self.to_call.ljust(9), self.to_call.ljust(9),
self._filter_for_send().rstrip("\n"), self._filter_for_send().rstrip("\n"),
str(self.msgNo), str(self.msgNo),
@@ -267,7 +319,7 @@ class StatusPacket(PathPacket):
messagecapable: bool = False messagecapable: bool = False
comment: str = None comment: str = None
def _build_raw(self): def _build_payload(self):
raise NotImplementedError raise NotImplementedError
@@ -281,10 +333,6 @@ class GPSPacket(PathPacket):
comment: str = None comment: str = None
symbol: str = field(default="l") symbol: str = field(default="l")
symbol_table: str = field(default="/") symbol_table: str = field(default="/")
# in MPH
speed: float = 0.00
# 0 to 360
course: int = 0
def decdeg2dms(self, degrees_decimal): def decdeg2dms(self, degrees_decimal):
is_positive = degrees_decimal >= 0 is_positive = degrees_decimal >= 0
@@ -370,16 +418,24 @@ class GPSPacket(PathPacket):
time_zulu = result_utc_datetime.strftime("%d%H%M") time_zulu = result_utc_datetime.strftime("%d%H%M")
return time_zulu return time_zulu
def _build_raw(self): def _build_payload(self):
"""The payload is the non headers portion of the packet."""
time_zulu = self._build_time_zulu() time_zulu = self._build_time_zulu()
lat = self.latitude
long = self.longitude
self.payload = (
f"@{time_zulu}z{lat}{self.symbol_table}"
f"{long}{self.symbol}"
)
if self.comment:
self.payload = f"{self.payload}{self.comment}"
def _build_raw(self):
self.raw = ( self.raw = (
f"{self.from_call}>{self.to_call},WIDE2-1:" f"{self.from_call}>{self.to_call},WIDE2-1:"
f"@{time_zulu}z{self.latitude}{self.symbol_table}" f"{self.payload}"
f"{self.longitude}{self.symbol}"
) )
if self.comment:
self.raw = f"{self.raw}{self.comment}"
@dataclass @dataclass
@@ -387,8 +443,12 @@ class MicEPacket(GPSPacket):
messagecapable: bool = False messagecapable: bool = False
mbits: str = None mbits: str = None
mtype: str = None mtype: str = None
# in MPH
speed: float = 0.00
# 0 to 360
course: int = 0
def _build_raw(self): def _build_payload(self):
raise NotImplementedError raise NotImplementedError
@@ -397,6 +457,23 @@ class ObjectPacket(GPSPacket):
alive: bool = True alive: bool = True
raw_timestamp: str = None raw_timestamp: str = None
symbol: str = field(default="r") symbol: str = field(default="r")
# in MPH
speed: float = 0.00
# 0 to 360
course: int = 0
def _build_payload(self):
time_zulu = self._build_time_zulu()
lat = self.convert_latitude(self.latitude)
long = self.convert_longitude(self.longitude)
self.payload = (
f"*{time_zulu}z{lat}{self.symbol_table}"
f"{long}{self.symbol}"
)
if self.comment:
self.payload = f"{self.payload}{self.comment}"
def _build_raw(self): def _build_raw(self):
""" """
@@ -408,22 +485,18 @@ class ObjectPacket(GPSPacket):
callsign is the station callsign for the object callsign is the station callsign for the object
The frequency, uplink_tone, offset is part of the comment The frequency, uplink_tone, offset is part of the comment
""" """
time_zulu = self._build_time_zulu()
lat = self.convert_latitude(self.latitude)
long = self.convert_longitude(self.longitude)
self.raw = ( self.raw = (
f"{self.from_call}>APZ100:;{self.to_call:9s}" f"{self.from_call}>APZ100:;{self.to_call:9s}"
f"*{time_zulu}z{lat}{self.symbol_table}" f"{self.payload}"
f"{long}{self.symbol}"
) )
if self.comment:
self.raw = f"{self.raw}{self.comment}"
@dataclass() @dataclass()
class WeatherPacket(GPSPacket): class WeatherPacket(GPSPacket):
symbol: str = "_" symbol: str = "_"
wind_speed: float = 0.00
wind_direction: int = 0
wind_gust: float = 0.00 wind_gust: float = 0.00
temperature: float = 0.00 temperature: float = 0.00
# in inches. 1.04 means 1.04 inches # in inches. 1.04 means 1.04 inches
@@ -434,7 +507,7 @@ class WeatherPacket(GPSPacket):
pressure: float = 0.00 pressure: float = 0.00
comment: str = None comment: str = None
def _build_raw(self): def _build_payload(self):
"""Build an uncompressed weather packet """Build an uncompressed weather packet
Format = Format =
@@ -461,16 +534,12 @@ class WeatherPacket(GPSPacket):
""" """
time_zulu = self._build_time_zulu() time_zulu = self._build_time_zulu()
course = "%03u" % self.course
contents = [ contents = [
f"{self.from_call}>{self.to_call},WIDE1-1,WIDE2-1:",
f"@{time_zulu}z{self.latitude}{self.symbol_table}", f"@{time_zulu}z{self.latitude}{self.symbol_table}",
f"{self.longitude}{self.symbol}", f"{self.longitude}{self.symbol}",
# Add CSE = Course f"{self.wind_direction:03d}",
f"{course}",
# Speed = sustained 1 minute wind speed in mph # Speed = sustained 1 minute wind speed in mph
f"{self.symbol_table}", f"{self.speed:03.0f}", f"{self.symbol_table}", f"{self.wind_speed:03.0f}",
# wind gust (peak wind speed in mph in the last 5 minutes) # wind gust (peak wind speed in mph in the last 5 minutes)
f"g{self.wind_gust:03.0f}", f"g{self.wind_gust:03.0f}",
# Temperature in degrees F # Temperature in degrees F
@@ -486,11 +555,32 @@ class WeatherPacket(GPSPacket):
# Barometric pressure (in tenths of millibars/tenths of hPascal) # Barometric pressure (in tenths of millibars/tenths of hPascal)
f"b{self.pressure:05.0f}", f"b{self.pressure:05.0f}",
] ]
if self.comment: if self.comment:
contents.append(self.comment) contents.append(self.comment)
self.payload = "".join(contents)
self.raw = "".join(contents) def _build_raw(self):
self.raw = (
f"{self.from_call}>{self.to_call},WIDE1-1,WIDE2-1:"
f"{self.payload}"
)
class ThirdParty(Packet):
# Holds the encapsulated packet
subpacket: Packet = None
def __repr__(self):
"""Build the repr version of the packet."""
repr_str = (
f"{self.__class__.__name__}:"
f" From: {self.from_call} "
f" To: {self.to_call} "
f" Subpacket: {repr(self.subpacket)}"
)
return repr_str
TYPE_LOOKUP = { TYPE_LOOKUP = {
@@ -503,6 +593,7 @@ TYPE_LOOKUP = {
PACKET_TYPE_STATUS: StatusPacket, PACKET_TYPE_STATUS: StatusPacket,
PACKET_TYPE_BEACON: GPSPacket, PACKET_TYPE_BEACON: GPSPacket,
PACKET_TYPE_UNKNOWN: Packet, PACKET_TYPE_UNKNOWN: Packet,
PACKET_TYPE_THIRDPARTY: ThirdParty,
} }
@@ -529,6 +620,8 @@ def get_packet_type(packet: dict):
elif pkt_format == PACKET_TYPE_UNCOMPRESSED: elif pkt_format == PACKET_TYPE_UNCOMPRESSED:
if packet.get("symbol", None) == "_": if packet.get("symbol", None) == "_":
packet_type = PACKET_TYPE_WX packet_type = PACKET_TYPE_WX
elif pkt_format == PACKET_TYPE_THIRDPARTY:
packet_type = PACKET_TYPE_THIRDPARTY
return packet_type return packet_type
+3 -3
View File
@@ -33,9 +33,9 @@ def get_weather_gov_for_gps(lat, lon):
) )
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}" # f"https://api.weather.gov/points/{lat},{lon}"
) )
LOG.debug(f"Fetching weather '{url2}'") LOG.debug(f"Fetching weather '{url2}'")
response = requests.get(url2, headers=headers) response = requests.get(url2, headers=headers)
+1 -1
View File
@@ -666,7 +666,7 @@ class APRSDEmailThread(threads.APRSDThread):
EmailInfo().delay = 60 EmailInfo().delay = 60
# reset clock # reset clock
LOG.debug("Done looping over Server.fetch, logging out.") LOG.debug("Done looping over Server.fetch, log out.")
self.past = datetime.datetime.now() self.past = datetime.datetime.now()
try: try:
server.logout() server.logout()
+26 -15
View File
@@ -2,6 +2,7 @@ import logging
import re import re
import time import time
from geopy.geocoders import Nominatim
from oslo_config import cfg from oslo_config import cfg
from aprsd import packets, plugin, plugin_utils from aprsd import packets, plugin, plugin_utils
@@ -50,8 +51,28 @@ class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
LOG.error("Didn't get any entries from aprs.fi") LOG.error("Didn't get any entries from aprs.fi")
return "Failed to fetch aprs.fi location" return "Failed to fetch aprs.fi location"
lat = aprs_data["entries"][0]["lat"] lat = float(aprs_data["entries"][0]["lat"])
lon = aprs_data["entries"][0]["lng"] lon = float(aprs_data["entries"][0]["lng"])
# Get some information about their location
try:
tic = time.perf_counter()
geolocator = Nominatim(user_agent="APRSD")
coordinates = f"{lat:0.6f}, {lon:0.6f}"
location = geolocator.reverse(coordinates)
address = location.raw.get("address")
toc = time.perf_counter()
if address:
LOG.info(f"Geopy address {address} took {toc - tic:0.4f}")
if address.get("country_code") == "us":
area_info = f"{address.get('county')}, {address.get('state')}"
else:
# what to do for address for non US?
area_info = f"{address.get('country'), 'Unknown'}"
except Exception as ex:
LOG.error(f"Failed to fetch Geopy address {ex}")
area_info = "Unknown Location"
try: # altitude not always provided try: # altitude not always provided
alt = float(aprs_data["entries"][0]["altitude"]) alt = float(aprs_data["entries"][0]["altitude"])
except Exception: except Exception:
@@ -64,22 +85,12 @@ class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
delta_seconds = time.time() - int(aprs_lasttime_seconds) delta_seconds = time.time() - int(aprs_lasttime_seconds)
delta_hours = delta_seconds / 60 / 60 delta_hours = delta_seconds / 60 / 60
try:
wx_data = plugin_utils.get_weather_gov_for_gps(lat, lon)
except Exception as ex:
LOG.error(f"Couldn't fetch forecast.weather.gov '{ex}'")
wx_data = {"location": {"areaDescription": "Unknown Location"}}
if "location" not in wx_data:
LOG.error(f"Couldn't fetch forecast.weather.gov '{wx_data}'")
wx_data = {"location": {"areaDescription": "Unknown Location"}}
reply = "{}: {} {}' {},{} {}h ago".format( reply = "{}: {} {}' {},{} {}h ago".format(
searchcall, searchcall,
wx_data["location"]["areaDescription"], area_info,
str(altfeet), str(altfeet),
str(lat), f"{lat:0.2f}",
str(lon), f"{lon:0.2f}",
str("%.1f" % round(delta_hours, 1)), str("%.1f" % round(delta_hours, 1)),
).rstrip() ).rstrip()
+11 -4
View File
@@ -26,7 +26,7 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin)
"weather" - returns weather near the calling callsign "weather" - returns weather near the calling callsign
""" """
command_regex = r"^([w]|[w]\s|[w][x]|[w][x]\s|weather)" command_regex = r"^([w][x]|[w][x]\s|weather)"
command_name = "USWeather" command_name = "USWeather"
short_description = "Provide USA only weather of GPS Beacon location" short_description = "Provide USA only weather of GPS Beacon location"
@@ -37,11 +37,18 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin)
def process(self, packet): def process(self, packet):
LOG.info("Weather Plugin") LOG.info("Weather Plugin")
fromcall = packet.from_call fromcall = packet.from_call
message = packet.get("message_text", None)
# message = packet.get("message_text", None) # message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0") # ack = packet.get("msgNo", "0")
a = re.search(r"^.*\s+(.*)", message)
if a is not None:
searchcall = a.group(1)
searchcall = searchcall.upper()
else:
searchcall = fromcall
api_key = CONF.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, searchcall)
except Exception as ex: except Exception as ex:
LOG.error(f"Failed to fetch aprs.fi data {ex}") LOG.error(f"Failed to fetch aprs.fi data {ex}")
return "Failed to fetch aprs.fi location" return "Failed to fetch aprs.fi location"
@@ -92,7 +99,7 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
""" """
command_regex = r"^([m]|[m]|[m]\s|metar)" command_regex = r"^([m]|[M]|[m]\s|metar)"
command_name = "USMetar" command_name = "USMetar"
short_description = "USA only METAR of GPS Beacon location" short_description = "USA only METAR of GPS Beacon location"
@@ -182,7 +189,7 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
""" """
command_regex = r"^([w]|[w]\s|[w][x]|[w][x]\s|weather)" command_regex = r"^([w][x]|[w][x]\s|weather)"
command_name = "OpenWeatherMap" command_name = "OpenWeatherMap"
short_description = "OpenWeatherMap weather of GPS Beacon location" short_description = "OpenWeatherMap weather of GPS Beacon location"
+30 -15
View File
@@ -16,31 +16,47 @@ class RPCClient:
_instance = None _instance = None
_rpc_client = None _rpc_client = None
ip = None
port = None
magic_word = 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)
return cls._instance return cls._instance
def __init__(self): def __init__(self, ip=None, port=None, magic_word=None):
if ip:
self.ip = ip
else:
self.ip = CONF.rpc_settings.ip
if port:
self.port = int(port)
else:
self.port = CONF.rpc_settings.port
if magic_word:
self.magic_word = magic_word
else:
self.magic_word = CONF.rpc_settings.magic_word
self._check_settings() self._check_settings()
self.get_rpc_client() self.get_rpc_client()
def _check_settings(self): def _check_settings(self):
if not CONF.rpc_settings.enabled: if not CONF.rpc_settings.enabled:
LOG.error("RPC is not enabled, no way to get stats!!") LOG.warning("RPC is not enabled, no way to get stats!!")
if CONF.rpc_settings.magic_word == conf.common.APRSD_DEFAULT_MAGIC_WORD: if self.magic_word == conf.common.APRSD_DEFAULT_MAGIC_WORD:
LOG.warning("You are using the default RPC magic word!!!") LOG.warning("You are using the default RPC magic word!!!")
LOG.warning("edit aprsd.conf and change rpc_settings.magic_word") LOG.warning("edit aprsd.conf and change rpc_settings.magic_word")
def _rpyc_connect( LOG.debug(f"RPC Client: {self.ip}:{self.port} {self.magic_word}")
self, host, port,
service=rpyc.VoidService,
config={}, ipv6=False,
keepalive=False, authorizer=None,
):
print(f"Connecting to RPC host {host}:{port}") def _rpyc_connect(
self, host, port, service=rpyc.VoidService,
config={}, ipv6=False,
keepalive=False, authorizer=None, ):
LOG.info(f"Connecting to RPC host '{host}:{port}'")
try: try:
s = rpc.AuthSocketStream.connect( s = rpc.AuthSocketStream.connect(
host, port, ipv6=ipv6, keepalive=keepalive, host, port, ipv6=ipv6, keepalive=keepalive,
@@ -48,16 +64,15 @@ class RPCClient:
) )
return rpyc.utils.factory.connect_stream(s, service, config=config) return rpyc.utils.factory.connect_stream(s, service, config=config)
except ConnectionRefusedError: except ConnectionRefusedError:
LOG.error(f"Failed to connect to RPC host {host}") LOG.error(f"Failed to connect to RPC host '{host}:{port}'")
return None return None
def get_rpc_client(self): def get_rpc_client(self):
if not self._rpc_client: if not self._rpc_client:
magic = CONF.rpc_settings.magic_word
self._rpc_client = self._rpyc_connect( self._rpc_client = self._rpyc_connect(
CONF.rpc_settings.ip, self.ip,
CONF.rpc_settings.port, self.port,
authorizer=lambda sock: sock.send(magic.encode()), authorizer=lambda sock: sock.send(self.magic_word.encode()),
) )
return self._rpc_client return self._rpc_client
+14 -3
View File
@@ -18,7 +18,10 @@ LOG = logging.getLogger("APRSD")
def magic_word_authenticator(sock): def magic_word_authenticator(sock):
magic = sock.recv(len(CONF.rpc_settings.magic_word)).decode() magic = sock.recv(len(CONF.rpc_settings.magic_word)).decode()
if magic != CONF.rpc_settings.magic_word: if magic != CONF.rpc_settings.magic_word:
raise AuthenticationError(f"wrong magic word {magic}") raise AuthenticationError(
f"wrong magic word passed in '{magic}'"
f" != '{CONF.rpc_settings.magic_word}'",
)
return sock, None return sock, None
@@ -60,32 +63,40 @@ class APRSDService(rpyc.Service):
@rpyc.exposed @rpyc.exposed
def get_stats(self): def get_stats(self):
LOG.info("get_stats")
stat = stats.APRSDStats() stat = stats.APRSDStats()
stats_dict = stat.stats() stats_dict = stat.stats()
LOG.debug(stats_dict) return_str = json.dumps(stats_dict, indent=4, sort_keys=True, default=str)
return json.dumps(stats_dict, indent=4, sort_keys=True, default=str) LOG.info(f"get_stats: return size {len(return_str)}")
return return_str
@rpyc.exposed @rpyc.exposed
def get_stats_obj(self): def get_stats_obj(self):
LOG.info("get_stats_obj")
return stats.APRSDStats() return stats.APRSDStats()
@rpyc.exposed @rpyc.exposed
def get_packet_list(self): def get_packet_list(self):
LOG.info("get_packet_list")
return packets.PacketList() return packets.PacketList()
@rpyc.exposed @rpyc.exposed
def get_packet_track(self): def get_packet_track(self):
LOG.info("get_packet_track")
return packets.PacketTrack() return packets.PacketTrack()
@rpyc.exposed @rpyc.exposed
def get_watch_list(self): def get_watch_list(self):
LOG.info("get_watch_list")
return packets.WatchList() return packets.WatchList()
@rpyc.exposed @rpyc.exposed
def get_seen_list(self): def get_seen_list(self):
LOG.info("get_seen_list")
return packets.SeenList() return packets.SeenList()
@rpyc.exposed @rpyc.exposed
def get_log_entries(self): def get_log_entries(self):
LOG.info("get_log_entries")
entries = log_monitor.LogEntries().get_all_and_purge() entries = log_monitor.LogEntries().get_all_and_purge()
return json.dumps(entries, default=str) return json.dumps(entries, default=str)
+17 -4
View File
@@ -29,6 +29,8 @@ class APRSDStats:
_mem_current = 0 _mem_current = 0
_mem_peak = 0 _mem_peak = 0
_thread_info = {}
_pkt_cnt = { _pkt_cnt = {
"Packet": { "Packet": {
"tx": 0, "tx": 0,
@@ -95,6 +97,15 @@ class APRSDStats:
def set_memory_peak(self, memory): def set_memory_peak(self, memory):
self._mem_peak = memory self._mem_peak = memory
@wrapt.synchronized(lock)
def set_thread_info(self, thread_info):
self._thread_info = thread_info
@wrapt.synchronized(lock)
@property
def thread_info(self):
return self._thread_info
@wrapt.synchronized(lock) @wrapt.synchronized(lock)
@property @property
def aprsis_server(self): def aprsis_server(self):
@@ -207,6 +218,7 @@ class APRSDStats:
"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),
"memory_peak_str": utils.human_size(self.memory_peak), "memory_peak_str": utils.human_size(self.memory_peak),
"threads": self._thread_info,
"watch_list": wl.get_all(), "watch_list": wl.get_all(),
"seen_list": sl.get_all(), "seen_list": sl.get_all(),
}, },
@@ -216,9 +228,10 @@ class APRSDStats:
"last_update": last_aprsis_keepalive, "last_update": last_aprsis_keepalive,
}, },
"packets": { "packets": {
"tracked": int(pl.total_tx() + pl.total_rx()), "total_tracked": int(pl.total_tx() + pl.total_rx()),
"sent": int(pl.total_tx()), "total_sent": int(pl.total_tx()),
"received": int(pl.total_rx()), "total_received": int(pl.total_rx()),
"by_type": self._pkt_cnt,
}, },
"messages": { "messages": {
"sent": self._pkt_cnt["MessagePacket"]["tx"], "sent": self._pkt_cnt["MessagePacket"]["tx"],
@@ -233,7 +246,7 @@ class APRSDStats:
}, },
"plugins": plugin_stats, "plugins": plugin_stats,
} }
LOG.debug(f"STATS = {stats}") LOG.info("APRSD Stats: DONE")
return stats return stats
def __str__(self): def __str__(self):
+7
View File
@@ -1,4 +1,5 @@
import abc import abc
import datetime
import logging import logging
import threading import threading
@@ -50,6 +51,7 @@ class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
super().__init__(name=name) super().__init__(name=name)
self.thread_stop = False self.thread_stop = False
APRSDThreadList().add(self) APRSDThreadList().add(self)
self._last_loop = datetime.datetime.now()
def _should_quit(self): def _should_quit(self):
""" see if we have a quit message from the global queue.""" """ see if we have a quit message from the global queue."""
@@ -70,10 +72,15 @@ class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
out = f"Thread <{self.__class__.__name__}({self.name}) Alive? {self.is_alive()}>" out = f"Thread <{self.__class__.__name__}({self.name}) Alive? {self.is_alive()}>"
return out return out
def loop_age(self):
"""How old is the last loop call?"""
return datetime.datetime.now() - self._last_loop
def run(self): def run(self):
LOG.debug("Starting") LOG.debug("Starting")
while not self._should_quit(): while not self._should_quit():
can_loop = self.loop() can_loop = self.loop()
self._last_loop = datetime.datetime.now()
if not can_loop: if not can_loop:
self.stop() self.stop()
self._cleanup() self._cleanup()
+9 -1
View File
@@ -65,12 +65,20 @@ class KeepAliveThread(APRSDThread):
) )
LOG.info(keepalive) LOG.info(keepalive)
thread_out = [] thread_out = []
thread_info = {}
for thread in thread_list.threads_list: for thread in thread_list.threads_list:
alive = thread.is_alive() alive = thread.is_alive()
thread_out.append(f"{thread.__class__.__name__}:{alive}") age = thread.loop_age()
key = thread.__class__.__name__
thread_out.append(f"{key}:{alive}:{age}")
if key not in thread_info:
thread_info[key] = {}
thread_info[key]["alive"] = alive
thread_info[key]["age"] = age
if not alive: if not alive:
LOG.error(f"Thread {thread}") LOG.error(f"Thread {thread}")
LOG.info(",".join(thread_out)) LOG.info(",".join(thread_out))
stats_obj.set_thread_info(thread_info)
# check the APRS connection # check the APRS connection
cl = client.factory.create() cl = client.factory.create()
+1 -1
View File
@@ -4,7 +4,7 @@ import threading
import wrapt import wrapt
from aprsd import threads from aprsd import threads
from aprsd.logging import log from aprsd.log import log
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
+25 -6
View File
@@ -3,9 +3,13 @@ import logging
import time import time
from oslo_config import cfg from oslo_config import cfg
from ratelimiter import RateLimiter from rush import quota, throttle
from rush.contrib import decorator
from rush.limiters import periodic
from rush.stores import dictionary
from aprsd import client from aprsd import client
from aprsd import conf # noqa
from aprsd import threads as aprsd_threads from aprsd import threads as aprsd_threads
from aprsd.packets import core, tracker from aprsd.packets import core, tracker
@@ -13,10 +17,25 @@ from aprsd.packets import core, tracker
CONF = cfg.CONF CONF = cfg.CONF
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
msg_t = throttle.Throttle(
limiter=periodic.PeriodicLimiter(
store=dictionary.DictionaryStore(),
),
rate=quota.Quota.per_second(
count=CONF.msg_rate_limit_period,
),
)
ack_t = throttle.Throttle(
limiter=periodic.PeriodicLimiter(
store=dictionary.DictionaryStore(),
),
rate=quota.Quota.per_second(
count=CONF.ack_rate_limit_period,
),
)
def limited(until): msg_throttle_decorator = decorator.ThrottleDecorator(throttle=msg_t)
duration = int(round(until - time.time())) ack_throttle_decorator = decorator.ThrottleDecorator(throttle=ack_t)
LOG.debug(f"Rate limited, sleeping for {duration:d} seconds")
def send(packet: core.Packet, direct=False, aprs_client=None): def send(packet: core.Packet, direct=False, aprs_client=None):
@@ -30,7 +49,7 @@ def send(packet: core.Packet, direct=False, aprs_client=None):
_send_packet(packet, direct=direct, aprs_client=aprs_client) _send_packet(packet, direct=direct, aprs_client=aprs_client)
@RateLimiter(max_calls=1, period=CONF.msg_rate_limit_period, callback=limited) @msg_throttle_decorator.sleep_and_retry
def _send_packet(packet: core.Packet, direct=False, aprs_client=None): def _send_packet(packet: core.Packet, direct=False, aprs_client=None):
if not direct: if not direct:
thread = SendPacketThread(packet=packet) thread = SendPacketThread(packet=packet)
@@ -39,7 +58,7 @@ def _send_packet(packet: core.Packet, direct=False, aprs_client=None):
_send_direct(packet, aprs_client=aprs_client) _send_direct(packet, aprs_client=aprs_client)
@RateLimiter(max_calls=1, period=CONF.ack_rate_limit_period, callback=limited) @ack_throttle_decorator.sleep_and_retry
def _send_ack(packet: core.AckPacket, direct=False, aprs_client=None): def _send_ack(packet: core.AckPacket, direct=False, aprs_client=None):
if not direct: if not direct:
thread = SendAckThread(packet=packet) thread = SendAckThread(packet=packet)
+1 -1
View File
@@ -3,7 +3,7 @@
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> <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"> <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://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.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.socket.io/4.7.1/socket.io.min.js" integrity="sha512-+NaO7d6gQ1YPxvc/qHIqZEchjGm207SszoNeMgppoqD/67fEqmc1edS8zrbxPD+4RQI3gDgT/83ihpFW61TG/Q==" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.bundle.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.bundle.js"></script>
+5
View File
@@ -26,6 +26,11 @@
start_update(); start_update();
init_chat(); init_chat();
reset_Tabs(); reset_Tabs();
if (location.protocol != 'https:') {
// Have to disable the beacon button.
$('#send_beacon').prop('disabled', true);
}
}); });
</script> </script>
</head> </head>
+368
View File
@@ -0,0 +1,368 @@
import datetime
import json
import logging
from logging.handlers import RotatingFileHandler
import time
import flask
from flask import Flask
from flask.logging import default_handler
from flask_httpauth import HTTPBasicAuth
from oslo_config import cfg
import socketio
from werkzeug.security import check_password_hash
import aprsd
from aprsd import cli_helper, client, conf, packets, plugin, threads
from aprsd.log import rich as aprsd_logging
from aprsd.rpc import client as aprsd_rpc_client
CONF = cfg.CONF
LOG = logging.getLogger("gunicorn.access")
auth = HTTPBasicAuth()
users = {}
app = Flask(
"aprsd",
static_url_path="/static",
static_folder="web/admin/static",
template_folder="web/admin/templates",
)
bg_thread = None
app.config["SECRET_KEY"] = "secret!"
# HTTPBasicAuth doesn't work on a class method.
# This has to be out here. Rely on the APRSDFlask
# class to initialize the users from the config
@auth.verify_password
def verify_password(username, password):
global users
if username in users and check_password_hash(users.get(username), password):
return username
def _stats():
track = aprsd_rpc_client.RPCClient().get_packet_track()
now = datetime.datetime.now()
time_format = "%m-%d-%Y %H:%M:%S"
stats_dict = aprsd_rpc_client.RPCClient().get_stats_dict()
if not stats_dict:
stats_dict = {
"aprsd": {},
"aprs-is": {"server": ""},
"messages": {
"sent": 0,
"received": 0,
},
"email": {
"sent": 0,
"received": 0,
},
"seen_list": {
"sent": 0,
"received": 0,
},
}
# Convert the watch_list entries to age
wl = aprsd_rpc_client.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
packet_list = aprsd_rpc_client.RPCClient().get_packet_list()
rx = tx = 0
if packet_list:
rx = packet_list.total_rx()
tx = packet_list.total_tx()
stats_dict["packets"] = {
"sent": tx,
"received": rx,
}
if track:
size_tracker = len(track)
else:
size_tracker = 0
result = {
"time": now.strftime(time_format),
"size_tracker": size_tracker,
"stats": stats_dict,
}
return result
@app.route("/stats")
def stats():
LOG.debug("/stats called")
return json.dumps(_stats())
@app.route("/")
def index():
stats = _stats()
LOG.debug(stats)
wl = aprsd_rpc_client.RPCClient().get_watch_list()
if wl and wl.is_enabled():
watch_count = len(wl)
watch_age = wl.max_delta()
else:
watch_count = 0
watch_age = 0
sl = aprsd_rpc_client.RPCClient().get_seen_list()
if sl:
seen_count = len(sl)
else:
seen_count = 0
pm = plugin.PluginManager()
plugins = pm.get_plugins()
plugin_count = len(plugins)
if CONF.aprs_network.enabled:
transport = "aprs-is"
aprs_connection = (
"APRS-IS Server: <a href='http://status.aprs2.net' >"
"{}</a>".format(stats["stats"]["aprs-is"]["server"])
)
else:
# We might be connected to a KISS socket?
if client.KISSClient.kiss_enabled():
transport = client.KISSClient.transport()
if transport == client.TRANSPORT_TCPKISS:
aprs_connection = (
"TCPKISS://{}:{}".format(
CONF.kiss_tcp.host,
CONF.kiss_tcp.port,
)
)
elif transport == client.TRANSPORT_SERIALKISS:
aprs_connection = (
"SerialKISS://{}@{} baud".format(
CONF.kiss_serial.device,
CONF.kiss_serial.baudrate,
)
)
stats["transport"] = transport
stats["aprs_connection"] = aprs_connection
entries = conf.conf_to_dict()
return flask.render_template(
"index.html",
initial_stats=stats,
aprs_connection=aprs_connection,
callsign=CONF.callsign,
version=aprsd.__version__,
config_json=json.dumps(
entries, indent=4,
sort_keys=True, default=str,
),
watch_count=watch_count,
watch_age=watch_age,
seen_count=seen_count,
plugin_count=plugin_count,
)
@auth.login_required
def messages():
track = packets.PacketTrack()
msgs = []
for id in track:
LOG.info(track[id].dict())
msgs.append(track[id].dict())
return flask.render_template("messages.html", messages=json.dumps(msgs))
@auth.login_required
@app.route("/packets")
def get_packets():
LOG.debug("/packets called")
packet_list = aprsd_rpc_client.RPCClient().get_packet_list()
if packet_list:
packets = packet_list.get()
tmp_list = []
for pkt in packets:
tmp_list.append(pkt.json)
return json.dumps(tmp_list)
else:
return json.dumps([])
@auth.login_required
@app.route("/plugins")
def plugins():
LOG.debug("/plugins called")
pm = plugin.PluginManager()
pm.reload_plugins()
return "reloaded"
@auth.login_required
@app.route("/save")
def save():
"""Save the existing queue to disk."""
track = packets.PacketTrack()
track.save()
return json.dumps({"messages": "saved"})
class LogUpdateThread(threads.APRSDThread):
def __init__(self):
super().__init__("LogUpdate")
def loop(self):
if sio:
log_entries = aprsd_rpc_client.RPCClient().get_log_entries()
if log_entries:
LOG.info(f"Sending log entries! {len(log_entries)}")
for entry in log_entries:
sio.emit(
"log_entry", entry,
namespace="/logs",
)
time.sleep(5)
return True
class LoggingNamespace(socketio.Namespace):
log_thread = None
def on_connect(self, sid, environ):
global sio
LOG.debug(f"LOG on_connect {sid}")
sio.emit(
"connected", {"data": "/logs Connected"},
namespace="/logs",
)
self.log_thread = LogUpdateThread()
self.log_thread.start()
def on_disconnect(self, sid):
LOG.debug(f"LOG Disconnected {sid}")
if self.log_thread:
self.log_thread.stop()
def setup_logging(flask_app, loglevel):
global app, LOG
log_level = conf.log.LOG_LEVELS[loglevel]
app.logger.setLevel(log_level)
flask_app.logger.removeHandler(default_handler)
date_format = CONF.logging.date_format
if CONF.logging.rich_logging:
log_format = "%(message)s"
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
rh = aprsd_logging.APRSDRichHandler(
show_thread=True, thread_width=15,
rich_tracebacks=True, omit_repeated_times=False,
)
rh.setFormatter(log_formatter)
app.logger.addHandler(rh)
log_file = CONF.logging.logfile
if log_file:
log_format = CONF.logging.logformat
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
fh = RotatingFileHandler(
log_file, maxBytes=(10248576 * 5),
backupCount=4,
)
fh.setFormatter(log_formatter)
app.logger.addHandler(fh)
LOG = app.logger
def init_app(config_file=None, log_level=None):
default_config_file = cli_helper.DEFAULT_CONFIG_FILE
if not config_file:
config_file = default_config_file
CONF(
[], project="aprsd", version=aprsd.__version__,
default_config_files=[config_file],
)
if not log_level:
log_level = CONF.logging.log_level
return log_level
if __name__ == "__main__":
async_mode = "threading"
sio = socketio.Server(logger=True, async_mode=async_mode)
app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app)
log_level = init_app(log_level="DEBUG")
setup_logging(app, log_level)
sio.register_namespace(LoggingNamespace("/logs"))
CONF.log_opt_values(LOG, logging.DEBUG)
app.run(
threaded=True,
debug=False,
port=CONF.admin.web_port,
host=CONF.admin.web_ip,
)
if __name__ == "uwsgi_file_aprsd_wsgi":
# Start with
# uwsgi --http :8000 --gevent 1000 --http-websockets --master -w aprsd.wsgi --callable app
async_mode = "gevent_uwsgi"
sio = socketio.Server(logger=True, async_mode=async_mode)
app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app)
log_level = init_app(
log_level="DEBUG",
config_file="/config/aprsd.conf",
)
setup_logging(app, log_level)
sio.register_namespace(LoggingNamespace("/logs"))
CONF.log_opt_values(LOG, logging.DEBUG)
if __name__ == "aprsd.wsgi":
# set async_mode to 'threading', 'eventlet', 'gevent' or 'gevent_uwsgi' to
# force a mode else, the best mode is selected automatically from what's
# installed
async_mode = "gevent_uwsgi"
sio = socketio.Server(logger=True, async_mode=async_mode)
app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app)
log_level = init_app(config_file="/config/aprsd.conf", log_level="DEBUG")
setup_logging(app, log_level)
sio.register_namespace(LoggingNamespace("/logs"))
CONF.log_opt_values(LOG, logging.DEBUG)
+60 -61
View File
@@ -1,88 +1,87 @@
# #
# 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 dev-requirements.in
# #
add-trailing-comma==2.4.0 # via gray add-trailing-comma==3.0.1 # via gray
alabaster==0.7.13 # via sphinx alabaster==0.7.13 # via sphinx
attrs==22.2.0 # via jsonschema, pytest attrs==23.1.0 # via jsonschema, referencing
autoflake==1.5.3 # via gray autoflake==1.5.3 # via gray
babel==2.11.0 # via sphinx babel==2.12.1 # via sphinx
black==22.12.0 # via gray black==23.7.0 # via gray
build==0.10.0 # via pip-tools build==0.10.0 # via pip-tools
cachetools==5.2.1 # via tox cachetools==5.3.1 # via tox
certifi==2022.12.7 # via requests certifi==2023.7.22 # via requests
cfgv==3.3.1 # via pre-commit cfgv==3.4.0 # via pre-commit
chardet==5.1.0 # via tox chardet==5.2.0 # via tox
charset-normalizer==3.0.1 # via requests charset-normalizer==3.2.0 # via requests
click==8.1.3 # via black, pip-tools click==8.1.6 # 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.7 # via gray
coverage[toml]==7.0.5 # via pytest-cov coverage[toml]==7.3.0 # via pytest-cov
distlib==0.3.6 # via virtualenv distlib==0.3.7 # via virtualenv
docutils==0.19 # via sphinx docutils==0.20.1 # via sphinx
exceptiongroup==1.1.0 # via pytest exceptiongroup==1.1.3 # via pytest
filelock==3.9.0 # via tox, virtualenv filelock==3.12.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.1.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.13 # via pre-commit identify==2.5.26 # 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==6.0.0 # via sphinx importlib-resources==6.0.1 # via fixit
importlib-resources==5.10.2 # via fixit
iniconfig==2.0.0 # via pytest iniconfig==2.0.0 # via pytest
isort==5.11.4 # via -r dev-requirements.in, gray isort==5.12.0 # 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.19.0 # via fixit
libcst==0.4.9 # via fixit jsonschema-specifications==2023.7.1 # via jsonschema
markupsafe==2.1.2 # via jinja2 libcst==1.0.1 # via fixit
markupsafe==2.1.3 # via jinja2
mccabe==0.7.0 # via flake8 mccabe==0.7.0 # via flake8
mypy==0.991 # via -r dev-requirements.in mypy==1.5.0 # via -r dev-requirements.in
mypy-extensions==0.4.3 # via black, mypy, typing-inspect mypy-extensions==1.0.0 # via black, mypy, typing-inspect
nodeenv==1.7.0 # via pre-commit nodeenv==1.8.0 # via pre-commit
packaging==23.0 # via build, pyproject-api, pytest, sphinx, tox packaging==23.1 # via black, build, pyproject-api, pytest, sphinx, tox
pathspec==0.10.3 # via black pathspec==0.11.2 # via black
pep8-naming==0.13.3 # via -r dev-requirements.in pep8-naming==0.13.3 # via -r dev-requirements.in
pip-tools==6.12.1 # via -r dev-requirements.in pip-tools==7.3.0 # via -r dev-requirements.in
platformdirs==2.6.2 # via black, tox, virtualenv platformdirs==3.10.0 # via black, tox, virtualenv
pluggy==1.0.0 # via pytest, tox pluggy==1.2.0 # via pytest, tox
pre-commit==2.21.0 # via -r dev-requirements.in pre-commit==3.3.3 # via -r dev-requirements.in
pycodestyle==2.10.0 # via flake8 pycodestyle==2.11.0 # via flake8
pyflakes==3.0.1 # via autoflake, flake8 pyflakes==3.1.0 # via autoflake, flake8
pygments==2.14.0 # via rich, sphinx pygments==2.16.1 # via rich, sphinx
pyproject-api==1.5.0 # via tox pyproject-api==1.5.3 # via tox
pyproject-hooks==1.0.0 # via build pyproject-hooks==1.0.0 # via build
pyrsistent==0.19.3 # via jsonschema pytest==7.4.0 # via -r dev-requirements.in, pytest-cov
pytest==7.2.1 # via -r dev-requirements.in, pytest-cov pytest-cov==4.1.0 # via -r dev-requirements.in
pytest-cov==4.0.0 # via -r dev-requirements.in pyupgrade==3.10.1 # via gray
pytz==2022.7.1 # via babel pyyaml==6.0.1 # via fixit, libcst, pre-commit
pyupgrade==3.3.1 # via gray referencing==0.30.2 # via jsonschema, jsonschema-specifications
pyyaml==6.0 # via fixit, libcst, pre-commit requests==2.31.0 # via sphinx
requests==2.28.2 # via sphinx
rich==12.6.0 # via gray rich==12.6.0 # via gray
rpds-py==0.9.2 # via jsonschema, referencing
snowballstemmer==2.2.0 # via sphinx snowballstemmer==2.2.0 # via sphinx
sphinx==6.1.3 # via -r dev-requirements.in sphinx==7.1.2 # via -r dev-requirements.in, sphinxcontrib-applehelp, sphinxcontrib-devhelp, sphinxcontrib-htmlhelp, sphinxcontrib-qthelp, sphinxcontrib-serializinghtml
sphinxcontrib-applehelp==1.0.3 # via sphinx sphinxcontrib-applehelp==1.0.7 # via sphinx
sphinxcontrib-devhelp==1.0.2 # via sphinx sphinxcontrib-devhelp==1.0.5 # via sphinx
sphinxcontrib-htmlhelp==2.0.0 # via sphinx sphinxcontrib-htmlhelp==2.0.4 # via sphinx
sphinxcontrib-jsmath==1.0.1 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx
sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-qthelp==1.0.6 # via sphinx
sphinxcontrib-serializinghtml==1.1.5 # via sphinx sphinxcontrib-serializinghtml==1.1.8 # via sphinx
tokenize-rt==5.0.0 # via add-trailing-comma, pyupgrade tokenize-rt==5.2.0 # via add-trailing-comma, pyupgrade
toml==0.10.2 # via autoflake toml==0.10.2 # via autoflake
tomli==2.0.1 # via black, build, coverage, mypy, pyproject-api, pyproject-hooks, pytest, tox tomli==2.0.1 # via black, build, coverage, mypy, pip-tools, pyproject-api, pyproject-hooks, pytest, tox
tox==4.3.5 # via -r dev-requirements.in tox==4.8.0 # via -r dev-requirements.in
typing-extensions==4.4.0 # via black, libcst, mypy, typing-inspect typing-extensions==4.7.1 # via libcst, mypy, typing-inspect
typing-inspect==0.8.0 # via libcst typing-inspect==0.9.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.14 # via requests urllib3==2.0.4 # via requests
virtualenv==20.17.1 # via pre-commit, tox virtualenv==20.24.3 # via pre-commit, tox
wheel==0.38.4 # via pip-tools wheel==0.41.1 # 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
+43 -41
View File
@@ -1,59 +1,61 @@
#FROM python:3-bullseye as aprsd FROM python:3.11-slim as build
FROM ubuntu:22.04 as aprsd
# Dockerfile for building a container during aprsd development. ARG VERSION=3.1.0
ARG UID
ARG GID
ARG TZ
ARG VERSION=3.0.0
ARG BUILDX_QEMU_ENV
ENV APRS_USER=aprs
ENV HOME=/home/aprs
ENV TZ=${TZ:-US/Eastern} ENV TZ=${TZ:-US/Eastern}
ENV UID=${UID:-1000}
ENV GID=${GID:-1000}
ENV LC_ALL=C.UTF-8 ENV LC_ALL=C.UTF-8
ENV LANG=C.UTF-8 ENV LANG=C.UTF-8
ENV APRSD_PIP_VERSION=${VERSION} ENV APRSD_PIP_VERSION=${VERSION}
ENV PIP_DEFAULT_TIMEOUT=100 \
# Allow statements and log messages to immediately appear
PYTHONUNBUFFERED=1 \
# disable a pip version check to reduce run-time & log-spam
PIP_DISABLE_PIP_VERSION_CHECK=1 \
# cache is useless in docker image, so disable to reduce image size
PIP_NO_CACHE_DIR=1
ENV DEBIAN_FRONTEND=noninteractive
RUN apt update
RUN apt install -y git build-essential
RUN apt install -y libffi-dev python3-dev libssl-dev libxml2-dev libxslt-dev
RUN apt install -y python3 python3-pip python3-dev python3-lxml
RUN pip3 install -U pip RUN set -ex \
# Create a non-root user
&& addgroup --system --gid 1001 appgroup \
&& useradd --uid 1001 --gid 1001 -s /usr/bin/bash -m -d /app appuser \
# Upgrade the package index and install security upgrades
&& apt-get update \
&& apt-get upgrade -y \
&& apt-get install -y git build-essential curl vim libffi-dev \
python3-dev libssl-dev libxml2-dev libxslt-dev telnet sudo \
# Install dependencies
# Clean up
&& apt-get autoremove -y \
&& apt-get clean -y
RUN addgroup --gid $GID $APRS_USER
RUN useradd -m -u $UID -g $APRS_USER $APRS_USER
# Handle an extremely specific issue when building the cryptography package for ### Final stage
# 32-bit architectures within QEMU running on a 64-bit host (issue #30). FROM build as final
RUN if [ "${BUILDX_QEMU_ENV}" = "true" -a "$(getconf LONG_BIT)" = "32" ]; then \ WORKDIR /app
pip3 install -U cryptography==3.3.2; \
else \
pip3 install cryptography ;\
fi
# Ensure /config is there with a default config file
USER root
# Install aprsd
RUN pip3 install aprsd==$APRSD_PIP_VERSION RUN pip3 install aprsd==$APRSD_PIP_VERSION
RUN mkdir -p /config RUN pip install gevent uwsgi
RUN which aprsd
RUN mkdir /config
RUN chown -R appuser:appgroup /app
RUN chown -R appuser:appgroup /config
USER appuser
RUN which aprsd
RUN aprsd sample-config > /config/aprsd.conf RUN aprsd sample-config > /config/aprsd.conf
RUN chown -R $APRS_USER:$APRS_USER /config
RUN chown -R $APRS_USER:$APRS_USER $HOME
# override this to run another configuration ADD bin/run.sh /app
ENV CONF default ADD bin/listen.sh /app
VOLUME ["/config", "/plugins"] ADD bin/admin.sh /app
USER $APRS_USER # For the web admin interface
ADD bin/run.sh /usr/local/bin EXPOSE 8001
ADD bin/listen.sh /usr/local/bin
ENTRYPOINT ["/usr/local/bin/run.sh"] ENTRYPOINT ["/app/run.sh"]
VOLUME ["/config"]
# Set the user to run the application
USER appuser
HEALTHCHECK --interval=5m --timeout=12s --start-period=30s \ HEALTHCHECK --interval=5m --timeout=12s --start-period=30s \
CMD aprsd healthcheck --config /config/aprsd.conf CMD aprsd healthcheck --config /config/aprsd.conf
+43 -57
View File
@@ -1,71 +1,57 @@
# syntax=docker/dockerfile:1 FROM python:3.11-slim as build
FROM ubuntu:22.04
# Dockerfile for building a container during aprsd development.
ARG BRANCH=master ARG BRANCH=master
ARG UID
ARG GID
ARG BUILDX_QEMU_ENV ARG BUILDX_QEMU_ENV
ENV APRS_USER=aprs
ENV HOME=/home/aprs
ENV APRSD=http://github.com/craigerl/aprsd.git
ENV APRSD_BRANCH=${BRANCH:-master} ENV APRSD_BRANCH=${BRANCH:-master}
ENV VIRTUAL_ENV=$HOME/.venv3
ENV UID=${UID:-1000}
ENV GID=${GID:-1000}
ENV DEBIAN_FRONTEND=noninteractive ENV PIP_DEFAULT_TIMEOUT=100 \
ENV INSTALL=$HOME/install # Allow statements and log messages to immediately appear
RUN apt update PYTHONUNBUFFERED=1 \
RUN apt install -y --no-install-recommends git build-essential bash fortune # disable a pip version check to reduce run-time & log-spam
RUN apt install -y libffi-dev python3-dev libssl-dev libxml2-dev libxslt-dev PIP_DISABLE_PIP_VERSION_CHECK=1 \
RUN apt install -y python3 python3-pip python3-dev python3-lxml # cache is useless in docker image, so disable to reduce image size
#RUN apt-get clean PIP_NO_CACHE_DIR=1
RUN apt-get -o Dpkg::Options::="--force-confmiss" install --reinstall netbase
RUN addgroup --gid 1001 $APRS_USER
RUN useradd -m -u $UID -g $APRS_USER $APRS_USER
ENV LC_ALL=C.UTF-8 RUN set -ex \
ENV LANG=C.UTF-8 # Create a non-root user
&& addgroup --system --gid 1001 appgroup \
&& useradd --uid 1001 --gid 1001 -s /usr/bin/bash -m -d /app appuser \
# Upgrade the package index and install security upgrades
&& apt-get update \
&& apt-get upgrade -y \
&& apt-get install -y git build-essential curl vim libffi-dev \
python3-dev libssl-dev libxml2-dev libxslt-dev telnet sudo \
# Install dependencies
# Clean up
&& apt-get autoremove -y \
&& apt-get clean -y
WORKDIR $HOME
USER $APRS_USER
RUN pip install wheel
#RUN python3 -m venv $VIRTUAL_ENV
#ENV PATH="$VIRTUAL_ENV/bin:$PATH"
RUN echo "export PATH=\$PATH:\$HOME/.local/bin" >> $HOME/.bashrc
RUN cat $HOME/.bashrc
USER root ### Final stage
WORKDIR $HOME FROM build as final
# Handle an extremely specific issue when building the cryptography package for WORKDIR /app
# 32-bit architectures within QEMU running on a 64-bit host (issue #30).
RUN if [ "${BUILDX_QEMU_ENV}" = "true" -a "$(getconf LONG_BIT)" = "32" ]; then \ RUN git clone -b $APRSD_BRANCH https://github.com/craigerl/aprsd
pip3 install -U cryptography==3.3.2; \ RUN cd aprsd && pip install --no-cache-dir .
else \ RUN pip install gevent uwsgi
pip3 install cryptography ;\ RUN which aprsd
fi RUN mkdir /config
RUN mkdir $INSTALL RUN chown -R appuser:appgroup /app
RUN git clone -b $BRANCH $APRSD $INSTALL/aprsd RUN chown -R appuser:appgroup /config
RUN cd $INSTALL/aprsd && pip3 install -v . USER appuser
RUN ls -al /usr/local/bin
RUN ls -al /usr/bin
RUN which aprsd RUN which aprsd
RUN mkdir -p /config
RUN aprsd sample-config > /config/aprsd.conf RUN aprsd sample-config > /config/aprsd.conf
RUN chown -R $APRS_USER:$APRS_USER /config
# override this to run another configuration ADD bin/run.sh /app
ENV CONF default ADD bin/listen.sh /app
USER $APRS_USER ADD bin/admin.sh /app
VOLUME ["/config", "/plugins"]
ADD bin/run.sh $HOME/ EXPOSE 8000
ADD bin/listen.sh $HOME/
ENTRYPOINT ["/home/aprs/run.sh"]
HEALTHCHECK --interval=5m --timeout=12s --start-period=30s \ # CMD ["gunicorn", "aprsd.wsgi:app", "--host", "0.0.0.0", "--port", "8000"]
CMD aprsd healthcheck --config /config/aprsd.conf ENTRYPOINT ["/app/run.sh"]
VOLUME ["/config"]
# Set the user to run the application
USER appuser
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -x
if [ ! -z "${APRSD_PLUGINS}" ]; then
OLDIFS=$IFS
IFS=','
echo "Installing pypi plugins '$APRSD_PLUGINS'";
for plugin in ${APRSD_PLUGINS}; do
IFS=$OLDIFS
# call your procedure/other scripts here below
echo "Installing '$plugin'"
pip3 install --user $plugin
done
fi
if [ -z "${LOG_LEVEL}" ] || [[ ! "${LOG_LEVEL}" =~ ^(CRITICAL|ERROR|WARNING|INFO)$ ]]; then
LOG_LEVEL="DEBUG"
fi
echo "Log level is set to ${LOG_LEVEL}";
# check to see if there is a config file
APRSD_CONFIG="/config/aprsd.conf"
if [ ! -e "$APRSD_CONFIG" ]; then
echo "'$APRSD_CONFIG' File does not exist. Creating."
aprsd sample-config > $APRSD_CONFIG
fi
export COLUMNS=200
#exec gunicorn -b :8000 --workers 4 "aprsd.admin_web:create_app(config_file='$APRSD_CONFIG', log_level='$LOG_LEVEL')"
# exec gunicorn -b :8000 --workers 4 "aprsd.wsgi:app"
exec uwsgi --http :8000 --gevent 1000 --http-websockets --master -w aprsd.wsgi --callable app
#exec aprsd listen -c $APRSD_CONFIG --loglevel ${LOG_LEVEL} ${APRSD_LOAD_PLUGINS} ${APRSD_LISTEN_FILTER}
+1 -1
View File
@@ -9,7 +9,7 @@ if [ ! -z "${APRSD_PLUGINS}" ]; then
IFS=$OLDIFS IFS=$OLDIFS
# call your procedure/other scripts here below # call your procedure/other scripts here below
echo "Installing '$plugin'" echo "Installing '$plugin'"
pip3 install $plugin pip3 install --user $plugin
done done
fi fi
+1 -1
View File
@@ -9,7 +9,7 @@ if [ ! -z "${APRSD_PLUGINS}" ]; then
IFS=$OLDIFS IFS=$OLDIFS
# call your procedure/other scripts here below # call your procedure/other scripts here below
echo "Installing '$plugin'" echo "Installing '$plugin'"
pip3 install $plugin pip3 install --user $plugin
done done
fi fi
+1 -1
View File
@@ -14,7 +14,7 @@ class HelloPlugin(plugin.APRSDRegexCommandPluginBase):
command_regex = "^[hH]" command_regex = "^[hH]"
command_name = "hello" command_name = "hello"
def command(self, packet): def process(self, packet):
LOG.info("HelloPlugin") LOG.info("HelloPlugin")
reply = f"Hello '{packet.from_call}'" reply = f"Hello '{packet.from_call}'"
return reply return reply
+8 -7
View File
@@ -1,9 +1,9 @@
aprslib>=0.7.0 aprslib>=0.7.0
click click
click-params
click-completion click-completion
flask==2.1.2 flask
werkzeug==2.1.2 werkzeug
flask-classful
flask-httpauth flask-httpauth
imapclient imapclient
pluggy pluggy
@@ -15,6 +15,8 @@ six
thesmuggler thesmuggler
update_checker update_checker
flask-socketio flask-socketio
python-socketio
gevent
eventlet eventlet
tabulate tabulate
# Pinned due to gray needing 12.6.0 # Pinned due to gray needing 12.6.0
@@ -27,13 +29,12 @@ kiss3
attrs attrs
# for mobile checking # for mobile checking
user-agents user-agents
pyopenssl
dataclasses dataclasses
dacite2 dacite2
oslo.config oslo.config
rpyc rpyc
# Pin this here so it doesn't require a compile on # Pin this here so it doesn't require a compile on
# raspi # raspi
cryptography==38.0.1 shellingham
shellingham==1.5.0.post1 geopy
ratelimiter rush
+46 -39
View File
@@ -1,69 +1,76 @@
# #
# 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 requirements.in
# #
aprslib==0.7.2 # via -r requirements.in aprslib==0.7.2 # via -r requirements.in
attrs==22.2.0 # via -r requirements.in, ax253, kiss3 attrs==23.1.0 # via -r requirements.in, ax253, kiss3, rush
ax253==0.1.5.post1 # via kiss3 ax253==0.1.5.post1 # via kiss3
beautifulsoup4==4.11.1 # via -r requirements.in beautifulsoup4==4.12.2 # via -r requirements.in
bidict==0.22.1 # via python-socketio bidict==0.22.1 # via python-socketio
bitarray==2.6.2 # via ax253, kiss3 bitarray==2.8.1 # via ax253, kiss3
certifi==2022.12.7 # via requests blinker==1.6.2 # via flask
cffi==1.15.1 # via cryptography certifi==2023.7.22 # via requests
charset-normalizer==3.0.1 # via requests charset-normalizer==3.2.0 # via requests
click==8.1.3 # via -r requirements.in, click-completion, flask click==8.1.6 # via -r requirements.in, click-completion, click-params, flask
click-completion==0.5.2 # via -r requirements.in click-completion==0.5.2 # via -r requirements.in
click-params==0.4.1 # via -r requirements.in
commonmark==0.9.1 # via rich commonmark==0.9.1 # via rich
cryptography==38.0.1 # via -r requirements.in, 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 debtcollector==2.5.0 # via oslo-config
dnspython==2.3.0 # via eventlet decorator==5.1.1 # via validators
dnspython==2.4.2 # via eventlet
eventlet==0.33.3 # via -r requirements.in eventlet==0.33.3 # via -r requirements.in
flask==2.1.2 # via -r requirements.in, flask-classful, flask-httpauth, flask-socketio flask==2.3.2 # via -r requirements.in, flask-httpauth, flask-socketio
flask-classful==0.14.2 # via -r requirements.in flask-httpauth==4.8.0 # via -r requirements.in
flask-httpauth==4.7.0 # via -r requirements.in flask-socketio==5.3.5 # via -r requirements.in
flask-socketio==5.3.2 # via -r requirements.in geographiclib==2.0 # via geopy
greenlet==2.0.1 # via eventlet geopy==2.3.0 # via -r requirements.in
gevent==23.7.0 # via -r requirements.in
greenlet==2.0.2 # via eventlet, gevent
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==6.0.0 # via ax253, flask, kiss3 importlib-metadata==6.8.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.2 # via jinja2 markupsafe==2.1.3 # via jinja2, werkzeug
netaddr==0.8.0 # via oslo-config netaddr==0.8.0 # via oslo-config
oslo-config==9.1.0 # via -r requirements.in oslo-config==9.1.1 # via -r requirements.in
oslo-i18n==5.1.0 # via oslo-config oslo-i18n==6.0.0 # via oslo-config
pbr==5.11.1 # via -r requirements.in, oslo-i18n, stevedore pbr==5.11.1 # via -r requirements.in, oslo-i18n, stevedore
pluggy==1.0.0 # via -r requirements.in pluggy==1.2.0 # via -r requirements.in
plumbum==1.8.1 # via rpyc plumbum==1.8.2 # via rpyc
pycparser==2.21 # via cffi pygments==2.16.1 # via rich
pygments==2.14.0 # via rich
pyopenssl==23.0.0 # via -r requirements.in
pyserial==3.5 # via pyserial-asyncio 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.5.1 # via python-socketio
python-socketio==5.7.2 # via flask-socketio python-socketio==5.8.0 # via -r requirements.in, flask-socketio
pytz==2022.7.1 # via -r requirements.in pytz==2023.3 # via -r requirements.in
pyyaml==6.0 # via -r requirements.in, oslo-config pyyaml==6.0.1 # via -r requirements.in, oslo-config
ratelimiter==1.2.0.post0 # via -r requirements.in requests==2.31.0 # via -r requirements.in, oslo-config, update-checker
requests==2.28.2 # via -r requirements.in, oslo-config, update-checker
rfc3986==2.0.0 # via oslo-config 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 rpyc==5.3.1 # via -r requirements.in
rush==2021.4.0 # via -r requirements.in
shellingham==1.5.0.post1 # via -r requirements.in, click-completion shellingham==1.5.0.post1 # via -r requirements.in, 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.4.1 # via beautifulsoup4
stevedore==4.1.1 # via oslo-config stevedore==5.1.0 # 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.18.0 # via user-agents
update-checker==0.18.0 # via -r requirements.in update-checker==0.18.0 # via -r requirements.in
urllib3==1.26.14 # via requests urllib3==2.0.4 # 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 validators==0.20.0 # via click-params
wrapt==1.14.1 # via -r requirements.in, debtcollector werkzeug==2.3.7 # via -r requirements.in, flask
zipp==3.11.0 # via importlib-metadata wrapt==1.15.0 # via -r requirements.in, debtcollector
zipp==3.16.2 # via importlib-metadata
zope-event==5.0 # via gevent
zope-interface==6.0 # via gevent
# The following packages are considered to be unsafe in a requirements file:
# setuptools
+1 -1
View File
@@ -33,7 +33,7 @@ packages =
[entry_points] [entry_points]
console_scripts = console_scripts =
aprsd = aprsd.aprsd:main aprsd = aprsd.main:main
oslo.config.opts = oslo.config.opts =
aprsd.conf = aprsd.conf.opts:list_opts aprsd.conf = aprsd.conf.opts:list_opts
oslo.config.opts.defaults = oslo.config.opts.defaults =
+3 -3
View File
@@ -6,8 +6,8 @@ from click.testing import CliRunner
from oslo_config import cfg from oslo_config import cfg
from aprsd import conf # noqa : F401 from aprsd import conf # noqa : F401
from aprsd.aprsd import cli
from aprsd.cmds import send_message # noqa from aprsd.cmds import send_message # noqa
from aprsd.main import cli
from .. import fake from .. import fake
@@ -30,7 +30,7 @@ class TestSendMessageCommand(unittest.TestCase):
CONF.admin.user = "admin" CONF.admin.user = "admin"
CONF.admin.password = "password" CONF.admin.password = "password"
@mock.patch("aprsd.logging.log.setup_logging") @mock.patch("aprsd.log.log.setup_logging")
def test_no_tocallsign(self, mock_logging): def test_no_tocallsign(self, mock_logging):
"""Make sure we get an error if there is no tocallsign.""" """Make sure we get an error if there is no tocallsign."""
@@ -47,7 +47,7 @@ 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.logging.log.setup_logging") @mock.patch("aprsd.log.log.setup_logging")
def test_no_command(self, mock_logging): 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."""
+3 -3
View File
@@ -32,16 +32,16 @@ class TestSendMessageCommand(unittest.TestCase):
CONF.admin.user = "admin" CONF.admin.user = "admin"
CONF.admin.password = "password" CONF.admin.password = "password"
@mock.patch("aprsd.logging.log.setup_logging") @mock.patch("aprsd.log.log.setup_logging")
def test_init_flask(self, mock_logging): 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()
self.config_and_init() self.config_and_init()
socketio, flask_app = webchat.init_flask("DEBUG", False) socketio = 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(webchat.flask_app, flask.Flask)
@mock.patch("aprsd.packets.tracker.PacketTrack.remove") @mock.patch("aprsd.packets.tracker.PacketTrack.remove")
@mock.patch("aprsd.cmds.webchat.socketio") @mock.patch("aprsd.cmds.webchat.socketio")
+24 -14
View File
@@ -49,51 +49,61 @@ class TestLocationPlugin(test_plugin.TestPlugin):
self.assertEqual(expected, actual) self.assertEqual(expected, actual)
@mock.patch("aprsd.plugin_utils.get_aprs_fi") @mock.patch("aprsd.plugin_utils.get_aprs_fi")
@mock.patch("aprsd.plugin_utils.get_weather_gov_for_gps") @mock.patch("geopy.geocoders.Nominatim.reverse")
@mock.patch("time.time") @mock.patch("time.time")
def test_location_unknown_gps(self, mock_time, mock_weather, mock_check_aprs): def test_location_unknown_gps(self, mock_time, mock_geocode, mock_check_aprs):
# 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_aprs.return_value = { mock_check_aprs.return_value = {
"entries": [ "entries": [
{ {
"lat": 10, "lat": 1,
"lng": 11, "lng": 1,
"lasttime": 10, "lasttime": 10,
}, },
], ],
} }
mock_weather.side_effect = Exception mock_geocode.side_effect = Exception
mock_time.return_value = 10 mock_time.return_value = 10
CONF.callsign = fake.FAKE_TO_CALLSIGN CONF.callsign = fake.FAKE_TO_CALLSIGN
fortune = location_plugin.LocationPlugin() fortune = location_plugin.LocationPlugin()
expected = "KFAKE: Unknown Location 0' 10,11 0.0h ago" expected = "KFAKE: Unknown Location 0' 1.00,1.00 0.0h ago"
packet = fake.fake_packet(message="location") packet = fake.fake_packet(message="location")
actual = fortune.filter(packet) actual = fortune.filter(packet)
self.assertEqual(expected, actual) self.assertEqual(expected, actual)
@mock.patch("aprsd.plugin_utils.get_aprs_fi") @mock.patch("aprsd.plugin_utils.get_aprs_fi")
@mock.patch("aprsd.plugin_utils.get_weather_gov_for_gps") @mock.patch("geopy.geocoders.Nominatim.reverse")
@mock.patch("time.time") @mock.patch("time.time")
def test_location_works(self, mock_time, mock_weather, mock_check_aprs): def test_location_works(self, mock_time, mock_geocode, mock_check_aprs):
# 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_aprs.return_value = { mock_check_aprs.return_value = {
"entries": [ "entries": [
{ {
"lat": 10, "lat": 1,
"lng": 11, "lng": 1,
"lasttime": 10, "lasttime": 10,
}, },
], ],
} }
expected_town = "Appomattox, VA" expected = "Appomattox"
wx_data = {"location": {"areaDescription": expected_town}} state = "VA"
mock_weather.return_value = wx_data
class TempLocation:
raw = {
"address": {
"county": expected,
"country_code": "us",
"state": state,
"country": "United States",
},
}
mock_geocode.return_value = TempLocation()
mock_time.return_value = 10 mock_time.return_value = 10
CONF.callsign = fake.FAKE_TO_CALLSIGN CONF.callsign = fake.FAKE_TO_CALLSIGN
fortune = location_plugin.LocationPlugin() fortune = location_plugin.LocationPlugin()
expected = f"KFAKE: {expected_town} 0' 10,11 0.0h ago" expected = f"KFAKE: {expected}, {state} 0' 1.00,1.00 0.0h ago"
packet = fake.fake_packet(message="location") packet = fake.fake_packet(message="location")
actual = fortune.filter(packet) actual = fortune.filter(packet)
self.assertEqual(expected, actual) self.assertEqual(expected, actual)