aprsd 3.3.4__py2.py3-none-any.whl → 3.4.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aprsd/client.py +133 -20
- aprsd/clients/aprsis.py +6 -3
- aprsd/clients/fake.py +1 -1
- aprsd/clients/kiss.py +1 -1
- aprsd/cmds/completion.py +13 -27
- aprsd/cmds/fetch_stats.py +53 -57
- aprsd/cmds/healthcheck.py +32 -30
- aprsd/cmds/list_plugins.py +2 -2
- aprsd/cmds/listen.py +33 -17
- aprsd/cmds/send_message.py +2 -2
- aprsd/cmds/server.py +26 -9
- aprsd/cmds/webchat.py +34 -29
- aprsd/conf/common.py +46 -31
- aprsd/log/log.py +28 -6
- aprsd/main.py +4 -17
- aprsd/packets/__init__.py +3 -2
- aprsd/packets/collector.py +56 -0
- aprsd/packets/core.py +456 -321
- aprsd/packets/log.py +143 -0
- aprsd/packets/packet_list.py +83 -66
- aprsd/packets/seen_list.py +30 -19
- aprsd/packets/tracker.py +60 -62
- aprsd/packets/watch_list.py +64 -38
- aprsd/plugin.py +41 -16
- aprsd/plugins/email.py +35 -7
- aprsd/plugins/time.py +3 -2
- aprsd/plugins/version.py +4 -5
- aprsd/plugins/weather.py +0 -1
- aprsd/stats/__init__.py +20 -0
- aprsd/stats/app.py +46 -0
- aprsd/stats/collector.py +38 -0
- aprsd/threads/__init__.py +3 -2
- aprsd/threads/aprsd.py +67 -36
- aprsd/threads/keep_alive.py +55 -49
- aprsd/threads/log_monitor.py +46 -0
- aprsd/threads/rx.py +43 -24
- aprsd/threads/stats.py +44 -0
- aprsd/threads/tx.py +36 -17
- aprsd/utils/__init__.py +12 -0
- aprsd/utils/counter.py +6 -3
- aprsd/utils/json.py +20 -0
- aprsd/utils/objectstore.py +22 -17
- aprsd/web/admin/static/css/prism.css +4 -189
- aprsd/web/admin/static/js/charts.js +9 -7
- aprsd/web/admin/static/js/echarts.js +71 -9
- aprsd/web/admin/static/js/main.js +47 -6
- aprsd/web/admin/static/js/prism.js +11 -2246
- aprsd/web/admin/templates/index.html +18 -7
- aprsd/web/chat/static/js/gps.js +3 -1
- aprsd/web/chat/static/js/main.js +4 -3
- aprsd/web/chat/static/js/send-message.js +5 -2
- aprsd/web/chat/templates/index.html +1 -0
- aprsd/wsgi.py +62 -127
- {aprsd-3.3.4.dist-info → aprsd-3.4.0.dist-info}/METADATA +14 -16
- {aprsd-3.3.4.dist-info → aprsd-3.4.0.dist-info}/RECORD +60 -63
- {aprsd-3.3.4.dist-info → aprsd-3.4.0.dist-info}/WHEEL +1 -1
- aprsd-3.4.0.dist-info/pbr.json +1 -0
- aprsd/plugins/query.py +0 -81
- aprsd/rpc/__init__.py +0 -14
- aprsd/rpc/client.py +0 -165
- aprsd/rpc/server.py +0 -99
- aprsd/stats.py +0 -266
- aprsd/web/admin/static/json-viewer/jquery.json-viewer.css +0 -57
- aprsd/web/admin/static/json-viewer/jquery.json-viewer.js +0 -158
- aprsd/web/chat/static/json-viewer/jquery.json-viewer.css +0 -57
- aprsd/web/chat/static/json-viewer/jquery.json-viewer.js +0 -158
- aprsd-3.3.4.dist-info/pbr.json +0 -1
- {aprsd-3.3.4.dist-info → aprsd-3.4.0.dist-info}/LICENSE +0 -0
- {aprsd-3.3.4.dist-info → aprsd-3.4.0.dist-info}/entry_points.txt +0 -0
- {aprsd-3.3.4.dist-info → aprsd-3.4.0.dist-info}/top_level.txt +0 -0
aprsd/client.py
CHANGED
@@ -1,15 +1,18 @@
|
|
1
1
|
import abc
|
2
|
+
import datetime
|
2
3
|
import logging
|
4
|
+
import threading
|
3
5
|
import time
|
4
6
|
|
5
7
|
import aprslib
|
6
8
|
from aprslib.exceptions import LoginError
|
7
9
|
from oslo_config import cfg
|
10
|
+
import wrapt
|
8
11
|
|
9
12
|
from aprsd import exception
|
10
13
|
from aprsd.clients import aprsis, fake, kiss
|
11
|
-
from aprsd.packets import core
|
12
|
-
from aprsd.utils import trace
|
14
|
+
from aprsd.packets import core
|
15
|
+
from aprsd.utils import singleton, trace
|
13
16
|
|
14
17
|
|
15
18
|
CONF = cfg.CONF
|
@@ -25,6 +28,34 @@ TRANSPORT_FAKE = "fake"
|
|
25
28
|
factory = None
|
26
29
|
|
27
30
|
|
31
|
+
@singleton
|
32
|
+
class APRSClientStats:
|
33
|
+
|
34
|
+
lock = threading.Lock()
|
35
|
+
|
36
|
+
@wrapt.synchronized(lock)
|
37
|
+
def stats(self, serializable=False):
|
38
|
+
client = factory.create()
|
39
|
+
stats = {
|
40
|
+
"transport": client.transport(),
|
41
|
+
"filter": client.filter,
|
42
|
+
"connected": client.connected,
|
43
|
+
}
|
44
|
+
|
45
|
+
if client.transport() == TRANSPORT_APRSIS:
|
46
|
+
stats["server_string"] = client.client.server_string
|
47
|
+
keepalive = client.client.aprsd_keepalive
|
48
|
+
if serializable:
|
49
|
+
keepalive = keepalive.isoformat()
|
50
|
+
stats["server_keepalive"] = keepalive
|
51
|
+
elif client.transport() == TRANSPORT_TCPKISS:
|
52
|
+
stats["host"] = CONF.kiss_tcp.host
|
53
|
+
stats["port"] = CONF.kiss_tcp.port
|
54
|
+
elif client.transport() == TRANSPORT_SERIALKISS:
|
55
|
+
stats["device"] = CONF.kiss_serial.device
|
56
|
+
return stats
|
57
|
+
|
58
|
+
|
28
59
|
class Client:
|
29
60
|
"""Singleton client class that constructs the aprslib connection."""
|
30
61
|
|
@@ -32,16 +63,21 @@ class Client:
|
|
32
63
|
_client = None
|
33
64
|
|
34
65
|
connected = False
|
35
|
-
server_string = None
|
36
66
|
filter = None
|
67
|
+
lock = threading.Lock()
|
37
68
|
|
38
69
|
def __new__(cls, *args, **kwargs):
|
39
70
|
"""This magic turns this into a singleton."""
|
40
71
|
if cls._instance is None:
|
41
72
|
cls._instance = super().__new__(cls)
|
42
73
|
# Put any initialization here.
|
74
|
+
cls._instance._create_client()
|
43
75
|
return cls._instance
|
44
76
|
|
77
|
+
@abc.abstractmethod
|
78
|
+
def stats(self) -> dict:
|
79
|
+
pass
|
80
|
+
|
45
81
|
def set_filter(self, filter):
|
46
82
|
self.filter = filter
|
47
83
|
if self._client:
|
@@ -50,21 +86,32 @@ class Client:
|
|
50
86
|
@property
|
51
87
|
def client(self):
|
52
88
|
if not self._client:
|
53
|
-
|
54
|
-
self._client = self.setup_connection()
|
55
|
-
if self.filter:
|
56
|
-
LOG.info("Creating APRS client filter")
|
57
|
-
self._client.set_filter(self.filter)
|
89
|
+
self._create_client()
|
58
90
|
return self._client
|
59
91
|
|
92
|
+
def _create_client(self):
|
93
|
+
self._client = self.setup_connection()
|
94
|
+
if self.filter:
|
95
|
+
LOG.info("Creating APRS client filter")
|
96
|
+
self._client.set_filter(self.filter)
|
97
|
+
|
98
|
+
def stop(self):
|
99
|
+
if self._client:
|
100
|
+
LOG.info("Stopping client connection.")
|
101
|
+
self._client.stop()
|
102
|
+
|
60
103
|
def send(self, packet: core.Packet):
|
61
|
-
|
104
|
+
"""Send a packet to the network."""
|
62
105
|
self.client.send(packet)
|
63
106
|
|
107
|
+
@wrapt.synchronized(lock)
|
64
108
|
def reset(self):
|
65
109
|
"""Call this to force a rebuild/reconnect."""
|
110
|
+
LOG.info("Resetting client connection.")
|
66
111
|
if self._client:
|
112
|
+
self._client.close()
|
67
113
|
del self._client
|
114
|
+
self._create_client()
|
68
115
|
else:
|
69
116
|
LOG.warning("Client not initialized, nothing to reset.")
|
70
117
|
|
@@ -89,11 +136,38 @@ class Client:
|
|
89
136
|
def decode_packet(self, *args, **kwargs):
|
90
137
|
pass
|
91
138
|
|
139
|
+
@abc.abstractmethod
|
140
|
+
def consumer(self, callback, blocking=False, immortal=False, raw=False):
|
141
|
+
pass
|
142
|
+
|
143
|
+
@abc.abstractmethod
|
144
|
+
def is_alive(self):
|
145
|
+
pass
|
146
|
+
|
147
|
+
@abc.abstractmethod
|
148
|
+
def close(self):
|
149
|
+
pass
|
150
|
+
|
92
151
|
|
93
152
|
class APRSISClient(Client):
|
94
153
|
|
95
154
|
_client = None
|
96
155
|
|
156
|
+
def __init__(self):
|
157
|
+
max_timeout = {"hours": 0.0, "minutes": 2, "seconds": 0}
|
158
|
+
self.max_delta = datetime.timedelta(**max_timeout)
|
159
|
+
|
160
|
+
def stats(self) -> dict:
|
161
|
+
stats = {}
|
162
|
+
if self.is_configured():
|
163
|
+
stats = {
|
164
|
+
"server_string": self._client.server_string,
|
165
|
+
"sever_keepalive": self._client.aprsd_keepalive,
|
166
|
+
"filter": self.filter,
|
167
|
+
}
|
168
|
+
|
169
|
+
return stats
|
170
|
+
|
97
171
|
@staticmethod
|
98
172
|
def is_enabled():
|
99
173
|
# Defaults to True if the enabled flag is non existent
|
@@ -125,44 +199,56 @@ class APRSISClient(Client):
|
|
125
199
|
return True
|
126
200
|
return True
|
127
201
|
|
202
|
+
def _is_stale_connection(self):
|
203
|
+
delta = datetime.datetime.now() - self._client.aprsd_keepalive
|
204
|
+
if delta > self.max_delta:
|
205
|
+
LOG.error(f"Connection is stale, last heard {delta} ago.")
|
206
|
+
return True
|
207
|
+
|
128
208
|
def is_alive(self):
|
129
209
|
if self._client:
|
130
|
-
return self._client.is_alive()
|
210
|
+
return self._client.is_alive() and not self._is_stale_connection()
|
131
211
|
else:
|
212
|
+
LOG.warning(f"APRS_CLIENT {self._client} alive? NO!!!")
|
132
213
|
return False
|
133
214
|
|
215
|
+
def close(self):
|
216
|
+
if self._client:
|
217
|
+
self._client.stop()
|
218
|
+
self._client.close()
|
219
|
+
|
134
220
|
@staticmethod
|
135
221
|
def transport():
|
136
222
|
return TRANSPORT_APRSIS
|
137
223
|
|
138
224
|
def decode_packet(self, *args, **kwargs):
|
139
225
|
"""APRS lib already decodes this."""
|
140
|
-
return core.
|
226
|
+
return core.factory(args[0])
|
141
227
|
|
142
228
|
def setup_connection(self):
|
143
229
|
user = CONF.aprs_network.login
|
144
230
|
password = CONF.aprs_network.password
|
145
231
|
host = CONF.aprs_network.host
|
146
232
|
port = CONF.aprs_network.port
|
147
|
-
connected = False
|
233
|
+
self.connected = False
|
148
234
|
backoff = 1
|
149
235
|
aprs_client = None
|
150
|
-
while not connected:
|
236
|
+
while not self.connected:
|
151
237
|
try:
|
152
|
-
LOG.info("Creating aprslib client")
|
238
|
+
LOG.info(f"Creating aprslib client({host}:{port}) and logging in {user}.")
|
153
239
|
aprs_client = aprsis.Aprsdis(user, passwd=password, host=host, port=port)
|
154
240
|
# Force the log to be the same
|
155
241
|
aprs_client.logger = LOG
|
156
242
|
aprs_client.connect()
|
157
|
-
connected = True
|
243
|
+
self.connected = True
|
158
244
|
backoff = 1
|
159
245
|
except LoginError as e:
|
160
246
|
LOG.error(f"Failed to login to APRS-IS Server '{e}'")
|
161
|
-
connected = False
|
247
|
+
self.connected = False
|
162
248
|
time.sleep(backoff)
|
163
249
|
except Exception as e:
|
164
250
|
LOG.error(f"Unable to connect to APRS-IS server. '{e}' ")
|
165
|
-
connected = False
|
251
|
+
self.connected = False
|
166
252
|
time.sleep(backoff)
|
167
253
|
# Don't allow the backoff to go to inifinity.
|
168
254
|
if backoff > 5:
|
@@ -170,15 +256,28 @@ class APRSISClient(Client):
|
|
170
256
|
else:
|
171
257
|
backoff += 1
|
172
258
|
continue
|
173
|
-
LOG.debug(f"Logging in to APRS-IS with user '{user}'")
|
174
259
|
self._client = aprs_client
|
175
260
|
return aprs_client
|
176
261
|
|
262
|
+
def consumer(self, callback, blocking=False, immortal=False, raw=False):
|
263
|
+
self._client.consumer(
|
264
|
+
callback, blocking=blocking,
|
265
|
+
immortal=immortal, raw=raw,
|
266
|
+
)
|
267
|
+
|
177
268
|
|
178
269
|
class KISSClient(Client):
|
179
270
|
|
180
271
|
_client = None
|
181
272
|
|
273
|
+
def stats(self) -> dict:
|
274
|
+
stats = {}
|
275
|
+
if self.is_configured():
|
276
|
+
return {
|
277
|
+
"transport": self.transport(),
|
278
|
+
}
|
279
|
+
return stats
|
280
|
+
|
182
281
|
@staticmethod
|
183
282
|
def is_enabled():
|
184
283
|
"""Return if tcp or serial KISS is enabled."""
|
@@ -217,6 +316,10 @@ class KISSClient(Client):
|
|
217
316
|
else:
|
218
317
|
return False
|
219
318
|
|
319
|
+
def close(self):
|
320
|
+
if self._client:
|
321
|
+
self._client.stop()
|
322
|
+
|
220
323
|
@staticmethod
|
221
324
|
def transport():
|
222
325
|
if CONF.kiss_serial.enabled:
|
@@ -238,7 +341,7 @@ class KISSClient(Client):
|
|
238
341
|
# LOG.debug(f"Decoding {msg}")
|
239
342
|
|
240
343
|
raw = aprslib.parse(str(frame))
|
241
|
-
packet = core.
|
344
|
+
packet = core.factory(raw)
|
242
345
|
if isinstance(packet, core.ThirdParty):
|
243
346
|
return packet.subpacket
|
244
347
|
else:
|
@@ -246,11 +349,18 @@ class KISSClient(Client):
|
|
246
349
|
|
247
350
|
def setup_connection(self):
|
248
351
|
self._client = kiss.KISS3Client()
|
352
|
+
self.connected = True
|
249
353
|
return self._client
|
250
354
|
|
355
|
+
def consumer(self, callback, blocking=False, immortal=False, raw=False):
|
356
|
+
self._client.consumer(callback)
|
357
|
+
|
251
358
|
|
252
359
|
class APRSDFakeClient(Client, metaclass=trace.TraceWrapperMetaclass):
|
253
360
|
|
361
|
+
def stats(self) -> dict:
|
362
|
+
return {}
|
363
|
+
|
254
364
|
@staticmethod
|
255
365
|
def is_enabled():
|
256
366
|
if CONF.fake_client.enabled:
|
@@ -264,7 +374,11 @@ class APRSDFakeClient(Client, metaclass=trace.TraceWrapperMetaclass):
|
|
264
374
|
def is_alive(self):
|
265
375
|
return True
|
266
376
|
|
377
|
+
def close(self):
|
378
|
+
pass
|
379
|
+
|
267
380
|
def setup_connection(self):
|
381
|
+
self.connected = True
|
268
382
|
return fake.APRSDFakeClient()
|
269
383
|
|
270
384
|
@staticmethod
|
@@ -304,7 +418,6 @@ class ClientFactory:
|
|
304
418
|
key = TRANSPORT_FAKE
|
305
419
|
|
306
420
|
builder = self._builders.get(key)
|
307
|
-
LOG.debug(f"Creating client {key}")
|
308
421
|
if not builder:
|
309
422
|
raise ValueError(key)
|
310
423
|
return builder()
|
aprsd/clients/aprsis.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import datetime
|
1
2
|
import logging
|
2
3
|
import select
|
3
4
|
import threading
|
@@ -11,7 +12,6 @@ from aprslib.exceptions import (
|
|
11
12
|
import wrapt
|
12
13
|
|
13
14
|
import aprsd
|
14
|
-
from aprsd import stats
|
15
15
|
from aprsd.packets import core
|
16
16
|
|
17
17
|
|
@@ -24,6 +24,9 @@ class Aprsdis(aprslib.IS):
|
|
24
24
|
# flag to tell us to stop
|
25
25
|
thread_stop = False
|
26
26
|
|
27
|
+
# date for last time we heard from the server
|
28
|
+
aprsd_keepalive = datetime.datetime.now()
|
29
|
+
|
27
30
|
# timeout in seconds
|
28
31
|
select_timeout = 1
|
29
32
|
lock = threading.Lock()
|
@@ -142,7 +145,6 @@ class Aprsdis(aprslib.IS):
|
|
142
145
|
|
143
146
|
self.logger.info(f"Connected to {server_string}")
|
144
147
|
self.server_string = server_string
|
145
|
-
stats.APRSDStats().set_aprsis_server(server_string)
|
146
148
|
|
147
149
|
except LoginError as e:
|
148
150
|
self.logger.error(str(e))
|
@@ -176,13 +178,14 @@ class Aprsdis(aprslib.IS):
|
|
176
178
|
try:
|
177
179
|
for line in self._socket_readlines(blocking):
|
178
180
|
if line[0:1] != b"#":
|
181
|
+
self.aprsd_keepalive = datetime.datetime.now()
|
179
182
|
if raw:
|
180
183
|
callback(line)
|
181
184
|
else:
|
182
185
|
callback(self._parse(line))
|
183
186
|
else:
|
184
187
|
self.logger.debug("Server: %s", line.decode("utf8"))
|
185
|
-
|
188
|
+
self.aprsd_keepalive = datetime.datetime.now()
|
186
189
|
except ParseError as exp:
|
187
190
|
self.logger.log(
|
188
191
|
11,
|
aprsd/clients/fake.py
CHANGED
@@ -67,7 +67,7 @@ class APRSDFakeClient(metaclass=trace.TraceWrapperMetaclass):
|
|
67
67
|
# Generate packets here?
|
68
68
|
raw = "GTOWN>APDW16,WIDE1-1,WIDE2-1:}KM6LYW-9>APZ100,TCPIP,GTOWN*::KM6LYW :KM6LYW: 19 Miles SW"
|
69
69
|
pkt_raw = aprslib.parse(raw)
|
70
|
-
pkt = core.
|
70
|
+
pkt = core.factory(pkt_raw)
|
71
71
|
callback(packet=pkt)
|
72
72
|
LOG.debug(f"END blocking FAKE consumer {self}")
|
73
73
|
time.sleep(8)
|
aprsd/clients/kiss.py
CHANGED
@@ -81,7 +81,7 @@ class KISS3Client:
|
|
81
81
|
LOG.error("Failed to parse bytes received from KISS interface.")
|
82
82
|
LOG.exception(ex)
|
83
83
|
|
84
|
-
def consumer(self, callback
|
84
|
+
def consumer(self, callback):
|
85
85
|
LOG.debug("Start blocking KISS consumer")
|
86
86
|
self._parse_callback = callback
|
87
87
|
self.kiss.read(callback=self.parse_frame, min_frames=None)
|
aprsd/cmds/completion.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import click
|
2
|
-
import
|
2
|
+
import click.shell_completion
|
3
3
|
|
4
4
|
from aprsd.main import cli
|
5
5
|
|
@@ -7,30 +7,16 @@ from aprsd.main import cli
|
|
7
7
|
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
8
8
|
|
9
9
|
|
10
|
-
@cli.
|
11
|
-
@click.
|
12
|
-
def completion(
|
13
|
-
|
10
|
+
@cli.command()
|
11
|
+
@click.argument("shell", type=click.Choice(list(click.shell_completion._available_shells)))
|
12
|
+
def completion(shell):
|
13
|
+
"""Show the shell completion code"""
|
14
|
+
from click.utils import _detect_program_name
|
14
15
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
"
|
22
|
-
extra_env = {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} if case_insensitive else {}
|
23
|
-
click.echo(click_completion.core.get_code(shell, extra_env=extra_env))
|
24
|
-
|
25
|
-
|
26
|
-
# install will install the completion code for a particular shell
|
27
|
-
@completion.command(help="Install completion code for a shell", name="install")
|
28
|
-
@click.option("--append/--overwrite", help="Append the completion code to the file", default=None)
|
29
|
-
@click.option("-i", "--case-insensitive/--no-case-insensitive", help="Case insensitive completion")
|
30
|
-
@click.argument("shell", required=False, type=click_completion.DocumentedChoice(click_completion.core.shells))
|
31
|
-
@click.argument("path", required=False)
|
32
|
-
def install(append, case_insensitive, shell, path):
|
33
|
-
"""Install the click-completion-command completion"""
|
34
|
-
extra_env = {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} if case_insensitive else {}
|
35
|
-
shell, path = click_completion.core.install(shell=shell, path=path, append=append, extra_env=extra_env)
|
36
|
-
click.echo(f"{shell} completion installed in {path}")
|
16
|
+
cls = click.shell_completion.get_completion_class(shell)
|
17
|
+
prog_name = _detect_program_name()
|
18
|
+
complete_var = f"_{prog_name}_COMPLETE".replace("-", "_").upper()
|
19
|
+
print(cls(cli, {}, prog_name, complete_var).source())
|
20
|
+
print("# Add the following line to your shell configuration file to have aprsd command line completion")
|
21
|
+
print("# but remove the leading '#' character.")
|
22
|
+
print(f"# eval \"$(aprsd completion {shell})\"")
|
aprsd/cmds/fetch_stats.py
CHANGED
@@ -1,10 +1,9 @@
|
|
1
|
-
# Fetch active stats from a remote running instance of aprsd
|
2
|
-
# This uses the RPC server to fetch the stats from the remote server.
|
3
|
-
|
1
|
+
# Fetch active stats from a remote running instance of aprsd admin web interface.
|
4
2
|
import logging
|
5
3
|
|
6
4
|
import click
|
7
5
|
from oslo_config import cfg
|
6
|
+
import requests
|
8
7
|
from rich.console import Console
|
9
8
|
from rich.table import Table
|
10
9
|
|
@@ -12,7 +11,6 @@ from rich.table import Table
|
|
12
11
|
import aprsd
|
13
12
|
from aprsd import cli_helper
|
14
13
|
from aprsd.main import cli
|
15
|
-
from aprsd.rpc import client as rpc_client
|
16
14
|
|
17
15
|
|
18
16
|
# setup the global logger
|
@@ -26,87 +24,80 @@ CONF = cfg.CONF
|
|
26
24
|
@click.option(
|
27
25
|
"--host", type=str,
|
28
26
|
default=None,
|
29
|
-
help="IP address of the remote aprsd
|
27
|
+
help="IP address of the remote aprsd admin web ui fetch stats from.",
|
30
28
|
)
|
31
29
|
@click.option(
|
32
30
|
"--port", type=int,
|
33
31
|
default=None,
|
34
|
-
help="Port of the remote aprsd
|
35
|
-
)
|
36
|
-
@click.option(
|
37
|
-
"--magic-word", type=str,
|
38
|
-
default=None,
|
39
|
-
help="Magic word of the remote aprsd server rpc port to fetch stats from.",
|
32
|
+
help="Port of the remote aprsd web admin interface to fetch stats from.",
|
40
33
|
)
|
41
34
|
@click.pass_context
|
42
35
|
@cli_helper.process_standard_options
|
43
|
-
def fetch_stats(ctx, host, port
|
44
|
-
"""Fetch stats from a
|
45
|
-
|
36
|
+
def fetch_stats(ctx, host, port):
|
37
|
+
"""Fetch stats from a APRSD admin web interface."""
|
38
|
+
console = Console()
|
39
|
+
console.print(f"APRSD Fetch-Stats started version: {aprsd.__version__}")
|
46
40
|
|
47
41
|
CONF.log_opt_values(LOG, logging.DEBUG)
|
48
42
|
if not host:
|
49
|
-
host = CONF.
|
43
|
+
host = CONF.admin.web_ip
|
50
44
|
if not port:
|
51
|
-
port = CONF.
|
52
|
-
if not magic_word:
|
53
|
-
magic_word = CONF.rpc_settings.magic_word
|
45
|
+
port = CONF.admin.web_port
|
54
46
|
|
55
|
-
msg = f"Fetching stats from {host}:{port}
|
56
|
-
console = Console()
|
47
|
+
msg = f"Fetching stats from {host}:{port}"
|
57
48
|
console.print(msg)
|
58
49
|
with console.status(msg):
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
50
|
+
response = requests.get(f"http://{host}:{port}/stats", timeout=120)
|
51
|
+
if not response:
|
52
|
+
console.print(
|
53
|
+
f"Failed to fetch stats from {host}:{port}?",
|
54
|
+
style="bold red",
|
55
|
+
)
|
65
56
|
return
|
57
|
+
|
58
|
+
stats = response.json()
|
59
|
+
if not stats:
|
60
|
+
console.print(
|
61
|
+
f"Failed to fetch stats from aprsd admin ui at {host}:{port}",
|
62
|
+
style="bold red",
|
63
|
+
)
|
64
|
+
return
|
65
|
+
|
66
66
|
aprsd_title = (
|
67
67
|
"APRSD "
|
68
|
-
f"[bold cyan]v{stats['
|
69
|
-
f"Callsign [bold green]{stats['
|
70
|
-
f"Uptime [bold yellow]{stats['
|
68
|
+
f"[bold cyan]v{stats['APRSDStats']['version']}[/] "
|
69
|
+
f"Callsign [bold green]{stats['APRSDStats']['callsign']}[/] "
|
70
|
+
f"Uptime [bold yellow]{stats['APRSDStats']['uptime']}[/]"
|
71
71
|
)
|
72
72
|
|
73
|
-
console.rule(f"Stats from {host}:{port}
|
73
|
+
console.rule(f"Stats from {host}:{port}")
|
74
74
|
console.print("\n\n")
|
75
75
|
console.rule(aprsd_title)
|
76
76
|
|
77
77
|
# Show the connection to APRS
|
78
78
|
# It can be a connection to an APRS-IS server or a local TNC via KISS or KISSTCP
|
79
79
|
if "aprs-is" in stats:
|
80
|
-
title = f"APRS-IS Connection {stats['
|
80
|
+
title = f"APRS-IS Connection {stats['APRSClientStats']['server_string']}"
|
81
81
|
table = Table(title=title)
|
82
82
|
table.add_column("Key")
|
83
83
|
table.add_column("Value")
|
84
|
-
for key, value in stats["
|
84
|
+
for key, value in stats["APRSClientStats"].items():
|
85
85
|
table.add_row(key, value)
|
86
86
|
console.print(table)
|
87
87
|
|
88
88
|
threads_table = Table(title="Threads")
|
89
89
|
threads_table.add_column("Name")
|
90
90
|
threads_table.add_column("Alive?")
|
91
|
-
for name, alive in stats["
|
91
|
+
for name, alive in stats["APRSDThreadList"].items():
|
92
92
|
threads_table.add_row(name, str(alive))
|
93
93
|
|
94
94
|
console.print(threads_table)
|
95
95
|
|
96
|
-
msgs_table = Table(title="Messages")
|
97
|
-
msgs_table.add_column("Key")
|
98
|
-
msgs_table.add_column("Value")
|
99
|
-
for key, value in stats["messages"].items():
|
100
|
-
msgs_table.add_row(key, str(value))
|
101
|
-
|
102
|
-
console.print(msgs_table)
|
103
|
-
|
104
96
|
packet_totals = Table(title="Packet Totals")
|
105
97
|
packet_totals.add_column("Key")
|
106
98
|
packet_totals.add_column("Value")
|
107
|
-
packet_totals.add_row("Total Received", str(stats["
|
108
|
-
packet_totals.add_row("Total Sent", str(stats["
|
109
|
-
packet_totals.add_row("Total Tracked", str(stats["packets"]["total_tracked"]))
|
99
|
+
packet_totals.add_row("Total Received", str(stats["PacketList"]["rx"]))
|
100
|
+
packet_totals.add_row("Total Sent", str(stats["PacketList"]["tx"]))
|
110
101
|
console.print(packet_totals)
|
111
102
|
|
112
103
|
# Show each of the packet types
|
@@ -114,47 +105,52 @@ def fetch_stats(ctx, host, port, magic_word):
|
|
114
105
|
packets_table.add_column("Packet Type")
|
115
106
|
packets_table.add_column("TX")
|
116
107
|
packets_table.add_column("RX")
|
117
|
-
for key, value in stats["
|
108
|
+
for key, value in stats["PacketList"]["packets"].items():
|
118
109
|
packets_table.add_row(key, str(value["tx"]), str(value["rx"]))
|
119
110
|
|
120
111
|
console.print(packets_table)
|
121
112
|
|
122
113
|
if "plugins" in stats:
|
123
|
-
count = len(stats["
|
114
|
+
count = len(stats["PluginManager"])
|
124
115
|
plugins_table = Table(title=f"Plugins ({count})")
|
125
116
|
plugins_table.add_column("Plugin")
|
126
117
|
plugins_table.add_column("Enabled")
|
127
118
|
plugins_table.add_column("Version")
|
128
119
|
plugins_table.add_column("TX")
|
129
120
|
plugins_table.add_column("RX")
|
130
|
-
|
121
|
+
plugins = stats["PluginManager"]
|
122
|
+
for key, value in plugins.items():
|
131
123
|
plugins_table.add_row(
|
132
124
|
key,
|
133
|
-
str(
|
134
|
-
|
135
|
-
str(
|
136
|
-
str(
|
125
|
+
str(plugins[key]["enabled"]),
|
126
|
+
plugins[key]["version"],
|
127
|
+
str(plugins[key]["tx"]),
|
128
|
+
str(plugins[key]["rx"]),
|
137
129
|
)
|
138
130
|
|
139
131
|
console.print(plugins_table)
|
140
132
|
|
141
|
-
|
142
|
-
|
133
|
+
seen_list = stats.get("SeenList")
|
134
|
+
|
135
|
+
if seen_list:
|
136
|
+
count = len(seen_list)
|
143
137
|
seen_table = Table(title=f"Seen List ({count})")
|
144
138
|
seen_table.add_column("Callsign")
|
145
139
|
seen_table.add_column("Message Count")
|
146
140
|
seen_table.add_column("Last Heard")
|
147
|
-
for key, value in
|
141
|
+
for key, value in seen_list.items():
|
148
142
|
seen_table.add_row(key, str(value["count"]), value["last"])
|
149
143
|
|
150
144
|
console.print(seen_table)
|
151
145
|
|
152
|
-
|
153
|
-
|
146
|
+
watch_list = stats.get("WatchList")
|
147
|
+
|
148
|
+
if watch_list:
|
149
|
+
count = len(watch_list)
|
154
150
|
watch_table = Table(title=f"Watch List ({count})")
|
155
151
|
watch_table.add_column("Callsign")
|
156
152
|
watch_table.add_column("Last Heard")
|
157
|
-
for key, value in
|
153
|
+
for key, value in watch_list.items():
|
158
154
|
watch_table.add_row(key, value["last"])
|
159
155
|
|
160
156
|
console.print(watch_table)
|