woninet 2.0.3__tar.gz → 2.1.0__tar.gz
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-2.0.3 → woninet-2.1.0}/PKG-INFO +37 -7
- {woninet-2.0.3 → woninet-2.1.0}/README.md +35 -6
- {woninet-2.0.3 → woninet-2.1.0}/pyproject.toml +2 -1
- {woninet-2.0.3 → woninet-2.1.0}/tests/test_alert_events.py +74 -0
- woninet-2.1.0/tests/test_api.py +37 -0
- woninet-2.1.0/tests/test_system_ping.py +121 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/__init__.py +1 -1
- {woninet-2.0.3 → woninet-2.1.0}/woninet/core/alerts.py +103 -18
- {woninet-2.0.3 → woninet-2.1.0}/woninet/core/collectors.py +56 -18
- {woninet-2.0.3 → woninet-2.1.0}/woninet/core/engine.py +47 -6
- {woninet-2.0.3 → woninet-2.1.0}/woninet/core/graph.py +1 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/core/manual_ip_enumerator.py +3 -3
- {woninet-2.0.3 → woninet-2.1.0}/woninet/core/models.py +25 -11
- {woninet-2.0.3 → woninet-2.1.0}/woninet/core/subnet_enumerator.py +1 -1
- {woninet-2.0.3 → woninet-2.1.0}/woninet/database/engine.py +1 -1
- {woninet-2.0.3 → woninet-2.1.0}/woninet/database/repositories/alert_event_repository.py +1 -1
- {woninet-2.0.3 → woninet-2.1.0}/woninet/database/repositories/alert_state_repository.py +3 -3
- {woninet-2.0.3 → woninet-2.1.0}/woninet/database/repositories/device_repository.py +13 -4
- {woninet-2.0.3 → woninet-2.1.0}/woninet/database/repositories/metric_repository.py +1 -1
- {woninet-2.0.3 → woninet-2.1.0}/woninet/database/tables.py +5 -4
- woninet-2.1.0/woninet/exc.py +2 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/main.py +1 -1
- {woninet-2.0.3 → woninet-2.1.0}/woninet/server/app.py +1 -4
- {woninet-2.0.3 → woninet-2.1.0}/woninet/server/dependencies.py +1 -1
- {woninet-2.0.3 → woninet-2.1.0}/woninet/server/routes/api/devices.py +10 -2
- {woninet-2.0.3 → woninet-2.1.0}/woninet/server/static/css/style.css +17 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/server/static/js/app.js +11 -1
- {woninet-2.0.3 → woninet-2.1.0}/woninet/server/templates/dashboard.html +6 -1
- {woninet-2.0.3 → woninet-2.1.0}/woninet/utilities/arguments.py +2 -2
- {woninet-2.0.3 → woninet-2.1.0}/woninet/utilities/create_configuration.py +8 -1
- woninet-2.1.0/woninet/utilities/detect_gateway.py +66 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/utilities/detect_ip_range.py +1 -1
- {woninet-2.0.3 → woninet-2.1.0}/woninet/utilities/ip_validator.py +1 -1
- woninet-2.1.0/woninet/utilities/ping.py +103 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet.egg-info/PKG-INFO +37 -7
- {woninet-2.0.3 → woninet-2.1.0}/woninet.egg-info/SOURCES.txt +5 -1
- {woninet-2.0.3 → woninet-2.1.0}/woninet.egg-info/requires.txt +1 -0
- woninet-2.0.3/tests/test_api.py +0 -10
- {woninet-2.0.3 → woninet-2.1.0}/LICENSE +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/setup.cfg +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/tests/test_ip_validation.py +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/tests/test_storage.py +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/__main__.py +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/core/__init__.py +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/core/storage.py +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/database/__init__.py +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/database/base.py +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/database/repositories/__init__.py +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/logging.yaml +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/server/__init__.py +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/server/routes/__init__.py +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/server/routes/api/__init__.py +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/server/routes/api/stats.py +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/server/routes/page/__init__.py +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/server/routes/page/devices.py +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/server/static/css/404.css +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/server/static/js/device_info.js +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/server/static/swagger-ui/favicon-32x32.png +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/server/static/swagger-ui/swagger-ui-bundle.js +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/server/static/swagger-ui/swagger-ui.css +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/server/templates/404.html +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/server/templates/device_info.html +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/utilities/__init__.py +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/utilities/cli_parser.py +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet/utilities/logger.py +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet.egg-info/dependency_links.txt +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet.egg-info/entry_points.txt +0 -0
- {woninet-2.0.3 → woninet-2.1.0}/woninet.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: woninet
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: A Python-based ARP+ICMP network monitoring tool
|
|
5
5
|
Author-email: Alireza Teymuri <teymuri.alireza98@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -23,6 +23,7 @@ Requires-Dist: matplotlib<3.12,>=3.11.0
|
|
|
23
23
|
Provides-Extra: test
|
|
24
24
|
Requires-Dist: pytest<9.2,>=9.1.1; extra == "test"
|
|
25
25
|
Requires-Dist: httpx<0.29,>=0.28.1; extra == "test"
|
|
26
|
+
Requires-Dist: ruff<0.16,>=0.15.11; extra == "test"
|
|
26
27
|
Dynamic: license-file
|
|
27
28
|
|
|
28
29
|
# woninet
|
|
@@ -71,6 +72,8 @@ IPs and filters out Wi‑Fi ARP‑delay noise.)
|
|
|
71
72
|
|
|
72
73
|
### Unix/Linux
|
|
73
74
|
|
|
75
|
+
1. **Method 1 (recommended (Uses the `icmplib` module)):**
|
|
76
|
+
|
|
74
77
|
Create and activate a virtual environment:
|
|
75
78
|
|
|
76
79
|
```shell
|
|
@@ -96,7 +99,28 @@ Then run:
|
|
|
96
99
|
woninet --help
|
|
97
100
|
```
|
|
98
101
|
|
|
99
|
-
|
|
102
|
+
2. **Method 2 (Uses the system's `ping` command as a fallback):**
|
|
103
|
+
|
|
104
|
+
Create and activate a virtual environment:
|
|
105
|
+
|
|
106
|
+
```shell
|
|
107
|
+
python3 -m venv venv
|
|
108
|
+
source venv/bin/activate
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Install from PyPI:
|
|
112
|
+
|
|
113
|
+
```shell
|
|
114
|
+
pip install woninet
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Then run:
|
|
118
|
+
|
|
119
|
+
```shell
|
|
120
|
+
woninet --help
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### macOS/Windows
|
|
100
124
|
|
|
101
125
|
Install from PyPI:
|
|
102
126
|
|
|
@@ -173,7 +197,7 @@ sudo ../venv/bin/python3 -m woninet
|
|
|
173
197
|
## Configuration
|
|
174
198
|
|
|
175
199
|
You can define alert rules in a JSON file. Rename the example file at
|
|
176
|
-
`
|
|
200
|
+
`config.example.json` to `config.json`. The application will load the alert
|
|
177
201
|
rules from that file at startup.
|
|
178
202
|
|
|
179
203
|
### Example:
|
|
@@ -200,6 +224,11 @@ rules from that file at startup.
|
|
|
200
224
|
"metric": "packet_loss",
|
|
201
225
|
"threshold": 0.0,
|
|
202
226
|
"consecutive_checks": 1
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"metric": "jitter",
|
|
230
|
+
"threshold": 40.0,
|
|
231
|
+
"consecutive_checks": 3
|
|
203
232
|
}
|
|
204
233
|
]
|
|
205
234
|
}
|
|
@@ -224,7 +253,7 @@ Higher values may increase system load and latency.
|
|
|
224
253
|
|
|
225
254
|
#### Alert Rules
|
|
226
255
|
|
|
227
|
-
- **metric**: The metric name to evaluate (`latency` or `
|
|
256
|
+
- **metric**: The metric name to evaluate (`latency`, `packet_loss` or `jitter`)
|
|
228
257
|
- **threshold**: The value that triggers the alert. For latency, this is in milliseconds.
|
|
229
258
|
For packet loss, this is a percentage (0.0-100.0).
|
|
230
259
|
- **consecutive_checks**: The number of consecutive evaluations required before the alert
|
|
@@ -235,10 +264,11 @@ state changes. Higher values reduce false positives.
|
|
|
235
264
|
- **Packet Loss**: Values are in the range [0.0, 100.0]. A value of 100.0 indicates 100%
|
|
236
265
|
packet loss. A value of 0.0 will trigger on any packet loss.
|
|
237
266
|
- **Latency**: All latency thresholds are evaluated in milliseconds.
|
|
267
|
+
- **Jitter**: All jitter thresholds are evaluated in milliseconds.
|
|
238
268
|
|
|
239
269
|
## Usage
|
|
240
270
|
|
|
241
|
-
This [guide](
|
|
271
|
+
This [guide](https://github.com/teymuri-alireza/woninet/blob/main/docs/usage.md) explains common ways to run *woninet* and how to resolve common issues.
|
|
242
272
|
|
|
243
273
|
## Testing
|
|
244
274
|
|
|
@@ -263,10 +293,10 @@ The test suite currently focuses on API behavior and core logic, and it uses pyt
|
|
|
263
293
|
|
|
264
294
|
## Documentation
|
|
265
295
|
|
|
266
|
-
if you have any question or need clarification, check the [documentation](
|
|
296
|
+
if you have any question or need clarification, check the [documentation](.https://github.com/teymuri-alireza/woninet/blob/main/docs/documentation.md) guide.
|
|
267
297
|
|
|
268
298
|
## Contribution
|
|
269
299
|
|
|
270
300
|
Thanks for considering contributing to *woninet!*
|
|
271
301
|
To help us maintain code quality, stability, and a predictable release process, please review
|
|
272
|
-
and follow the guidelines in our [Contributing Guide.](
|
|
302
|
+
and follow the guidelines in our [Contributing Guide.](https://github.com/teymuri-alireza/woninet/blob/main/CONTRIBUTING.md)
|
|
@@ -44,6 +44,8 @@ IPs and filters out Wi‑Fi ARP‑delay noise.)
|
|
|
44
44
|
|
|
45
45
|
### Unix/Linux
|
|
46
46
|
|
|
47
|
+
1. **Method 1 (recommended (Uses the `icmplib` module)):**
|
|
48
|
+
|
|
47
49
|
Create and activate a virtual environment:
|
|
48
50
|
|
|
49
51
|
```shell
|
|
@@ -69,7 +71,28 @@ Then run:
|
|
|
69
71
|
woninet --help
|
|
70
72
|
```
|
|
71
73
|
|
|
72
|
-
|
|
74
|
+
2. **Method 2 (Uses the system's `ping` command as a fallback):**
|
|
75
|
+
|
|
76
|
+
Create and activate a virtual environment:
|
|
77
|
+
|
|
78
|
+
```shell
|
|
79
|
+
python3 -m venv venv
|
|
80
|
+
source venv/bin/activate
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Install from PyPI:
|
|
84
|
+
|
|
85
|
+
```shell
|
|
86
|
+
pip install woninet
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Then run:
|
|
90
|
+
|
|
91
|
+
```shell
|
|
92
|
+
woninet --help
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### macOS/Windows
|
|
73
96
|
|
|
74
97
|
Install from PyPI:
|
|
75
98
|
|
|
@@ -146,7 +169,7 @@ sudo ../venv/bin/python3 -m woninet
|
|
|
146
169
|
## Configuration
|
|
147
170
|
|
|
148
171
|
You can define alert rules in a JSON file. Rename the example file at
|
|
149
|
-
`
|
|
172
|
+
`config.example.json` to `config.json`. The application will load the alert
|
|
150
173
|
rules from that file at startup.
|
|
151
174
|
|
|
152
175
|
### Example:
|
|
@@ -173,6 +196,11 @@ rules from that file at startup.
|
|
|
173
196
|
"metric": "packet_loss",
|
|
174
197
|
"threshold": 0.0,
|
|
175
198
|
"consecutive_checks": 1
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"metric": "jitter",
|
|
202
|
+
"threshold": 40.0,
|
|
203
|
+
"consecutive_checks": 3
|
|
176
204
|
}
|
|
177
205
|
]
|
|
178
206
|
}
|
|
@@ -197,7 +225,7 @@ Higher values may increase system load and latency.
|
|
|
197
225
|
|
|
198
226
|
#### Alert Rules
|
|
199
227
|
|
|
200
|
-
- **metric**: The metric name to evaluate (`latency` or `
|
|
228
|
+
- **metric**: The metric name to evaluate (`latency`, `packet_loss` or `jitter`)
|
|
201
229
|
- **threshold**: The value that triggers the alert. For latency, this is in milliseconds.
|
|
202
230
|
For packet loss, this is a percentage (0.0-100.0).
|
|
203
231
|
- **consecutive_checks**: The number of consecutive evaluations required before the alert
|
|
@@ -208,10 +236,11 @@ state changes. Higher values reduce false positives.
|
|
|
208
236
|
- **Packet Loss**: Values are in the range [0.0, 100.0]. A value of 100.0 indicates 100%
|
|
209
237
|
packet loss. A value of 0.0 will trigger on any packet loss.
|
|
210
238
|
- **Latency**: All latency thresholds are evaluated in milliseconds.
|
|
239
|
+
- **Jitter**: All jitter thresholds are evaluated in milliseconds.
|
|
211
240
|
|
|
212
241
|
## Usage
|
|
213
242
|
|
|
214
|
-
This [guide](
|
|
243
|
+
This [guide](https://github.com/teymuri-alireza/woninet/blob/main/docs/usage.md) explains common ways to run *woninet* and how to resolve common issues.
|
|
215
244
|
|
|
216
245
|
## Testing
|
|
217
246
|
|
|
@@ -236,10 +265,10 @@ The test suite currently focuses on API behavior and core logic, and it uses pyt
|
|
|
236
265
|
|
|
237
266
|
## Documentation
|
|
238
267
|
|
|
239
|
-
if you have any question or need clarification, check the [documentation](
|
|
268
|
+
if you have any question or need clarification, check the [documentation](.https://github.com/teymuri-alireza/woninet/blob/main/docs/documentation.md) guide.
|
|
240
269
|
|
|
241
270
|
## Contribution
|
|
242
271
|
|
|
243
272
|
Thanks for considering contributing to *woninet!*
|
|
244
273
|
To help us maintain code quality, stability, and a predictable release process, please review
|
|
245
|
-
and follow the guidelines in our [Contributing Guide.](
|
|
274
|
+
and follow the guidelines in our [Contributing Guide.](https://github.com/teymuri-alireza/woninet/blob/main/CONTRIBUTING.md)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "woninet"
|
|
7
|
-
version = "2.0
|
|
7
|
+
version = "2.1.0"
|
|
8
8
|
description = "A Python-based ARP+ICMP network monitoring tool"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -52,4 +52,5 @@ woninet = "woninet.main:main"
|
|
|
52
52
|
test = [
|
|
53
53
|
"pytest>=9.1.1,<9.2",
|
|
54
54
|
"httpx>=0.28.1,<0.29",
|
|
55
|
+
"ruff>=0.15.11,<0.16",
|
|
55
56
|
]
|
|
@@ -71,6 +71,40 @@ def test_packet_loss_trigger_alert_event(db_session):
|
|
|
71
71
|
assert packet_loss.event_type == "trigger"
|
|
72
72
|
|
|
73
73
|
|
|
74
|
+
def test_jitter_trigger_alert_event(db_session):
|
|
75
|
+
storage = StorageEngine(session_factory=lambda: db_session)
|
|
76
|
+
|
|
77
|
+
metric = MetricRecord(device_ip="192.168.1.10", metric="jitter", value=60.0)
|
|
78
|
+
storage.store_metric(metric)
|
|
79
|
+
|
|
80
|
+
alert_rule = {"metric": "jitter", "threshold": 40.0, "consecutive_checks": 0}
|
|
81
|
+
|
|
82
|
+
alert = AlertEngine(
|
|
83
|
+
storage=storage,
|
|
84
|
+
rules=[
|
|
85
|
+
AlertRule(
|
|
86
|
+
alert_rule["metric"],
|
|
87
|
+
alert_rule["threshold"],
|
|
88
|
+
alert_rule["consecutive_checks"],
|
|
89
|
+
)
|
|
90
|
+
],
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
alert.evaluate(
|
|
94
|
+
ip="192.168.1.10",
|
|
95
|
+
metrics_list=[
|
|
96
|
+
MetricRecord(device_ip="192.168.1.10", metric="jitter", value=60.0),
|
|
97
|
+
],
|
|
98
|
+
default_consecutive_checks={
|
|
99
|
+
"jitter": 0,
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
jitter = storage.get_recent_alert_events()[0]
|
|
104
|
+
|
|
105
|
+
assert jitter.event_type == "trigger"
|
|
106
|
+
|
|
107
|
+
|
|
74
108
|
def test_latency_recover_alert_event(db_session):
|
|
75
109
|
storage = StorageEngine(session_factory=lambda: db_session)
|
|
76
110
|
|
|
@@ -149,3 +183,43 @@ def test_packet_loss_recover_alert_event(db_session):
|
|
|
149
183
|
packet_loss = storage.get_recent_alert_events()[0]
|
|
150
184
|
|
|
151
185
|
assert packet_loss.event_type == "recover"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_jitter_recover_alert_event(db_session):
|
|
189
|
+
storage = StorageEngine(session_factory=lambda: db_session)
|
|
190
|
+
|
|
191
|
+
metric = MetricRecord(device_ip="192.168.1.10", metric="jitter", value=20.0)
|
|
192
|
+
storage.store_metric(metric)
|
|
193
|
+
|
|
194
|
+
state = storage.get_or_create_alert_state(
|
|
195
|
+
ip="192.168.1.10", metric="jitter", consecutive_checks=0
|
|
196
|
+
)
|
|
197
|
+
state.state = "warning"
|
|
198
|
+
storage.update_alert_state(state)
|
|
199
|
+
|
|
200
|
+
alert_rule = {"metric": "jitter", "threshold": 40.0, "consecutive_checks": 0}
|
|
201
|
+
|
|
202
|
+
alert = AlertEngine(
|
|
203
|
+
storage=storage,
|
|
204
|
+
rules=[
|
|
205
|
+
AlertRule(
|
|
206
|
+
alert_rule["metric"],
|
|
207
|
+
alert_rule["threshold"],
|
|
208
|
+
alert_rule["consecutive_checks"],
|
|
209
|
+
)
|
|
210
|
+
],
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
alert.evaluate(
|
|
214
|
+
ip="192.168.1.10",
|
|
215
|
+
metrics_list=[
|
|
216
|
+
MetricRecord(device_ip="192.168.1.10", metric="jitter", value=20.0),
|
|
217
|
+
],
|
|
218
|
+
default_consecutive_checks={
|
|
219
|
+
"jitter": 0,
|
|
220
|
+
},
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
jitter = storage.get_recent_alert_events()[0]
|
|
224
|
+
|
|
225
|
+
assert jitter.event_type == "recover"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
def test_dashboard_endpoint(client):
|
|
2
|
+
response = client.get("/")
|
|
3
|
+
assert response.status_code == 200
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_devices_api_returns_mocked_devices(client):
|
|
7
|
+
response = client.get("/api/devices")
|
|
8
|
+
|
|
9
|
+
assert response.status_code == 200
|
|
10
|
+
assert response.json()["devices"][0]["ip"] == "192.168.1.10"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_device_info_api_returns_mocked_device(client):
|
|
14
|
+
response = client.get("/api/devices/192.168.100.1")
|
|
15
|
+
|
|
16
|
+
assert response.status_code == 200
|
|
17
|
+
assert response.json()["device"]["ip"] == "192.168.100.1"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_device_info_page_returns_mocked_device(client):
|
|
21
|
+
response = client.get("/devices/192.168.100.2")
|
|
22
|
+
|
|
23
|
+
assert response.status_code == 200
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_device_info_page_returns_unknown_device(client):
|
|
27
|
+
response = client.get("/devices/192.168.100.10")
|
|
28
|
+
|
|
29
|
+
assert response.status_code == 404
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_stats_api_returns_mocked_devices(client):
|
|
33
|
+
response = client.get("/api/stats")
|
|
34
|
+
|
|
35
|
+
assert response.status_code == 200
|
|
36
|
+
assert response.json()["health"]["engine_alive"]
|
|
37
|
+
assert response.json()["identity"]["server_ip"] == "192.168.200.100"
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _parse_ping_output(output: str, os_name: str):
|
|
5
|
+
if os_name != "Windows":
|
|
6
|
+
avg_rtt_regex = r"rtt min/avg/max/mdev = [\d\.]+/([\d\.]+)/[\d\.]+/[\d\.]+ ms"
|
|
7
|
+
|
|
8
|
+
packet_loss_regex = r"([\d\.]+)% packet loss"
|
|
9
|
+
|
|
10
|
+
per_ping_time_regex = r"time[=<]([\d\.]+) ?ms"
|
|
11
|
+
else:
|
|
12
|
+
avg_rtt_regex = r"Average = ([\d\.]+)ms"
|
|
13
|
+
|
|
14
|
+
packet_loss_regex = r"\(([^\)]+)% loss\)"
|
|
15
|
+
|
|
16
|
+
per_ping_time_regex = r"time[=<]([\d\.]+)ms"
|
|
17
|
+
|
|
18
|
+
avg_rtt = 0
|
|
19
|
+
packet_loss = 0.0
|
|
20
|
+
per_ping_times = []
|
|
21
|
+
for line in output.splitlines():
|
|
22
|
+
m = re.search(avg_rtt_regex, line)
|
|
23
|
+
if m:
|
|
24
|
+
try:
|
|
25
|
+
avg_rtt = float(m.group(1))
|
|
26
|
+
except ValueError:
|
|
27
|
+
avg_rtt = 0
|
|
28
|
+
continue
|
|
29
|
+
|
|
30
|
+
m = re.search(packet_loss_regex, line)
|
|
31
|
+
if m:
|
|
32
|
+
try:
|
|
33
|
+
packet_loss = float(m.group(1))
|
|
34
|
+
except ValueError:
|
|
35
|
+
packet_loss = 0.0
|
|
36
|
+
|
|
37
|
+
m = re.search(per_ping_time_regex, line)
|
|
38
|
+
if m:
|
|
39
|
+
try:
|
|
40
|
+
per_ping_times.append(float(m.group(1)))
|
|
41
|
+
except ValueError:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
jitter = 0.0
|
|
45
|
+
if len(per_ping_times) >= 2:
|
|
46
|
+
diffs = [
|
|
47
|
+
abs(per_ping_times[i] - per_ping_times[i - 1])
|
|
48
|
+
for i in range(1, len(per_ping_times))
|
|
49
|
+
]
|
|
50
|
+
jitter = round(sum(diffs) / len(diffs), 3)
|
|
51
|
+
|
|
52
|
+
return avg_rtt, packet_loss, jitter
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_successful_parse_output_on_linux():
|
|
56
|
+
sample_output = """
|
|
57
|
+
PING 192.168.1.1 (192.168.1.1) 56(84) bytes of data.
|
|
58
|
+
64 bytes from 192.168.1.1: icmp_seq=1 ttl=64 time=2.67 ms
|
|
59
|
+
64 bytes from 192.168.1.1: icmp_seq=2 ttl=64 time=6.38 ms
|
|
60
|
+
|
|
61
|
+
--- 192.168.1.1 ping statistics ---
|
|
62
|
+
2 packets transmitted, 2 received, 0% packet loss, time 1002ms
|
|
63
|
+
rtt min/avg/max/mdev = 2.672/4.524/6.376/1.852 ms
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
avg_rtt, packet_loss, jitter = _parse_ping_output(sample_output, "Linux")
|
|
67
|
+
|
|
68
|
+
assert avg_rtt == 4.524
|
|
69
|
+
assert packet_loss == 0.0
|
|
70
|
+
assert jitter == 3.71
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_failed_parse_output_on_linux():
|
|
74
|
+
sample_output = """
|
|
75
|
+
PING 192.168.1.2 (192.168.1.2) 56(84) bytes of data.
|
|
76
|
+
|
|
77
|
+
--- 192.168.1.2 ping statistics ---
|
|
78
|
+
2 packets transmitted, 0 received, 100% packet loss, time 1044ms
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
avg_rtt, packet_loss, jitter = _parse_ping_output(sample_output, "Linux")
|
|
82
|
+
|
|
83
|
+
assert avg_rtt == 0.0
|
|
84
|
+
assert packet_loss == 100.0
|
|
85
|
+
assert jitter == 0.0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_successful_parse_output_on_windows():
|
|
89
|
+
sample_output = """
|
|
90
|
+
Pinging 192.168.1.1 with 32 bytes of data:
|
|
91
|
+
Reply from 192.168.1.1: bytes=32 time=6ms TTL=64
|
|
92
|
+
Reply from 192.168.1.1: bytes=32 time=2ms TTL=64
|
|
93
|
+
|
|
94
|
+
Ping statistics for 192.168.1.1:
|
|
95
|
+
Packets: Sent = 2, Received = 2, Lost = 0 (0% loss),
|
|
96
|
+
Approximate round trip times in milli-seconds:
|
|
97
|
+
Minimum = 2ms, Maximum = 6ms, Average = 4ms
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
avg_rtt, packet_loss, jitter = _parse_ping_output(sample_output, "Windows")
|
|
101
|
+
|
|
102
|
+
assert avg_rtt == 4.0
|
|
103
|
+
assert packet_loss == 0.0
|
|
104
|
+
assert jitter == 4.0
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_failed_parse_output_on_windows():
|
|
108
|
+
sample_output = """
|
|
109
|
+
Pinging 192.168.1.2 with 32 bytes of data:
|
|
110
|
+
Request timed out.
|
|
111
|
+
Request timed out.
|
|
112
|
+
|
|
113
|
+
Ping statistics for 192.168.1.2:
|
|
114
|
+
Packets: Sent = 2, Received = 0, Lost = 2 (100% loss),
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
avg_rtt, packet_loss, jitter = _parse_ping_output(sample_output, "Windows")
|
|
118
|
+
|
|
119
|
+
assert avg_rtt == 0.0
|
|
120
|
+
assert packet_loss == 100.0
|
|
121
|
+
assert jitter == 0.0
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# woninet/core/__init__.py
|
|
2
|
-
__version__ = "2.0
|
|
2
|
+
__version__ = "2.1.0"
|
|
@@ -85,24 +85,34 @@ class AlertEngine:
|
|
|
85
85
|
"""
|
|
86
86
|
for metric in metrics_list:
|
|
87
87
|
if metric is not None:
|
|
88
|
-
if metric.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
88
|
+
if metric.value is not None:
|
|
89
|
+
if metric.metric == "latency":
|
|
90
|
+
self._evaluate_latency(
|
|
91
|
+
ip=ip,
|
|
92
|
+
metric="latency",
|
|
93
|
+
value=metric.value,
|
|
94
|
+
default_consecutive_checks=default_consecutive_checks[
|
|
95
|
+
"latency"
|
|
96
|
+
],
|
|
97
|
+
)
|
|
98
|
+
elif metric.metric == "packet_loss":
|
|
99
|
+
self._evaluate_packet_loss(
|
|
100
|
+
ip=ip,
|
|
101
|
+
metric="packet_loss",
|
|
102
|
+
value=metric.value,
|
|
103
|
+
default_consecutive_checks=default_consecutive_checks[
|
|
104
|
+
"packet_loss"
|
|
105
|
+
],
|
|
106
|
+
)
|
|
107
|
+
elif metric.metric == "jitter":
|
|
108
|
+
self._evaluate_jitter(
|
|
109
|
+
ip=ip,
|
|
110
|
+
metric="jitter",
|
|
111
|
+
value=metric.value,
|
|
112
|
+
default_consecutive_checks=default_consecutive_checks[
|
|
113
|
+
"jitter"
|
|
114
|
+
],
|
|
115
|
+
)
|
|
106
116
|
|
|
107
117
|
def _evaluate_latency(
|
|
108
118
|
self, ip: str, metric: str, value: float, default_consecutive_checks: int
|
|
@@ -249,3 +259,78 @@ class AlertEngine:
|
|
|
249
259
|
# Reset consecutive checks to its default value
|
|
250
260
|
state.consecutive_checks = default_consecutive_checks
|
|
251
261
|
self.storage.update_alert_state(state)
|
|
262
|
+
|
|
263
|
+
def _evaluate_jitter(
|
|
264
|
+
self, ip: str, metric: str, value: float, default_consecutive_checks: int
|
|
265
|
+
):
|
|
266
|
+
"""
|
|
267
|
+
Evaluate a jitter metric against the related alert rule.
|
|
268
|
+
|
|
269
|
+
If the metric crosses its threshold for a consecutive number of checks,
|
|
270
|
+
trigger or resolve an alert accordingly. Persists state transitions
|
|
271
|
+
and logs any alert changes.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
ip (str): IP address to evaluate.
|
|
275
|
+
metric (str): Metric name (e.g. `jitter`).
|
|
276
|
+
value (float): Value of the current metric.
|
|
277
|
+
default_consecutive_checks (int): Number of consecutive evaluations
|
|
278
|
+
required to confirm a state transition.
|
|
279
|
+
|
|
280
|
+
Side Effects:
|
|
281
|
+
- Persists alert state in the database via `StorageEngine`.
|
|
282
|
+
- Emits log events.
|
|
283
|
+
- Creates `AlertEventTable` records on trigger and recover.
|
|
284
|
+
"""
|
|
285
|
+
state = self.storage.get_or_create_alert_state(
|
|
286
|
+
ip=ip, metric=metric, consecutive_checks=default_consecutive_checks
|
|
287
|
+
)
|
|
288
|
+
violated = self.is_metric_violated(metric=metric, value=value)
|
|
289
|
+
if violated:
|
|
290
|
+
if state.state == "ok":
|
|
291
|
+
# Confirm remaining consecutive checks before a state transition
|
|
292
|
+
if state.consecutive_checks > 0:
|
|
293
|
+
state.consecutive_checks = state.consecutive_checks - 1
|
|
294
|
+
self.storage.update_alert_state(state)
|
|
295
|
+
return
|
|
296
|
+
# Update "ok" to "warning"
|
|
297
|
+
state.state = "warning"
|
|
298
|
+
state.triggered_at = datetime.now()
|
|
299
|
+
state.consecutive_checks = default_consecutive_checks
|
|
300
|
+
self.storage.update_alert_state(state)
|
|
301
|
+
|
|
302
|
+
event = AlertEventTable(
|
|
303
|
+
device_ip=ip, metric=metric, value=value, event_type="trigger"
|
|
304
|
+
)
|
|
305
|
+
self.storage.store_alert_event(event=event)
|
|
306
|
+
core_logger.warning(
|
|
307
|
+
f"ALERT TRIGGERED | {ip}, metric={metric}, value={value} ms"
|
|
308
|
+
)
|
|
309
|
+
else:
|
|
310
|
+
# Reset consecutive checks to its default value
|
|
311
|
+
state.consecutive_checks = default_consecutive_checks
|
|
312
|
+
self.storage.update_alert_state(state)
|
|
313
|
+
else:
|
|
314
|
+
if state.state == "warning":
|
|
315
|
+
# Confirm remaining consecutive checks before a state transition
|
|
316
|
+
if state.consecutive_checks > 0:
|
|
317
|
+
state.consecutive_checks = state.consecutive_checks - 1
|
|
318
|
+
self.storage.update_alert_state(state)
|
|
319
|
+
return
|
|
320
|
+
# Update "warning" to "ok"
|
|
321
|
+
state.state = "ok"
|
|
322
|
+
state.triggered_at = datetime.now()
|
|
323
|
+
state.consecutive_checks = default_consecutive_checks
|
|
324
|
+
self.storage.update_alert_state(state)
|
|
325
|
+
|
|
326
|
+
event = AlertEventTable(
|
|
327
|
+
device_ip=ip, metric=metric, value=value, event_type="recover"
|
|
328
|
+
)
|
|
329
|
+
self.storage.store_alert_event(event=event)
|
|
330
|
+
core_logger.warning(
|
|
331
|
+
f"ALERT RECOVERED | {ip}, metric={metric}, value={value} ms"
|
|
332
|
+
)
|
|
333
|
+
else:
|
|
334
|
+
# Reset consecutive checks to its default value
|
|
335
|
+
state.consecutive_checks = default_consecutive_checks
|
|
336
|
+
self.storage.update_alert_state(state)
|