mirror of
https://github.com/craigerl/aprsd.git
synced 2024-11-25 17:38:44 -05:00
Remove flask pinning
Also removed need for flask-classful. Created new aprsd/wsgi.py for the web admin interface.
This commit is contained in:
parent
5723e3a77b
commit
e1183a7e30
332
aprsd/wsgi.py
332
aprsd/wsgi.py
@ -1,12 +1,334 @@
|
||||
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 flask_socketio import Namespace, SocketIO
|
||||
from oslo_config import cfg
|
||||
from werkzeug.security import check_password_hash
|
||||
|
||||
from aprsd import admin_web
|
||||
from aprsd import conf # noqa
|
||||
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("APRSD")
|
||||
app = None
|
||||
app = admin_web.create_app()
|
||||
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",
|
||||
)
|
||||
socket_io = SocketIO(app)
|
||||
|
||||
|
||||
# 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())
|
||||
|
||||
|
||||
@auth.login_required
|
||||
@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 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):
|
||||
global socket_io
|
||||
|
||||
if socket_io:
|
||||
log_entries = aprsd_rpc_client.RPCClient().get_log_entries()
|
||||
|
||||
if log_entries:
|
||||
for entry in log_entries:
|
||||
socket_io.emit(
|
||||
"log_entry", entry,
|
||||
namespace="/logs",
|
||||
)
|
||||
|
||||
time.sleep(5)
|
||||
return True
|
||||
|
||||
|
||||
class LoggingNamespace(Namespace):
|
||||
log_thread = None
|
||||
|
||||
def on_connect(self):
|
||||
global socket_io
|
||||
socket_io.emit(
|
||||
"connected", {"data": "/logs Connected"},
|
||||
namespace="/logs",
|
||||
)
|
||||
self.log_thread = LogUpdateThread()
|
||||
self.log_thread.start()
|
||||
|
||||
def on_disconnect(self):
|
||||
LOG.debug("LOG Disconnected")
|
||||
if self.log_thread:
|
||||
self.log_thread.stop()
|
||||
|
||||
|
||||
def setup_logging(flask_app, loglevel):
|
||||
flask_log = logging.getLogger("werkzeug")
|
||||
flask_app.logger.removeHandler(default_handler)
|
||||
flask_log.removeHandler(default_handler)
|
||||
LOG.handlers = []
|
||||
|
||||
log_level = conf.log.LOG_LEVELS[loglevel]
|
||||
LOG.setLevel(log_level)
|
||||
date_format = CONF.logging.date_format
|
||||
flask_app.logger.disabled = True
|
||||
gunicorn_err = logging.getLogger("gunicorn.error")
|
||||
|
||||
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)
|
||||
LOG.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)
|
||||
LOG.addHandler(fh)
|
||||
|
||||
gunicorn_err.handlers = LOG.handlers
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
print(f"APP {__name__}")
|
||||
if __name__ == "__main__":
|
||||
socket_io.run(app)
|
||||
|
||||
if __name__ == "aprsd.wsgi":
|
||||
log_level = init_app(config_file="~/.config/aprsd/aprsd.conf", log_level="DEBUG")
|
||||
socket_io.on_namespace(LoggingNamespace("/logs"))
|
||||
setup_logging(app, log_level)
|
||||
|
@ -1,5 +1,5 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.8
|
||||
# This file is autogenerated by pip-compile with Python 3.10
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile --annotation-style=line dev-requirements.in
|
||||
@ -31,13 +31,12 @@ gray==0.13.0 # via -r dev-requirements.in
|
||||
identify==2.5.24 # via pre-commit
|
||||
idna==3.4 # via requests
|
||||
imagesize==1.4.1 # via sphinx
|
||||
importlib-metadata==6.8.0 # via sphinx
|
||||
importlib-resources==6.0.0 # via fixit, jsonschema, jsonschema-specifications
|
||||
importlib-resources==6.0.0 # via fixit
|
||||
iniconfig==2.0.0 # via pytest
|
||||
isort==5.12.0 # via -r dev-requirements.in, gray
|
||||
jinja2==3.1.2 # via sphinx
|
||||
jsonschema==4.18.3 # via fixit
|
||||
jsonschema-specifications==2023.6.1 # via jsonschema
|
||||
jsonschema==4.18.4 # via fixit
|
||||
jsonschema-specifications==2023.7.1 # via jsonschema
|
||||
libcst==1.0.1 # via fixit
|
||||
markupsafe==2.1.3 # via jinja2
|
||||
mccabe==0.7.0 # via flake8
|
||||
@ -48,7 +47,6 @@ packaging==23.1 # via black, build, pyproject-api, pytest, sphinx, tox
|
||||
pathspec==0.11.1 # via black
|
||||
pep8-naming==0.13.3 # via -r dev-requirements.in
|
||||
pip-tools==7.0.0 # via -r dev-requirements.in
|
||||
pkgutil-resolve-name==1.3.10 # via jsonschema
|
||||
platformdirs==3.9.1 # via black, tox, virtualenv
|
||||
pluggy==1.2.0 # via pytest, tox
|
||||
pre-commit==3.3.3 # via -r dev-requirements.in
|
||||
@ -59,13 +57,12 @@ pyproject-api==1.5.3 # via tox
|
||||
pyproject-hooks==1.0.0 # via build
|
||||
pytest==7.4.0 # via -r dev-requirements.in, pytest-cov
|
||||
pytest-cov==4.1.0 # via -r dev-requirements.in
|
||||
pytz==2023.3 # via babel
|
||||
pyupgrade==3.9.0 # via gray
|
||||
pyyaml==6.0 # via fixit, libcst, pre-commit
|
||||
referencing==0.29.1 # via jsonschema, jsonschema-specifications
|
||||
pyyaml==6.0.1 # via fixit, libcst, pre-commit
|
||||
referencing==0.30.0 # via jsonschema, jsonschema-specifications
|
||||
requests==2.31.0 # via sphinx
|
||||
rich==12.6.0 # via gray
|
||||
rpds-py==0.8.11 # via jsonschema, referencing
|
||||
rpds-py==0.9.2 # via jsonschema, referencing
|
||||
snowballstemmer==2.2.0 # via sphinx
|
||||
sphinx==7.0.1 # via -r dev-requirements.in
|
||||
sphinxcontrib-applehelp==1.0.4 # via sphinx
|
||||
@ -78,14 +75,13 @@ tokenize-rt==5.1.0 # via add-trailing-comma, pyupgrade
|
||||
toml==0.10.2 # via autoflake
|
||||
tomli==2.0.1 # via black, build, coverage, mypy, pip-tools, pyproject-api, pyproject-hooks, pytest, tox
|
||||
tox==4.6.4 # via -r dev-requirements.in
|
||||
typing-extensions==4.7.1 # via black, libcst, mypy, rich, typing-inspect
|
||||
typing-extensions==4.7.1 # via libcst, mypy, typing-inspect
|
||||
typing-inspect==0.9.0 # via libcst
|
||||
unify==0.5 # via gray
|
||||
untokenize==0.1.1 # via unify
|
||||
urllib3==2.0.3 # via requests
|
||||
virtualenv==20.24.0 # via pre-commit, tox
|
||||
wheel==0.40.0 # via pip-tools
|
||||
zipp==3.16.2 # via importlib-metadata, importlib-resources
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# pip
|
||||
|
@ -2,9 +2,8 @@ aprslib>=0.7.0
|
||||
click
|
||||
click-params
|
||||
click-completion
|
||||
flask==2.1.2
|
||||
werkzeug==2.1.2
|
||||
flask-classful
|
||||
flask
|
||||
werkzeug
|
||||
flask-httpauth
|
||||
imapclient
|
||||
pluggy
|
||||
|
@ -1,5 +1,5 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.8
|
||||
# This file is autogenerated by pip-compile with Python 3.10
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile --annotation-style=line requirements.in
|
||||
@ -11,6 +11,7 @@ ax253==0.1.5.post1 # via kiss3
|
||||
beautifulsoup4==4.12.2 # via -r requirements.in
|
||||
bidict==0.22.1 # via python-socketio
|
||||
bitarray==2.7.6 # via ax253, kiss3
|
||||
blinker==1.6.2 # via flask
|
||||
certifi==2023.5.7 # via httpcore, requests
|
||||
cffi==1.15.1 # via cryptography
|
||||
charset-normalizer==3.2.0 # via requests
|
||||
@ -26,8 +27,7 @@ decorator==5.1.1 # via validators
|
||||
dnspython==2.4.0 # via eventlet
|
||||
eventlet==0.33.3 # via -r requirements.in
|
||||
exceptiongroup==1.1.2 # via anyio
|
||||
flask==2.1.2 # via -r requirements.in, flask-classful, flask-httpauth, flask-socketio
|
||||
flask-classful==0.14.2 # via -r requirements.in
|
||||
flask==2.3.2 # via -r requirements.in, flask-httpauth, flask-socketio
|
||||
flask-httpauth==4.8.0 # via -r requirements.in
|
||||
flask-socketio==5.3.4 # via -r requirements.in
|
||||
geographiclib==2.0 # via geopy
|
||||
@ -37,11 +37,11 @@ h11==0.14.0 # via httpcore
|
||||
httpcore==0.17.3 # via dnspython
|
||||
idna==3.4 # via anyio, requests
|
||||
imapclient==2.3.1 # via -r requirements.in
|
||||
importlib-metadata==6.8.0 # via ax253, flask, kiss3
|
||||
importlib-metadata==6.8.0 # via ax253, kiss3
|
||||
itsdangerous==2.1.2 # via flask
|
||||
jinja2==3.1.2 # via click-completion, flask
|
||||
kiss3==8.0.0 # via -r requirements.in
|
||||
markupsafe==2.1.3 # via jinja2
|
||||
markupsafe==2.1.3 # via jinja2, werkzeug
|
||||
netaddr==0.8.0 # via oslo-config
|
||||
oslo-config==9.1.1 # via -r requirements.in
|
||||
oslo-i18n==6.0.0 # via oslo-config
|
||||
@ -56,7 +56,7 @@ pyserial-asyncio==0.6 # via kiss3
|
||||
python-engineio==4.5.1 # via python-socketio
|
||||
python-socketio==5.8.0 # via flask-socketio
|
||||
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
|
||||
requests==2.31.0 # via -r requirements.in, oslo-config, update-checker
|
||||
rfc3986==2.0.0 # via oslo-config
|
||||
rich==12.6.0 # via -r requirements.in
|
||||
@ -69,12 +69,11 @@ soupsieve==2.4.1 # via beautifulsoup4
|
||||
stevedore==5.1.0 # via oslo-config
|
||||
tabulate==0.9.0 # via -r requirements.in
|
||||
thesmuggler==1.0.1 # via -r requirements.in
|
||||
typing-extensions==4.7.1 # via rich
|
||||
ua-parser==0.18.0 # via user-agents
|
||||
update-checker==0.18.0 # via -r requirements.in
|
||||
urllib3==2.0.3 # via requests
|
||||
user-agents==2.2.0 # via -r requirements.in
|
||||
validators==0.20.0 # via click-params
|
||||
werkzeug==2.1.2 # via -r requirements.in, flask
|
||||
werkzeug==2.3.6 # via -r requirements.in, flask
|
||||
wrapt==1.15.0 # via -r requirements.in, debtcollector
|
||||
zipp==3.16.2 # via importlib-metadata
|
||||
|
Loading…
Reference in New Issue
Block a user