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/logging.yaml
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
disable_existing_loggers: False
|
|
3
|
+
|
|
4
|
+
formatters:
|
|
5
|
+
default:
|
|
6
|
+
format: "%(asctime)s [%(levelname)s] %(message)s"
|
|
7
|
+
datefmt: "%Y-%m-%d %H:%M:%S"
|
|
8
|
+
|
|
9
|
+
colored:
|
|
10
|
+
(): woninet.utilities.logger.ColorFormatter
|
|
11
|
+
format: "%(asctime)s [%(levelname)s] %(message)s"
|
|
12
|
+
datefmt: "%Y-%m-%d %H:%M:%S"
|
|
13
|
+
|
|
14
|
+
handlers:
|
|
15
|
+
file:
|
|
16
|
+
class: logging.FileHandler
|
|
17
|
+
filename: PLACEHOLDER.txt
|
|
18
|
+
formatter: default
|
|
19
|
+
level: INFO
|
|
20
|
+
|
|
21
|
+
console:
|
|
22
|
+
class: logging.StreamHandler
|
|
23
|
+
formatter: colored
|
|
24
|
+
level: INFO
|
|
25
|
+
|
|
26
|
+
loggers:
|
|
27
|
+
uvicorn:
|
|
28
|
+
handlers: [file, console]
|
|
29
|
+
level: INFO
|
|
30
|
+
propagate: False
|
|
31
|
+
|
|
32
|
+
uvicorn.error:
|
|
33
|
+
handlers: [file, console]
|
|
34
|
+
level: INFO
|
|
35
|
+
propagate: False
|
|
36
|
+
|
|
37
|
+
uvicorn.access:
|
|
38
|
+
handlers: [file, console]
|
|
39
|
+
level: INFO
|
|
40
|
+
propagate: False
|
|
41
|
+
|
|
42
|
+
core:
|
|
43
|
+
handlers: [file, console]
|
|
44
|
+
level: ERROR
|
|
45
|
+
propagate: False
|
|
46
|
+
|
|
47
|
+
root:
|
|
48
|
+
handlers: [console]
|
|
49
|
+
level: INFO
|
woninet/main.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
import logging
|
|
3
|
+
import uvicorn
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from yaml import safe_load
|
|
7
|
+
from argparse import Namespace
|
|
8
|
+
from sqlalchemy.exc import OperationalError
|
|
9
|
+
from woninet.core.engine import NetworkMonitorCore
|
|
10
|
+
from woninet.utilities.arguments import args
|
|
11
|
+
from woninet.utilities.logger import get_core_logger, TRACE_LEVEL
|
|
12
|
+
|
|
13
|
+
# Global Variables
|
|
14
|
+
LOGGING_YAML_PATH = Path(__file__).parent / "logging.yaml"
|
|
15
|
+
CONFIG_JSON_PATH = Path(__file__).parent / "config.json"
|
|
16
|
+
REMOTE_PROBE_IP = "8.8.8.8"
|
|
17
|
+
monitor = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def detect_local_ip(logger: logging.Logger) -> str:
|
|
21
|
+
"""
|
|
22
|
+
Determine the local IP address by opening a dummy UDP connection
|
|
23
|
+
to `8.8.8.8:80`.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
logger (Logger): Logger used for recording logs.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
str: IP address of device.
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
|
33
|
+
logger.trace(f"Sending a dummy UDP request to {REMOTE_PROBE_IP}:80")
|
|
34
|
+
s.connect((REMOTE_PROBE_IP, 80))
|
|
35
|
+
return s.getsockname()[0]
|
|
36
|
+
except OSError:
|
|
37
|
+
logger.error("Network is unreachable. Quitting.")
|
|
38
|
+
raise SystemExit(1)
|
|
39
|
+
except Exception as e:
|
|
40
|
+
logger.error(f"Local IP detection failed: {e}")
|
|
41
|
+
raise SystemExit(1)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_logging_yaml(log_output: str | None) -> dict:
|
|
45
|
+
"""
|
|
46
|
+
Load logging YAML configuration and adjust file handler if needed.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
log_output (str): The file path to save logs.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
dict: Logging configuration for uvicorn.
|
|
53
|
+
"""
|
|
54
|
+
with open(LOGGING_YAML_PATH) as file:
|
|
55
|
+
config = safe_load(file)
|
|
56
|
+
|
|
57
|
+
if log_output:
|
|
58
|
+
config["handlers"]["file"]["filename"] = log_output
|
|
59
|
+
else:
|
|
60
|
+
config["handlers"].pop("file", None)
|
|
61
|
+
|
|
62
|
+
for logger in config.get("loggers", {}).values():
|
|
63
|
+
handlers = logger.get("handlers", [])
|
|
64
|
+
if "file" in handlers:
|
|
65
|
+
handlers.remove("file")
|
|
66
|
+
|
|
67
|
+
return config
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def load_config_json() -> dict:
|
|
71
|
+
try:
|
|
72
|
+
with open(CONFIG_JSON_PATH) as file:
|
|
73
|
+
config = json.load(file)
|
|
74
|
+
return config
|
|
75
|
+
except FileNotFoundError:
|
|
76
|
+
raise
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def configure_logger(arguments: Namespace) -> logging.Logger:
|
|
80
|
+
"""
|
|
81
|
+
Initialize the core logger and adjust verbosity.
|
|
82
|
+
"""
|
|
83
|
+
log_output = arguments.logs
|
|
84
|
+
use_color = arguments.color != "false"
|
|
85
|
+
|
|
86
|
+
core_logger = get_core_logger(log_output, use_color)
|
|
87
|
+
|
|
88
|
+
if arguments.verbose == 1:
|
|
89
|
+
core_logger.setLevel(logging.DEBUG)
|
|
90
|
+
elif arguments.verbose >= 2:
|
|
91
|
+
core_logger.setLevel(TRACE_LEVEL)
|
|
92
|
+
|
|
93
|
+
if not arguments.cli:
|
|
94
|
+
core_logger.setLevel(logging.ERROR)
|
|
95
|
+
|
|
96
|
+
return core_logger
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def create_monitor(
|
|
100
|
+
local_ip: str,
|
|
101
|
+
target_ip: str,
|
|
102
|
+
database_path: str,
|
|
103
|
+
arp_noise_limit: float,
|
|
104
|
+
max_thread_workers: int,
|
|
105
|
+
logger: logging.Logger,
|
|
106
|
+
alert_rules: dict,
|
|
107
|
+
) -> NetworkMonitorCore:
|
|
108
|
+
"""
|
|
109
|
+
Create an instance of NetworkMonitoreCore.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
local_ip (str): IP address of device.
|
|
113
|
+
target_ip (str): A single or range of user-provided IP addresses to scan.
|
|
114
|
+
database_path (str): Path to SQLite database.
|
|
115
|
+
arp_noise_limit (float): Threshold above which ARP fluctuations are treated as noise.
|
|
116
|
+
max_thread_workers (int): The concurrency limit for the
|
|
117
|
+
`ThreadPoolExecutor` handling ICMP probes
|
|
118
|
+
logger (Logger): Logger used for recording logs
|
|
119
|
+
alert_rules (dict): alert_rules loaded from config.json.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
NetworkMonitorCore: Instance of NetworkMonitorCore.
|
|
123
|
+
"""
|
|
124
|
+
global monitor
|
|
125
|
+
if monitor is None:
|
|
126
|
+
logger.debug(f"ARP noise limit set to {arp_noise_limit}")
|
|
127
|
+
logger.trace(f"Using {database_path} as SQLite database.")
|
|
128
|
+
logger.trace(f"Max thread workers for scanning set to {max_thread_workers}.")
|
|
129
|
+
monitor = NetworkMonitorCore(
|
|
130
|
+
local_ip=local_ip,
|
|
131
|
+
target_ip=target_ip,
|
|
132
|
+
arp_noise_limit=arp_noise_limit,
|
|
133
|
+
database_path=database_path,
|
|
134
|
+
max_thread_workers=max_thread_workers,
|
|
135
|
+
alert_rules=alert_rules,
|
|
136
|
+
)
|
|
137
|
+
return monitor
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def get_monitor() -> NetworkMonitorCore:
|
|
141
|
+
"""
|
|
142
|
+
Returns the instance of NetworkMonitorCore for easier access in both CLI mode
|
|
143
|
+
and web dashboard.
|
|
144
|
+
"""
|
|
145
|
+
global monitor
|
|
146
|
+
return monitor
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def run_server(ip: str, port: int, log_yaml: dict) -> None:
|
|
150
|
+
"""
|
|
151
|
+
Initialize and run the uvicorn ASGI server.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
ip (str): Host used for running the server.
|
|
155
|
+
port (int): Port number to serve the host.
|
|
156
|
+
log_yaml (dict): Logging configuration for uvicorn.
|
|
157
|
+
"""
|
|
158
|
+
from woninet.server.app import app
|
|
159
|
+
|
|
160
|
+
config = uvicorn.Config(app, host=ip, port=port, log_config=log_yaml)
|
|
161
|
+
server = uvicorn.Server(config)
|
|
162
|
+
app.state.uvicorn_server = server
|
|
163
|
+
server.run()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def main() -> None:
|
|
167
|
+
"""
|
|
168
|
+
Entry point for woninet. Launch either the web dashboard or the CLI
|
|
169
|
+
monitor based on command-line arguments.
|
|
170
|
+
"""
|
|
171
|
+
global monitor
|
|
172
|
+
|
|
173
|
+
arguments = args()
|
|
174
|
+
|
|
175
|
+
if arguments.version:
|
|
176
|
+
from woninet.__init__ import __version__
|
|
177
|
+
|
|
178
|
+
print(__version__)
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
configuration = load_config_json()
|
|
182
|
+
|
|
183
|
+
target_ip = configuration["target_ip_list"]
|
|
184
|
+
port = arguments.port
|
|
185
|
+
|
|
186
|
+
core_logger = configure_logger(arguments=arguments)
|
|
187
|
+
log_yaml = load_logging_yaml(log_output=arguments.logs)
|
|
188
|
+
|
|
189
|
+
local_ip = detect_local_ip(logger=core_logger)
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
if hasattr(arguments, "func"):
|
|
193
|
+
core_logger.setLevel(logging.ERROR)
|
|
194
|
+
|
|
195
|
+
monitor = create_monitor(
|
|
196
|
+
local_ip=local_ip,
|
|
197
|
+
target_ip=target_ip,
|
|
198
|
+
database_path=configuration["database"],
|
|
199
|
+
arp_noise_limit=configuration["monitoring"]["arp_noise_limit"],
|
|
200
|
+
max_thread_workers=configuration["monitoring"]["max_workers"],
|
|
201
|
+
logger=core_logger,
|
|
202
|
+
alert_rules=configuration["alert_rules"],
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if hasattr(arguments, "func"):
|
|
206
|
+
arguments.monitor = monitor
|
|
207
|
+
arguments.func(arguments)
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
if arguments.cli:
|
|
211
|
+
monitor.start()
|
|
212
|
+
monitor.wait()
|
|
213
|
+
else:
|
|
214
|
+
run_server(ip=local_ip, port=port, log_yaml=log_yaml)
|
|
215
|
+
except KeyboardInterrupt:
|
|
216
|
+
core_logger.info("Keyboard interrupted. Wait for shut down...")
|
|
217
|
+
except ValueError as e:
|
|
218
|
+
core_logger.error(f"{e} Quitting.")
|
|
219
|
+
except FileNotFoundError:
|
|
220
|
+
core_logger.error("Can not find the config.json file. Quitting.")
|
|
221
|
+
except OperationalError as e:
|
|
222
|
+
core_logger.error(e)
|
|
223
|
+
finally:
|
|
224
|
+
if hasattr(monitor, "stop"):
|
|
225
|
+
monitor.stop()
|
woninet/server/app.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from fastapi import FastAPI, Request, HTTPException, status
|
|
4
|
+
from fastapi.responses import HTMLResponse
|
|
5
|
+
from fastapi.staticfiles import StaticFiles
|
|
6
|
+
from fastapi.templating import Jinja2Templates
|
|
7
|
+
from fastapi.openapi.docs import get_swagger_ui_html
|
|
8
|
+
from contextlib import asynccontextmanager
|
|
9
|
+
from woninet.server.routes.api.devices import router as devices_api
|
|
10
|
+
from woninet.server.routes.api.stats import router as stats_api
|
|
11
|
+
from woninet.server.routes.page.devices import router as devices_page
|
|
12
|
+
from woninet.server.dependencies import get_static_path
|
|
13
|
+
from woninet.main import get_monitor
|
|
14
|
+
from woninet.__init__ import __version__
|
|
15
|
+
|
|
16
|
+
# Global variables
|
|
17
|
+
TEMPLATES_DIR, STATIC_DIR = get_static_path()
|
|
18
|
+
CHARTS_DIR = Path(__file__).parent.parent / "charts"
|
|
19
|
+
|
|
20
|
+
monitor = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@asynccontextmanager
|
|
24
|
+
async def lifespan(app: FastAPI):
|
|
25
|
+
global monitor
|
|
26
|
+
|
|
27
|
+
# Startup
|
|
28
|
+
monitor = get_monitor()
|
|
29
|
+
monitor.start()
|
|
30
|
+
|
|
31
|
+
# Assign the instance of Server
|
|
32
|
+
server = app.state.uvicorn_server
|
|
33
|
+
|
|
34
|
+
async def watch():
|
|
35
|
+
"""
|
|
36
|
+
Periodic monitor watchdog. Triggers server shutdown when the monitor
|
|
37
|
+
is no longer alive.
|
|
38
|
+
"""
|
|
39
|
+
while True:
|
|
40
|
+
await asyncio.sleep(1)
|
|
41
|
+
if not monitor.is_alive():
|
|
42
|
+
server.should_exit = True
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
task = asyncio.create_task(watch())
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
yield
|
|
49
|
+
finally:
|
|
50
|
+
# Shutdown
|
|
51
|
+
task.cancel()
|
|
52
|
+
monitor.stop()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
app = FastAPI(
|
|
56
|
+
title="woninet",
|
|
57
|
+
description="Network Monitoring Dashboard",
|
|
58
|
+
lifespan=lifespan,
|
|
59
|
+
version=__version__,
|
|
60
|
+
docs_url=None,
|
|
61
|
+
redoc_url=None,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Templates
|
|
65
|
+
templates = Jinja2Templates(directory=TEMPLATES_DIR)
|
|
66
|
+
|
|
67
|
+
# Static files
|
|
68
|
+
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
|
69
|
+
app.mount("/charts", StaticFiles(directory=CHARTS_DIR), name="charts")
|
|
70
|
+
|
|
71
|
+
# Routes
|
|
72
|
+
app.include_router(devices_api)
|
|
73
|
+
app.include_router(stats_api)
|
|
74
|
+
app.include_router(devices_page)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# The root path
|
|
78
|
+
@app.get("/", response_class=HTMLResponse)
|
|
79
|
+
def root(request: Request):
|
|
80
|
+
return templates.TemplateResponse(request=request, name="dashboard.html")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@app.get("/docs", include_in_schema=False)
|
|
84
|
+
def custom_swagger_ui():
|
|
85
|
+
return get_swagger_ui_html(
|
|
86
|
+
openapi_url=app.openapi_url,
|
|
87
|
+
title="woninet - Swagger UI",
|
|
88
|
+
swagger_js_url="/static/swagger-ui/swagger-ui-bundle.js",
|
|
89
|
+
swagger_css_url="/static/swagger-ui/swagger-ui.css",
|
|
90
|
+
swagger_favicon_url="/static/swagger-ui/favicon-32x32.png",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@app.exception_handler(404)
|
|
95
|
+
def custom_not_found_exception(request: Request, exc: HTTPException):
|
|
96
|
+
return templates.TemplateResponse(
|
|
97
|
+
request=request,
|
|
98
|
+
name="404.html",
|
|
99
|
+
context={"request": request},
|
|
100
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
101
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_static_path() -> tuple[Path, Path]:
|
|
5
|
+
"""
|
|
6
|
+
Return the paths to the templates and static directories.
|
|
7
|
+
|
|
8
|
+
Returns:
|
|
9
|
+
tuple[Path, Path]: A tuple containing (TEMPLATES_DIR, STATIC_DIR)
|
|
10
|
+
"""
|
|
11
|
+
STATIC_DIR = Path(__file__).parent / "static"
|
|
12
|
+
TEMPLATES_DIR = Path(__file__).parent / "templates"
|
|
13
|
+
|
|
14
|
+
return TEMPLATES_DIR, STATIC_DIR
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_monitor_gracefully():
|
|
18
|
+
"""
|
|
19
|
+
Call get_monitor() and return the intance of NetworkMonitorCore class
|
|
20
|
+
to prevent circular import error.
|
|
21
|
+
"""
|
|
22
|
+
from woninet.main import get_monitor
|
|
23
|
+
|
|
24
|
+
monitor = get_monitor()
|
|
25
|
+
return monitor
|
|
File without changes
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from fastapi import APIRouter, Request, Path, status
|
|
2
|
+
from fastapi.responses import JSONResponse
|
|
3
|
+
from fastapi.encoders import jsonable_encoder
|
|
4
|
+
from woninet.server.dependencies import get_monitor_gracefully
|
|
5
|
+
|
|
6
|
+
router = APIRouter(prefix="/api/devices", tags=["devices"])
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@router.get("")
|
|
10
|
+
def list_devices(request: Request):
|
|
11
|
+
monitor = get_monitor_gracefully()
|
|
12
|
+
return JSONResponse(
|
|
13
|
+
content={"devices": jsonable_encoder(monitor.get_device_history())},
|
|
14
|
+
status_code=status.HTTP_200_OK,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@router.get("/{ip}")
|
|
19
|
+
def device_info_api(
|
|
20
|
+
request: Request,
|
|
21
|
+
ip: str = Path(
|
|
22
|
+
...,
|
|
23
|
+
title="IP address",
|
|
24
|
+
description="IP address of the device.",
|
|
25
|
+
min_length=7,
|
|
26
|
+
max_length=15,
|
|
27
|
+
),
|
|
28
|
+
):
|
|
29
|
+
monitor = get_monitor_gracefully()
|
|
30
|
+
device, device_alert_state, device_recent_alert_events = monitor.get_device_info(
|
|
31
|
+
ip=ip
|
|
32
|
+
)
|
|
33
|
+
monitor.graph_engine.design_device_latency_events(
|
|
34
|
+
ip=ip, recent_device_alert_events=device_recent_alert_events
|
|
35
|
+
)
|
|
36
|
+
return JSONResponse(
|
|
37
|
+
content={
|
|
38
|
+
"device": jsonable_encoder(device),
|
|
39
|
+
"device_alert_state": jsonable_encoder(device_alert_state),
|
|
40
|
+
"device_recent_alert_events": jsonable_encoder(device_recent_alert_events),
|
|
41
|
+
},
|
|
42
|
+
status_code=status.HTTP_200_OK,
|
|
43
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from fastapi import APIRouter, status
|
|
2
|
+
from fastapi.responses import JSONResponse
|
|
3
|
+
from woninet.server.dependencies import get_monitor_gracefully
|
|
4
|
+
|
|
5
|
+
router = APIRouter(prefix="/api/stats", tags=["stats"])
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@router.get("")
|
|
9
|
+
def stats():
|
|
10
|
+
from woninet.__init__ import __version__
|
|
11
|
+
|
|
12
|
+
monitor = get_monitor_gracefully()
|
|
13
|
+
devices_total, metrics_total = monitor.count_resources()
|
|
14
|
+
|
|
15
|
+
return JSONResponse(
|
|
16
|
+
content={
|
|
17
|
+
"identity": {
|
|
18
|
+
"service": "woninet",
|
|
19
|
+
"version": __version__,
|
|
20
|
+
"server_ip": monitor.local_ip,
|
|
21
|
+
},
|
|
22
|
+
"health": {
|
|
23
|
+
"engine_alive": monitor.is_alive(),
|
|
24
|
+
"database": monitor.database_health(),
|
|
25
|
+
},
|
|
26
|
+
"stats": {
|
|
27
|
+
"devices_total": devices_total,
|
|
28
|
+
"metrics_total": metrics_total,
|
|
29
|
+
"uptime_seconds": monitor.uptime(),
|
|
30
|
+
},
|
|
31
|
+
"recent_alert_events": monitor.classify_recent_alert_events(),
|
|
32
|
+
},
|
|
33
|
+
status_code=status.HTTP_200_OK,
|
|
34
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from fastapi import APIRouter, Request, Path, HTTPException, status
|
|
2
|
+
from fastapi.responses import HTMLResponse
|
|
3
|
+
from fastapi.templating import Jinja2Templates
|
|
4
|
+
from woninet.server.dependencies import get_static_path, get_monitor_gracefully
|
|
5
|
+
|
|
6
|
+
TEMPLATES_DIR, STATIC_DIR = get_static_path()
|
|
7
|
+
templates = Jinja2Templates(directory=TEMPLATES_DIR)
|
|
8
|
+
|
|
9
|
+
router = APIRouter(prefix="/devices", tags=["devices"])
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@router.get("/{ip}", response_class=HTMLResponse)
|
|
13
|
+
def device_info(
|
|
14
|
+
request: Request,
|
|
15
|
+
ip: str = Path(
|
|
16
|
+
...,
|
|
17
|
+
title="IP address",
|
|
18
|
+
description="IP address of the device.",
|
|
19
|
+
min_length=7,
|
|
20
|
+
max_length=15,
|
|
21
|
+
),
|
|
22
|
+
):
|
|
23
|
+
monitor = get_monitor_gracefully()
|
|
24
|
+
|
|
25
|
+
if not monitor.device_exists(ip=ip):
|
|
26
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not Found")
|
|
27
|
+
|
|
28
|
+
return templates.TemplateResponse(request=request, name="device_info.html")
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
:root{
|
|
2
|
+
--bg:#08111f;
|
|
3
|
+
--panel:#101b31;
|
|
4
|
+
--panel-light:#172541;
|
|
5
|
+
|
|
6
|
+
--text:#edf3ff;
|
|
7
|
+
--muted:#8ea1c3;
|
|
8
|
+
|
|
9
|
+
--accent:#4f8cff;
|
|
10
|
+
|
|
11
|
+
--good:#22c55e;
|
|
12
|
+
--warn:#f59e0b;
|
|
13
|
+
--bad:#ef4444;
|
|
14
|
+
|
|
15
|
+
--border:rgba(255,255,255,.08);
|
|
16
|
+
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
*{
|
|
20
|
+
margin:0;
|
|
21
|
+
padding:0;
|
|
22
|
+
box-sizing:border-box;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
body{
|
|
26
|
+
font-family:Inter,sans-serif;
|
|
27
|
+
color:var(--text);
|
|
28
|
+
min-height:100vh;
|
|
29
|
+
display:flex;
|
|
30
|
+
justify-content:center;
|
|
31
|
+
align-items:center;
|
|
32
|
+
background:
|
|
33
|
+
radial-gradient(circle at top left,
|
|
34
|
+
rgba(79,140,255,.15),
|
|
35
|
+
transparent 25%),
|
|
36
|
+
|
|
37
|
+
radial-gradient(circle at bottom right,
|
|
38
|
+
rgba(34,197,94,.08),
|
|
39
|
+
transparent 30%),
|
|
40
|
+
|
|
41
|
+
linear-gradient(180deg,#08111f,#050b15);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.card{
|
|
45
|
+
width:min(700px,90vw);
|
|
46
|
+
background:var(--panel);
|
|
47
|
+
border:1px solid var(--border);
|
|
48
|
+
border-radius:24px;
|
|
49
|
+
padding:50px;
|
|
50
|
+
text-align:center;
|
|
51
|
+
box-shadow:
|
|
52
|
+
0 20px 60px rgba(0,0,0,.35);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.network-icon{
|
|
56
|
+
width:120px;
|
|
57
|
+
height:120px;
|
|
58
|
+
margin:auto;
|
|
59
|
+
border-radius:50%;
|
|
60
|
+
background:rgba(79,140,255,.12);
|
|
61
|
+
display:flex;
|
|
62
|
+
align-items:center;
|
|
63
|
+
justify-content:center;
|
|
64
|
+
font-size:52px;
|
|
65
|
+
color:var(--accent);
|
|
66
|
+
margin-bottom:30px;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
h1{
|
|
70
|
+
font-size:100px;
|
|
71
|
+
line-height:1;
|
|
72
|
+
margin-bottom:10px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
h2{
|
|
76
|
+
font-size:32px;
|
|
77
|
+
margin-bottom:15px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
p{
|
|
81
|
+
color:var(--muted);
|
|
82
|
+
line-height:1.7;
|
|
83
|
+
max-width:520px;
|
|
84
|
+
margin:auto;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.divider{
|
|
88
|
+
width:100%;
|
|
89
|
+
height:1px;
|
|
90
|
+
background:var(--border);
|
|
91
|
+
margin:40px 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.actions{
|
|
95
|
+
display:flex;
|
|
96
|
+
justify-content:center;
|
|
97
|
+
gap:15px;
|
|
98
|
+
flex-wrap:wrap;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.btn{
|
|
102
|
+
display:inline-flex;
|
|
103
|
+
align-items:center;
|
|
104
|
+
gap:10px;
|
|
105
|
+
text-decoration:none;
|
|
106
|
+
padding:14px 22px;
|
|
107
|
+
border-radius:12px;
|
|
108
|
+
transition:.25s;
|
|
109
|
+
font-weight:600;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.btn-primary{
|
|
113
|
+
background:var(--accent);
|
|
114
|
+
color:white;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.btn-primary:hover{
|
|
118
|
+
transform:translateY(-2px);
|
|
119
|
+
box-shadow:0 8px 20px rgba(79,140,255,.35);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.btn-secondary{
|
|
123
|
+
background:var(--panel-light);
|
|
124
|
+
color:var(--text);
|
|
125
|
+
border:1px solid var(--border);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.btn-secondary:hover{
|
|
129
|
+
background:#213250;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.footer{
|
|
133
|
+
margin-top:35px;
|
|
134
|
+
color:var(--muted);
|
|
135
|
+
font-size:.85rem;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.status{
|
|
139
|
+
display:inline-flex;
|
|
140
|
+
align-items:center;
|
|
141
|
+
gap:8px;
|
|
142
|
+
color:var(--warn);
|
|
143
|
+
margin-top:20px;
|
|
144
|
+
font-size:.9rem;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.status-dot{
|
|
148
|
+
width:10px;
|
|
149
|
+
height:10px;
|
|
150
|
+
border-radius:50%;
|
|
151
|
+
background:var(--warn);
|
|
152
|
+
animation:pulse 1.8s infinite;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@keyframes pulse{
|
|
156
|
+
0%{
|
|
157
|
+
transform:scale(1);
|
|
158
|
+
opacity:1;
|
|
159
|
+
}
|
|
160
|
+
100%{
|
|
161
|
+
transform:scale(2);
|
|
162
|
+
opacity:0;
|
|
163
|
+
}
|
|
164
|
+
}
|