woninet 2.0.1__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.
- woninet/__init__.py +2 -0
- woninet/__main__.py +4 -0
- woninet/core/__init__.py +2 -0
- woninet/core/alerts.py +251 -0
- woninet/core/collectors.py +303 -0
- woninet/core/engine.py +353 -0
- woninet/core/graph.py +63 -0
- woninet/core/manual_ip_enumerator.py +33 -0
- woninet/core/models.py +124 -0
- woninet/core/storage.py +163 -0
- woninet/core/subnet_enumerator.py +32 -0
- woninet/database/__init__.py +2 -0
- woninet/database/base.py +9 -0
- woninet/database/engine.py +44 -0
- woninet/database/repositories/__init__.py +2 -0
- woninet/database/repositories/alert_event_repository.py +69 -0
- woninet/database/repositories/alert_state_repository.py +81 -0
- woninet/database/repositories/device_repository.py +105 -0
- woninet/database/repositories/metric_repository.py +75 -0
- woninet/database/tables.py +89 -0
- woninet/logging.yaml +49 -0
- woninet/main.py +225 -0
- woninet/server/__init__.py +2 -0
- woninet/server/app.py +101 -0
- woninet/server/dependencies.py +25 -0
- woninet/server/routes/__init__.py +2 -0
- woninet/server/routes/api/__init__.py +0 -0
- woninet/server/routes/api/devices.py +43 -0
- woninet/server/routes/api/stats.py +34 -0
- woninet/server/routes/page/__init__.py +0 -0
- woninet/server/routes/page/devices.py +28 -0
- woninet/server/static/css/404.css +164 -0
- woninet/server/static/css/style.css +368 -0
- woninet/server/static/js/app.js +242 -0
- woninet/server/static/js/device_info.js +95 -0
- woninet/server/static/swagger-ui/favicon-32x32.png +0 -0
- woninet/server/static/swagger-ui/swagger-ui-bundle.js +2 -0
- woninet/server/static/swagger-ui/swagger-ui.css +3 -0
- woninet/server/templates/404.html +76 -0
- woninet/server/templates/dashboard.html +118 -0
- woninet/server/templates/device_info.html +93 -0
- woninet/utilities/__init__.py +2 -0
- woninet/utilities/arguments.py +94 -0
- woninet/utilities/cli_parser.py +90 -0
- woninet/utilities/detect_ip_range.py +21 -0
- woninet/utilities/ip_validator.py +39 -0
- woninet/utilities/logger.py +97 -0
- woninet-2.0.1.dist-info/METADATA +243 -0
- woninet-2.0.1.dist-info/RECORD +53 -0
- woninet-2.0.1.dist-info/WHEEL +5 -0
- woninet-2.0.1.dist-info/entry_points.txt +2 -0
- woninet-2.0.1.dist-info/licenses/LICENSE +21 -0
- woninet-2.0.1.dist-info/top_level.txt +1 -0
woninet/core/engine.py
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import logging
|
|
3
|
+
import threading
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any
|
|
6
|
+
from sqlalchemy import text, inspect
|
|
7
|
+
from sqlalchemy.exc import SQLAlchemyError
|
|
8
|
+
from icmplib import SocketPermissionError, SocketAddressError
|
|
9
|
+
from woninet.core.models import Device, MetricRecord
|
|
10
|
+
from woninet.core.collectors import PingCollector
|
|
11
|
+
from woninet.core.storage import StorageEngine
|
|
12
|
+
from woninet.core.alerts import AlertEngine, AlertRule
|
|
13
|
+
from woninet.core.subnet_enumerator import SubnetEnumerator
|
|
14
|
+
from woninet.core.manual_ip_enumerator import ManualIPEnumerator
|
|
15
|
+
from woninet.core.graph import GraphEngine
|
|
16
|
+
from woninet.database.engine import DatabaseEngine
|
|
17
|
+
from woninet.database.tables import AlertEventTable
|
|
18
|
+
from woninet.utilities.ip_validator import is_device_ip_valid
|
|
19
|
+
from woninet.utilities.detect_ip_range import detect_ip_range
|
|
20
|
+
|
|
21
|
+
core_logger = logging.getLogger("core")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class NetworkMonitorCore:
|
|
25
|
+
"""
|
|
26
|
+
Central controller coordinating database initialization, telemetry
|
|
27
|
+
collection, persistence, and heuristic alert processing.
|
|
28
|
+
|
|
29
|
+
This class acts as the orchestrator for the network monitoring lifecycle,
|
|
30
|
+
managing thread pools for asynchronous ICMP polling and interfacing with
|
|
31
|
+
the SQLite persistence layer via SQLAlchemy.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
local_ip (str): The source interface IP used for packet origination.
|
|
35
|
+
arp_noise_limit (float): Statistical threshold for filtering jitter
|
|
36
|
+
in ARP resolution timings.
|
|
37
|
+
max_thread_workers (int): The concurrency limit for the
|
|
38
|
+
`ThreadPoolExecutor` handling ICMP probes.
|
|
39
|
+
database_engine (DatabaseEngine): The high-level wrapper for
|
|
40
|
+
SQLAlchemy engine configuration.
|
|
41
|
+
storage (StorageEngine): The Data Access Object (DAO) layer for
|
|
42
|
+
CRUD operations on network metrics.
|
|
43
|
+
alert_engine (AlertEngine): Logic processor for evaluating
|
|
44
|
+
latency-based threshold breaches.
|
|
45
|
+
candidate_devices (list): A list of IP addresses identified during
|
|
46
|
+
the initial enumeration phase.
|
|
47
|
+
_stop_event (threading.Event): Primitive used to signal graceful
|
|
48
|
+
shutdown to the background worker thread.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
local_ip: str,
|
|
54
|
+
target_ip: str,
|
|
55
|
+
arp_noise_limit: float,
|
|
56
|
+
database_path: str,
|
|
57
|
+
max_thread_workers: int,
|
|
58
|
+
alert_rules: dict,
|
|
59
|
+
) -> None:
|
|
60
|
+
"""
|
|
61
|
+
Initialize the monitor core and prepare the persistence layer.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
local_ip (str): Source IP address used to send packets.
|
|
65
|
+
target_ip (str): User-provided IP address or range to scan.
|
|
66
|
+
arp_noise_limit (float): Threshold in milliseconds above which ARP
|
|
67
|
+
fluctuations are treated as noise.
|
|
68
|
+
database_path (str): Path to SQLite database.
|
|
69
|
+
max_thread_workers (int): Maximum number of thread workers used to send ICMP pings.
|
|
70
|
+
alert_rules (dict): Provided alert rules, loaded from config.json.
|
|
71
|
+
"""
|
|
72
|
+
self._running: bool = False
|
|
73
|
+
self._thread: threading.Thread | None = None
|
|
74
|
+
self._stop_event: threading.Event = threading.Event()
|
|
75
|
+
self._start_uptime = datetime.now()
|
|
76
|
+
|
|
77
|
+
self.local_ip: str = local_ip
|
|
78
|
+
self.arp_noise_limit: float = arp_noise_limit
|
|
79
|
+
self.max_thread_workers: int = max_thread_workers
|
|
80
|
+
|
|
81
|
+
# Initialize database
|
|
82
|
+
self.database_engine = DatabaseEngine(database_path=database_path)
|
|
83
|
+
self.database_engine.init_db()
|
|
84
|
+
self._engine = self.database_engine.get_database_enigne()
|
|
85
|
+
self.session_factory = self.database_engine.get_session_factory()
|
|
86
|
+
|
|
87
|
+
core_logger.info(f"Initializing network monitor for {self.local_ip}")
|
|
88
|
+
|
|
89
|
+
self.subnet_enumerator = SubnetEnumerator()
|
|
90
|
+
self.manual_ip_enumerator = ManualIPEnumerator()
|
|
91
|
+
self.storage = StorageEngine(session_factory=self.session_factory)
|
|
92
|
+
|
|
93
|
+
self.candidate_devices = self.enumerate_candidate_devices(target_ip)
|
|
94
|
+
|
|
95
|
+
self.ping_collector = PingCollector()
|
|
96
|
+
|
|
97
|
+
self.alert_engine = AlertEngine(
|
|
98
|
+
storage=self.storage,
|
|
99
|
+
rules=[
|
|
100
|
+
AlertRule(item["metric"], item["threshold"], item["consecutive_checks"])
|
|
101
|
+
for item in alert_rules
|
|
102
|
+
],
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
self.consecutive_checks = {
|
|
106
|
+
item["metric"]: item["consecutive_checks"]
|
|
107
|
+
for item in alert_rules
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
self.graph_engine = GraphEngine()
|
|
111
|
+
|
|
112
|
+
def start(self) -> None:
|
|
113
|
+
"""
|
|
114
|
+
Start the monitoring worker thread.
|
|
115
|
+
"""
|
|
116
|
+
if self._running:
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
self._running = True
|
|
120
|
+
self._stop_event.clear()
|
|
121
|
+
self._thread = threading.Thread(
|
|
122
|
+
target=self.worker_loop, name="woninet-worker", daemon=False
|
|
123
|
+
)
|
|
124
|
+
self._thread.start()
|
|
125
|
+
|
|
126
|
+
def stop(self) -> None:
|
|
127
|
+
"""
|
|
128
|
+
Stop the monitoring worker thread.
|
|
129
|
+
"""
|
|
130
|
+
if not self._running:
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
self._running = False
|
|
134
|
+
self._stop_event.set()
|
|
135
|
+
if self._thread and self._thread.is_alive():
|
|
136
|
+
self._thread.join(timeout=3)
|
|
137
|
+
|
|
138
|
+
def wait(self) -> None:
|
|
139
|
+
"""
|
|
140
|
+
Block until the worker thread finishes.
|
|
141
|
+
"""
|
|
142
|
+
if self._thread:
|
|
143
|
+
self._thread.join()
|
|
144
|
+
|
|
145
|
+
def worker_loop(self) -> None:
|
|
146
|
+
"""
|
|
147
|
+
Main monitoring loop.
|
|
148
|
+
Start the collectors and evaluate alerts.
|
|
149
|
+
"""
|
|
150
|
+
try:
|
|
151
|
+
while self._running and not self._stop_event.is_set():
|
|
152
|
+
if not self._running or self._stop_event.is_set():
|
|
153
|
+
break
|
|
154
|
+
|
|
155
|
+
for result in self.ping_collector.collect(
|
|
156
|
+
candidate_devices=self.candidate_devices,
|
|
157
|
+
local_ip=self.local_ip,
|
|
158
|
+
device_records=self.get_device_history(),
|
|
159
|
+
stop_event=self._stop_event,
|
|
160
|
+
arp_noise_limit=self.arp_noise_limit,
|
|
161
|
+
max_thread_workers=self.max_thread_workers,
|
|
162
|
+
):
|
|
163
|
+
try:
|
|
164
|
+
self.submit_to_history(
|
|
165
|
+
device=result.device,
|
|
166
|
+
metrics=[result.latency, result.packet_loss],
|
|
167
|
+
)
|
|
168
|
+
if result.device is not None:
|
|
169
|
+
self.alert_engine.evaluate(
|
|
170
|
+
ip=result.device.ip,
|
|
171
|
+
metrics_list=[result.latency, result.packet_loss],
|
|
172
|
+
default_consecutive_checks={
|
|
173
|
+
"latency": self.consecutive_checks["latency"],
|
|
174
|
+
"packet_loss": self.consecutive_checks[
|
|
175
|
+
"packet_loss"
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
)
|
|
179
|
+
except AttributeError:
|
|
180
|
+
# If the tuple doesn't have enough values to unpack
|
|
181
|
+
pass
|
|
182
|
+
time.sleep(1)
|
|
183
|
+
except (PermissionError, SocketPermissionError):
|
|
184
|
+
core_logger.error("woninet requires root privileges to scan.")
|
|
185
|
+
self._running = False
|
|
186
|
+
self._stop_event.set()
|
|
187
|
+
except SocketAddressError:
|
|
188
|
+
core_logger.error("Network connection failed. Quitting.")
|
|
189
|
+
self._running = False
|
|
190
|
+
self._stop_event.set()
|
|
191
|
+
except Exception as e:
|
|
192
|
+
core_logger.error(f"Unexpected error in NetworkMonitorCore: {e}")
|
|
193
|
+
self._running = False
|
|
194
|
+
self._stop_event.set()
|
|
195
|
+
finally:
|
|
196
|
+
self._running = False
|
|
197
|
+
self._stop_event.set()
|
|
198
|
+
|
|
199
|
+
def is_alive(self) -> bool:
|
|
200
|
+
"""
|
|
201
|
+
Return the running state of monitor.
|
|
202
|
+
"""
|
|
203
|
+
return self._running
|
|
204
|
+
|
|
205
|
+
def get_device_history(self) -> list[Device]:
|
|
206
|
+
"""
|
|
207
|
+
Return a list of all devices in the history.
|
|
208
|
+
"""
|
|
209
|
+
return self.storage.list_device_history()
|
|
210
|
+
|
|
211
|
+
def get_device_info(
|
|
212
|
+
self, ip: str
|
|
213
|
+
) -> tuple[Device | None, dict[str, str] | None, list[AlertEventTable]]:
|
|
214
|
+
"""
|
|
215
|
+
Retrieve device information and related alert data for a given IP address.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
ip (str): IP address of the device to search for.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
tuple[Device|None,dict[str,str]|None,list[AlertEventTable]]:
|
|
222
|
+
A tuple containing
|
|
223
|
+
|
|
224
|
+
1. The found device, or `None` if no device exists for the given IP.
|
|
225
|
+
2. The current alert state as `(metric_name, value)`, or `None` if no state exists.
|
|
226
|
+
3. The last 10 alert events for the device, or an empty list if none exist.
|
|
227
|
+
"""
|
|
228
|
+
device_info = self.storage.fetch_device_info(ip=ip)
|
|
229
|
+
device_alert_state = self.storage.get_device_alert_state(ip=ip)
|
|
230
|
+
recent_device_alert_events = self.storage.get_recent_device_alert_events(ip=ip)
|
|
231
|
+
return (device_info, device_alert_state, recent_device_alert_events)
|
|
232
|
+
|
|
233
|
+
def device_exists(self, ip: str) -> bool:
|
|
234
|
+
"""
|
|
235
|
+
Return boolean indicating if device exists for a given IP address.
|
|
236
|
+
"""
|
|
237
|
+
exists = self.storage.fetch_device_info(ip=ip)
|
|
238
|
+
if exists:
|
|
239
|
+
return True
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
def classify_recent_alert_events(self) -> list[dict[str, Any]]:
|
|
243
|
+
"""
|
|
244
|
+
Re-order and organize the recent alert events
|
|
245
|
+
dictionary keys and values.
|
|
246
|
+
"""
|
|
247
|
+
result = []
|
|
248
|
+
for event in self.storage.get_recent_alert_events():
|
|
249
|
+
result.append(
|
|
250
|
+
{
|
|
251
|
+
"device_ip": event.device_ip,
|
|
252
|
+
"id": event.id,
|
|
253
|
+
"metric": event.metric,
|
|
254
|
+
"event_type": event.event_type,
|
|
255
|
+
"value": event.value,
|
|
256
|
+
"timestamp": event.timestamp.isoformat(),
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
return result
|
|
260
|
+
|
|
261
|
+
def count_resources(self) -> tuple[int, int]:
|
|
262
|
+
"""
|
|
263
|
+
Return number of devices and metrics in the history.
|
|
264
|
+
"""
|
|
265
|
+
return self.storage.count_devices_and_metrics()
|
|
266
|
+
|
|
267
|
+
def submit_to_history(self, device: str, metrics: list[MetricRecord]) -> None:
|
|
268
|
+
"""
|
|
269
|
+
Handle storing new devices and metrics in history.
|
|
270
|
+
"""
|
|
271
|
+
if isinstance(device, Device):
|
|
272
|
+
self.storage.store_device(device=device)
|
|
273
|
+
for metric in metrics:
|
|
274
|
+
if isinstance(metric, MetricRecord):
|
|
275
|
+
self.storage.store_metric(metric=metric)
|
|
276
|
+
|
|
277
|
+
def enumerate_candidate_devices(
|
|
278
|
+
self, target_ip_list: list[str] | None
|
|
279
|
+
) -> dict[str, Device]:
|
|
280
|
+
"""
|
|
281
|
+
Control enumeration based on user-provided IP addresses. Use range enumeration
|
|
282
|
+
if IP range is detected, otherwise use the single IP address. Enumerate on the
|
|
283
|
+
/24 subnet if no target IP address is provided.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
target_ip (list[str]|None): Target IP addresses to create candidate devices on.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
dict[str,Device]: Candidate devices.
|
|
290
|
+
|
|
291
|
+
Raises:
|
|
292
|
+
ValueError: If IP addresses are not valid.
|
|
293
|
+
"""
|
|
294
|
+
candidate_devices = {}
|
|
295
|
+
if target_ip_list is not None:
|
|
296
|
+
for target_ip in target_ip_list:
|
|
297
|
+
if detect_ip_range(ip=target_ip):
|
|
298
|
+
candidate_devices.update(
|
|
299
|
+
self.manual_ip_enumerator.enumerate_range(target_ip)
|
|
300
|
+
)
|
|
301
|
+
else:
|
|
302
|
+
candidate_devices[target_ip] = Device(ip=target_ip)
|
|
303
|
+
else:
|
|
304
|
+
candidate_devices.update(self.subnet_enumerator.enumerate(self.local_ip))
|
|
305
|
+
if not is_device_ip_valid(candidate_devices):
|
|
306
|
+
raise ValueError("IP address is not valid")
|
|
307
|
+
return candidate_devices
|
|
308
|
+
|
|
309
|
+
def uptime(self) -> int:
|
|
310
|
+
"""
|
|
311
|
+
Calculate the uptime of program from the initialization of the
|
|
312
|
+
NetworkMonitorCore class.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
int: Uptime in seconds.
|
|
316
|
+
"""
|
|
317
|
+
uptime_seconds = datetime.now() - self._start_uptime
|
|
318
|
+
return uptime_seconds.seconds
|
|
319
|
+
|
|
320
|
+
def database_health(self) -> dict[str, str]:
|
|
321
|
+
"""
|
|
322
|
+
Check the health status of the database.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
dict[str,str]: A dictionary with two keys
|
|
326
|
+
|
|
327
|
+
- 'connection': 'ok' if executing `SELECT 1` succeeds, otherwise 'error'.
|
|
328
|
+
- 'schema': 'ok' if database table inspection succeeds, otherwise 'error'.
|
|
329
|
+
If the database connection fails, this value is 'unknown'.
|
|
330
|
+
"""
|
|
331
|
+
result = {
|
|
332
|
+
"connection": "error",
|
|
333
|
+
"schema": "unknown",
|
|
334
|
+
}
|
|
335
|
+
try:
|
|
336
|
+
# Check connection
|
|
337
|
+
with self._engine.connect() as conn:
|
|
338
|
+
conn.execute(text("SELECT 1"))
|
|
339
|
+
result["connection"] = "ok"
|
|
340
|
+
|
|
341
|
+
# Check tables existence
|
|
342
|
+
inspect_engine = inspect(self._engine)
|
|
343
|
+
tables = inspect_engine.get_table_names()
|
|
344
|
+
required = {"devices", "metrics"}
|
|
345
|
+
if required.issubset(set(tables)):
|
|
346
|
+
result["schema"] = "ok"
|
|
347
|
+
else:
|
|
348
|
+
result["schema"] = "error"
|
|
349
|
+
except SQLAlchemyError:
|
|
350
|
+
# Keep default values
|
|
351
|
+
pass
|
|
352
|
+
|
|
353
|
+
return result
|
woninet/core/graph.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from matplotlib import pyplot as plt
|
|
3
|
+
from woninet.database.tables import AlertEventTable
|
|
4
|
+
|
|
5
|
+
CHARTS_PATH = Path(__file__).parent.parent / "charts"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GraphEngine:
|
|
9
|
+
"""
|
|
10
|
+
Engine for generating graphs and visualizations of network device metrics.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def design_device_latency_events(
|
|
14
|
+
self, ip: str, recent_device_alert_events: list[AlertEventTable]
|
|
15
|
+
) -> None:
|
|
16
|
+
"""
|
|
17
|
+
Generate and save a latency chart for a device.
|
|
18
|
+
|
|
19
|
+
Creates a line plot showing latency values over time with trigger and recovery events
|
|
20
|
+
marked as scatter points. The chart is saved as a PNG file.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
ip: The IP address of the device for the chart title and filename.
|
|
24
|
+
recent_device_alert_events: List of alert events containing latency metrics and event types.
|
|
25
|
+
"""
|
|
26
|
+
times = []
|
|
27
|
+
latency_values = []
|
|
28
|
+
trigger_times = []
|
|
29
|
+
trigger_values = []
|
|
30
|
+
recover_times = []
|
|
31
|
+
recover_values = []
|
|
32
|
+
|
|
33
|
+
for event in recent_device_alert_events:
|
|
34
|
+
if event.metric == "latency":
|
|
35
|
+
times.append(event.timestamp)
|
|
36
|
+
latency_values.append(event.value)
|
|
37
|
+
if event.event_type == "trigger":
|
|
38
|
+
trigger_times.append(event.timestamp)
|
|
39
|
+
trigger_values.append(event.value)
|
|
40
|
+
elif event.event_type == "recover":
|
|
41
|
+
recover_times.append(event.timestamp)
|
|
42
|
+
recover_values.append(event.value)
|
|
43
|
+
|
|
44
|
+
fig, ax = plt.subplots(figsize=(16, 8), dpi=150)
|
|
45
|
+
|
|
46
|
+
ax.plot(times, latency_values, label="Latency")
|
|
47
|
+
ax.axhline(100, linestyle="--", label="Alert Threshold")
|
|
48
|
+
ax.scatter(
|
|
49
|
+
trigger_times, trigger_values, marker="^", color="red", label="Trigger"
|
|
50
|
+
)
|
|
51
|
+
ax.scatter(
|
|
52
|
+
recover_times, recover_values, marker="v", color="blue", label="Recover"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
ax.set_title(f"{ip} - Latency")
|
|
56
|
+
ax.set_xlabel("Time")
|
|
57
|
+
ax.set_ylabel("Latency (ms)")
|
|
58
|
+
ax.legend()
|
|
59
|
+
ax.grid(True, alpha=0.3)
|
|
60
|
+
|
|
61
|
+
fig.autofmt_xdate()
|
|
62
|
+
fig.savefig(f"{CHARTS_PATH}/{ip}.png", bbox_inches="tight")
|
|
63
|
+
plt.close(fig)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from woninet.core.models import Device
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ManualIPEnumerator:
|
|
5
|
+
"""
|
|
6
|
+
Build a dictionary of candidate devices for user-provided IP addresses.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def enumerate(self, ip_list: list[str]) -> dict[str, str]:
|
|
10
|
+
devices = {}
|
|
11
|
+
for ip in ip_list:
|
|
12
|
+
devices[ip] = Device(ip=ip)
|
|
13
|
+
return devices
|
|
14
|
+
|
|
15
|
+
def enumerate_range(self, ip_range: str) -> dict[str, Device]:
|
|
16
|
+
"""
|
|
17
|
+
Build a dictionary of candidate devices for user-provided IP addresses range.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
ip_range (str): IP address as a range (e.g. 192.168.1.10-50).
|
|
21
|
+
|
|
22
|
+
Raises:
|
|
23
|
+
ValueError: If the range notation is not in the last octet.
|
|
24
|
+
"""
|
|
25
|
+
base, last_octect = ip_range.rsplit(".", maxsplit=1)
|
|
26
|
+
if "-" in base:
|
|
27
|
+
raise ValueError("Range enumeraton is only supported on the last octet.")
|
|
28
|
+
start, stop = last_octect.split("-")
|
|
29
|
+
devices = {}
|
|
30
|
+
for i in range(int(start), int(stop) + 1):
|
|
31
|
+
ip = f"{base}.{i}"
|
|
32
|
+
devices[ip] = Device(ip=ip)
|
|
33
|
+
return devices
|
woninet/core/models.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Device:
|
|
6
|
+
"""
|
|
7
|
+
Represents a discovered network device and stores identity information
|
|
8
|
+
and device state.
|
|
9
|
+
|
|
10
|
+
Attributes:
|
|
11
|
+
ip (str): IP address of the host.
|
|
12
|
+
last_seen (datetime|None): Last time device responded to ICMP request; Otherwise `None`.
|
|
13
|
+
exists (bool): If device responded to the last ARP scan.
|
|
14
|
+
reachable (bool): If device responded to the last ICMP scan.
|
|
15
|
+
mac (str): MAC address if known; Otherwise `None`.
|
|
16
|
+
latency (float): The last latency of host if reachable; Otherwise zero.
|
|
17
|
+
packet_loss (float): Most recent packet-loss ratio in the range [0.0, 100.0].
|
|
18
|
+
A value of 100.0 indicates 100% packet loss or that the device is unreachable.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, ip: str) -> None:
|
|
22
|
+
"""
|
|
23
|
+
Initialize the device model.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
ip: IP address of the host
|
|
27
|
+
"""
|
|
28
|
+
self.ip: str = ip
|
|
29
|
+
self.last_seen: datetime | None = None
|
|
30
|
+
self.exists: bool = False
|
|
31
|
+
self.reachable: bool = False
|
|
32
|
+
self.mac: str | None = None
|
|
33
|
+
self.latency: float = 0
|
|
34
|
+
self.packet_loss: float = 100
|
|
35
|
+
|
|
36
|
+
def update_seen(self) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Update timestamp indicating the device responded recently.
|
|
39
|
+
"""
|
|
40
|
+
self.last_seen = datetime.now()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MetricRecord:
|
|
44
|
+
"""
|
|
45
|
+
Represents a single collected metric measurement
|
|
46
|
+
for a specific device at a specific time.
|
|
47
|
+
|
|
48
|
+
Attributes:
|
|
49
|
+
device_ip (str): IP address of the host.
|
|
50
|
+
metric (str): Name of the metric.
|
|
51
|
+
value (float): Recorded value of the metric.
|
|
52
|
+
timestamp (datetime|None): Timestamp when the metric was captured.
|
|
53
|
+
If `None`, defaults to the current time.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
device_ip: str,
|
|
59
|
+
metric: str,
|
|
60
|
+
value: float,
|
|
61
|
+
timestamp: datetime | None = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Initialize a metric record.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
device_ip (str): IP address of the host the metric belongs to.
|
|
68
|
+
metric (str): Name of the recorded metric.
|
|
69
|
+
value (float): Recorded value of the metric.
|
|
70
|
+
timestamp (datetime|None): Timestamp of the measurement.
|
|
71
|
+
If omitted, the current time is used.
|
|
72
|
+
"""
|
|
73
|
+
self.device_ip = device_ip
|
|
74
|
+
self.metric = metric
|
|
75
|
+
self.value = value
|
|
76
|
+
self.timestamp = timestamp or datetime.now()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class HostStatus:
|
|
80
|
+
"""
|
|
81
|
+
Represents a Combined ARP + ICMP status for a given IP.
|
|
82
|
+
|
|
83
|
+
Attributes:
|
|
84
|
+
ip (str): IP address of the host.
|
|
85
|
+
exists (bool): If device responded to the ARP scan
|
|
86
|
+
reachable (bool): If device responded to the ICMP scan
|
|
87
|
+
latency (float): The latency of host if reachable; Otherwise zero.
|
|
88
|
+
packet_loss (float): Most recent packet-loss ratio in the range [0.0, 100.0].
|
|
89
|
+
A value of 100.0 indicates 100% packet loss or that the device is unreachable.
|
|
90
|
+
mac (str): MAC address if known; Otherwise `None`.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
ip: str,
|
|
96
|
+
exists: bool = False,
|
|
97
|
+
reachable: bool = False,
|
|
98
|
+
latency: float = 0.0,
|
|
99
|
+
packet_loss: float = 100.0,
|
|
100
|
+
mac: str | None = None,
|
|
101
|
+
) -> None:
|
|
102
|
+
"""
|
|
103
|
+
Initialize the host status model.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
ip (str): IP address of the host.
|
|
107
|
+
exists (bool): If device responded to the ARP scan
|
|
108
|
+
reachable (bool): If device responded to the ICMP scan
|
|
109
|
+
latency (float): The latency of host if reachable; Otherwise zero.
|
|
110
|
+
mac (str): MAC address if known; Otherwise `None`.
|
|
111
|
+
"""
|
|
112
|
+
self.ip: str = ip
|
|
113
|
+
self.exists: bool = exists
|
|
114
|
+
self.reachable: bool = reachable
|
|
115
|
+
self.latency: float = latency
|
|
116
|
+
self.packet_loss: float = packet_loss
|
|
117
|
+
self.mac: str | None = mac
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass(frozen=True)
|
|
121
|
+
class ScanResult:
|
|
122
|
+
device: Device | None
|
|
123
|
+
latency: float | None
|
|
124
|
+
packet_loss: float | None
|