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/storage.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
from woninet.core.models import Device, MetricRecord
|
|
2
|
+
from woninet.database.tables import AlertStateTable, AlertEventTable
|
|
3
|
+
from woninet.database.repositories.device_repository import DeviceRepository
|
|
4
|
+
from woninet.database.repositories.metric_repository import MetricRepository
|
|
5
|
+
from woninet.database.repositories.alert_state_repository import AlertStateRepository
|
|
6
|
+
from woninet.database.repositories.alert_event_repository import AlertEventRepository
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class StorageEngine:
|
|
10
|
+
"""
|
|
11
|
+
Provide a high-level API for storing, updating, and
|
|
12
|
+
retrieving data from the database.
|
|
13
|
+
|
|
14
|
+
Also manage database sessions and delegate persistence
|
|
15
|
+
operations to repository classes.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
session_factory (SessionLocal): Factory responsible for
|
|
19
|
+
creating SQLAlchemy sessions.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, session_factory) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Initialize the storage engine.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
session_factory (SessionLocal): Factory responsible for
|
|
28
|
+
creating SQLAlchemy sessions.
|
|
29
|
+
"""
|
|
30
|
+
self.session_factory = session_factory
|
|
31
|
+
|
|
32
|
+
def store_device(self, device: Device) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Insert or update a collected device.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
device (Device): Device instance to be persisted.
|
|
38
|
+
"""
|
|
39
|
+
with self.session_factory() as session:
|
|
40
|
+
repo = DeviceRepository(session)
|
|
41
|
+
repo.upsert(device)
|
|
42
|
+
session.commit()
|
|
43
|
+
|
|
44
|
+
def list_device_history(self) -> list[Device]:
|
|
45
|
+
"""
|
|
46
|
+
Return all stored devices in the database.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
list[Device]: list devices retrieved from the database.
|
|
50
|
+
"""
|
|
51
|
+
with self.session_factory() as session:
|
|
52
|
+
repo = DeviceRepository(session)
|
|
53
|
+
return repo.fetch_devices()
|
|
54
|
+
|
|
55
|
+
def fetch_device_info(self, ip) -> Device | None:
|
|
56
|
+
"""
|
|
57
|
+
Return a device from the database for the given IP address or
|
|
58
|
+
`None` if not found.
|
|
59
|
+
"""
|
|
60
|
+
with self.session_factory() as session:
|
|
61
|
+
repo = DeviceRepository(session)
|
|
62
|
+
return repo.fetch_device(ip)
|
|
63
|
+
|
|
64
|
+
def count_devices_and_metrics(self) -> tuple[int, int]:
|
|
65
|
+
"""
|
|
66
|
+
Return number of devices and metrics in the database.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
tuple[int,int]: Number of devices and metrics in the database.
|
|
70
|
+
"""
|
|
71
|
+
with self.session_factory() as session:
|
|
72
|
+
device_repo = DeviceRepository(session)
|
|
73
|
+
metric_repo = MetricRepository(session)
|
|
74
|
+
return (
|
|
75
|
+
device_repo.count_devices(),
|
|
76
|
+
metric_repo.count_metrics(),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def store_metric(self, metric: MetricRecord) -> None:
|
|
80
|
+
"""
|
|
81
|
+
Persist newly collected metric record.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
metric (MetricRecord): A metric record captured during a scan cycle.
|
|
85
|
+
"""
|
|
86
|
+
with self.session_factory() as session:
|
|
87
|
+
repo = MetricRepository(session)
|
|
88
|
+
repo.insert(metric)
|
|
89
|
+
session.commit()
|
|
90
|
+
|
|
91
|
+
def list_metric_history(self) -> list[MetricRecord]:
|
|
92
|
+
"""
|
|
93
|
+
Return stored metric history.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
list[MetricRecord]: Metric records retrieved from the database.
|
|
97
|
+
"""
|
|
98
|
+
with self.session_factory() as session:
|
|
99
|
+
repo = MetricRepository(session)
|
|
100
|
+
return repo.fetch_metrics()
|
|
101
|
+
|
|
102
|
+
def get_or_create_alert_state(
|
|
103
|
+
self, ip: str, metric: str, consecutive_checks: int
|
|
104
|
+
) -> AlertStateTable:
|
|
105
|
+
"""
|
|
106
|
+
Return the alert state for a given IP address and metric.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
AlertStateTable: The found alert state.
|
|
110
|
+
"""
|
|
111
|
+
with self.session_factory() as session:
|
|
112
|
+
repo = AlertStateRepository(session=session)
|
|
113
|
+
return repo.fetch_or_create_alert_state(
|
|
114
|
+
ip=ip, metric=metric, consecutive_checks=consecutive_checks
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def update_alert_state(self, state: AlertStateTable) -> None:
|
|
118
|
+
"""
|
|
119
|
+
Update an alert state.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
state (AlertStateTable): The updated instance of AlertStateTable class.
|
|
123
|
+
"""
|
|
124
|
+
with self.session_factory() as session:
|
|
125
|
+
repo = AlertStateRepository(session=session)
|
|
126
|
+
repo.update(state=state)
|
|
127
|
+
session.commit()
|
|
128
|
+
|
|
129
|
+
def get_device_alert_state(self, ip: str) -> dict[str, str] | None:
|
|
130
|
+
"""
|
|
131
|
+
Return alert state of a device for a given IP address.
|
|
132
|
+
"""
|
|
133
|
+
with self.session_factory() as session:
|
|
134
|
+
repo = AlertStateRepository(session)
|
|
135
|
+
return repo.fetch_alert_state(ip=ip)
|
|
136
|
+
|
|
137
|
+
def store_alert_event(self, event: AlertEventTable) -> None:
|
|
138
|
+
"""
|
|
139
|
+
Store the new alert event.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
event (AlertEvent): Instance of the AlertEventTable class.
|
|
143
|
+
"""
|
|
144
|
+
with self.session_factory() as session:
|
|
145
|
+
repo = AlertEventRepository(session=session)
|
|
146
|
+
repo.insert(event=event)
|
|
147
|
+
session.commit()
|
|
148
|
+
|
|
149
|
+
def get_recent_alert_events(self) -> list[AlertEventTable]:
|
|
150
|
+
"""
|
|
151
|
+
Return recent alert events of all devices.
|
|
152
|
+
"""
|
|
153
|
+
with self.session_factory() as session:
|
|
154
|
+
repo = AlertEventRepository(session)
|
|
155
|
+
return repo.fetch_recent_alert_events()
|
|
156
|
+
|
|
157
|
+
def get_recent_device_alert_events(self, ip: str) -> list[AlertEventTable]:
|
|
158
|
+
"""
|
|
159
|
+
Return recent alert events of a device for a given IP address.
|
|
160
|
+
"""
|
|
161
|
+
with self.session_factory() as session:
|
|
162
|
+
repo = AlertEventRepository(session=session)
|
|
163
|
+
return repo.fetch_recent_device_alert_events(ip=ip)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from woninet.core.models import Device
|
|
3
|
+
|
|
4
|
+
core_logger = logging.getLogger("core")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SubnetEnumerator:
|
|
8
|
+
"""
|
|
9
|
+
Build a dictionary of candidate devices for a given IPv4 /24 subnet.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def enumerate(self, ip: str) -> dict[str, Device]:
|
|
13
|
+
"""
|
|
14
|
+
Build a dictionary of candidate devices based on the /24 subnet
|
|
15
|
+
of the provided IPv4 address.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
ip (str): IPv4 address used to derive the subnet.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
dict[str,Device]: Dictionary of candidate devices containing host
|
|
22
|
+
IP addresses and the related `Device` instances. Hosts range
|
|
23
|
+
from `.1` to `.254`.
|
|
24
|
+
"""
|
|
25
|
+
ip_split = ip.split(".")
|
|
26
|
+
subnet = f"{ip_split[0]}.{ip_split[1]}.{ip_split[2]}"
|
|
27
|
+
core_logger.debug(f"Building a dictionaty of IP addresses base on {subnet}.x")
|
|
28
|
+
devices = {}
|
|
29
|
+
for i in range(1, 255):
|
|
30
|
+
ip = f"{subnet}.{i}"
|
|
31
|
+
devices[ip] = Device(ip)
|
|
32
|
+
return devices
|
woninet/database/base.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from sqlalchemy import create_engine, Engine
|
|
2
|
+
from sqlalchemy.orm import sessionmaker, Session
|
|
3
|
+
from woninet.database.base import Base
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DatabaseEngine:
|
|
7
|
+
"""
|
|
8
|
+
Encapsulate database engine creation and initialization logic.
|
|
9
|
+
|
|
10
|
+
Attributes:
|
|
11
|
+
database_path (str): Path to the SQLite database file.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, database_path: str) -> None:
|
|
15
|
+
"""
|
|
16
|
+
Create a SQLAlchemy engine and session factory for the given database.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
database_path (str): Path to the SQLite database file.
|
|
20
|
+
"""
|
|
21
|
+
self.engine = create_engine(
|
|
22
|
+
f"sqlite:///{database_path}", echo=False, future=True
|
|
23
|
+
)
|
|
24
|
+
self.SessionLocal = sessionmaker(bind=self.engine, autoflush=False)
|
|
25
|
+
|
|
26
|
+
def init_db(self) -> None:
|
|
27
|
+
"""
|
|
28
|
+
Initialize the database and create missing tables.
|
|
29
|
+
"""
|
|
30
|
+
import woninet.database.tables # noqa: F401
|
|
31
|
+
|
|
32
|
+
Base.metadata.create_all(bind=self.engine)
|
|
33
|
+
|
|
34
|
+
def get_database_enigne(self) -> Engine:
|
|
35
|
+
"""
|
|
36
|
+
Return the SQLAlchemy Engine instance.
|
|
37
|
+
"""
|
|
38
|
+
return self.engine
|
|
39
|
+
|
|
40
|
+
def get_session_factory(self) -> sessionmaker[Session]:
|
|
41
|
+
"""
|
|
42
|
+
Return the SQLAlchemy session factory used to create sessions.
|
|
43
|
+
"""
|
|
44
|
+
return self.SessionLocal
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from sqlalchemy.orm import Session
|
|
2
|
+
from woninet.database.tables import AlertEventTable
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AlertEventRepository:
|
|
6
|
+
"""
|
|
7
|
+
Repository for presisting Alert Event entries.
|
|
8
|
+
|
|
9
|
+
Encapsulate ORM operations on `AlertEventTable`.
|
|
10
|
+
|
|
11
|
+
Attributes:
|
|
12
|
+
session (Session): SQLAlchemy session used for
|
|
13
|
+
database operations
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, session: Session) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Initialize the repository.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
session (Session): SQLAlchemy session used for all database operations.
|
|
22
|
+
"""
|
|
23
|
+
self.session = session
|
|
24
|
+
|
|
25
|
+
def insert(self, event: AlertEventTable) -> None:
|
|
26
|
+
"""
|
|
27
|
+
Insert new alert event in the database.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
event (AlertEvent): Instance of the AlertEventTable class.
|
|
31
|
+
"""
|
|
32
|
+
self.session.add(event)
|
|
33
|
+
|
|
34
|
+
def fetch_recent_alert_events(self, limit: int = 10) -> list[AlertEventTable]:
|
|
35
|
+
"""
|
|
36
|
+
Return the last 10 alert events of all devices.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
limit (int): Apply a limit to search.
|
|
40
|
+
|
|
41
|
+
Returns: List of found events, or an empty list if found none
|
|
42
|
+
"""
|
|
43
|
+
return (
|
|
44
|
+
self.session.query(AlertEventTable)
|
|
45
|
+
.order_by(AlertEventTable.id.desc())
|
|
46
|
+
.limit(limit)
|
|
47
|
+
.all()
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def fetch_recent_device_alert_events(
|
|
51
|
+
self, ip: str, limit: int = 10
|
|
52
|
+
) -> list[AlertEventTable]:
|
|
53
|
+
"""
|
|
54
|
+
Returns the last 10 alert events of a device for a given IP address.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
ip (str): IP address to search for.
|
|
58
|
+
limit (int): Apply a limit to search.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
list[AlertEventTable]: List of found event, or an empty list if found none.
|
|
62
|
+
"""
|
|
63
|
+
return (
|
|
64
|
+
self.session.query(AlertEventTable)
|
|
65
|
+
.filter_by(device_ip=ip)
|
|
66
|
+
.order_by(AlertEventTable.id.desc())
|
|
67
|
+
.limit(limit)
|
|
68
|
+
.all()
|
|
69
|
+
)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from sqlalchemy.orm import Session
|
|
2
|
+
from woninet.database.tables import AlertStateTable
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AlertStateRepository:
|
|
6
|
+
"""
|
|
7
|
+
Repository for presisting and updating Alert State entries.
|
|
8
|
+
|
|
9
|
+
Encapsulate ORM operations on `AlertStateTable`.
|
|
10
|
+
|
|
11
|
+
Attributes:
|
|
12
|
+
session (Session): SQLAlchemy session used for
|
|
13
|
+
database operations
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, session: Session) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Initialize the repository.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
session (Session): SQLAlchemy session used for all database operations.
|
|
22
|
+
"""
|
|
23
|
+
self.session = session
|
|
24
|
+
|
|
25
|
+
def fetch_or_create_alert_state(
|
|
26
|
+
self, ip: str, metric: str, consecutive_checks: int
|
|
27
|
+
) -> AlertStateTable:
|
|
28
|
+
"""
|
|
29
|
+
Return an alert state for a specfic IP address and metric; Create a new one
|
|
30
|
+
if it already doesn't exist.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
ip (str): IP address of host.
|
|
34
|
+
metric (str): Name of the metric beaing monitored.
|
|
35
|
+
consecutive_checks (int): Number of consecutive evaluations required
|
|
36
|
+
before a state change.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
AlertStateTable: The found alert state for a given IP and metric,
|
|
40
|
+
or a new alert state for new entries.
|
|
41
|
+
"""
|
|
42
|
+
state = (
|
|
43
|
+
self.session.query(AlertStateTable)
|
|
44
|
+
.filter_by(device_ip=ip, metric=metric)
|
|
45
|
+
.one_or_none()
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if state is None:
|
|
49
|
+
state = AlertStateTable(device_ip=ip, metric=metric)
|
|
50
|
+
state.state = "ok"
|
|
51
|
+
state.consecutive_checks = consecutive_checks
|
|
52
|
+
self.session.add(state)
|
|
53
|
+
|
|
54
|
+
return state
|
|
55
|
+
|
|
56
|
+
def update(self, state: AlertStateTable) -> None:
|
|
57
|
+
"""
|
|
58
|
+
Update an alert state.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
state (AlertStateTable): The updated instance of AlertStateTable class.
|
|
62
|
+
"""
|
|
63
|
+
self.session.add(state)
|
|
64
|
+
|
|
65
|
+
def fetch_alert_state(self, ip: str) -> dict[str, str] | None:
|
|
66
|
+
"""
|
|
67
|
+
Return current alert state for a given IP address.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
ip (str): IP address to search for.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
dict[str,str]|None: A Dictionary containing `metric` and `state`.
|
|
74
|
+
"""
|
|
75
|
+
device_state = self.session.query(AlertStateTable).filter_by(device_ip=ip).all()
|
|
76
|
+
if device_state is None:
|
|
77
|
+
return None
|
|
78
|
+
result = {}
|
|
79
|
+
for state in device_state:
|
|
80
|
+
result[state.metric] = state.state
|
|
81
|
+
return result
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from sqlalchemy.orm import Session
|
|
3
|
+
from woninet.database.tables import DeviceTable
|
|
4
|
+
from woninet.core.models import Device
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DeviceRepository:
|
|
8
|
+
"""
|
|
9
|
+
Repository for persisting and retrieving Device entities.
|
|
10
|
+
|
|
11
|
+
Eencapsulate ORM operations on `DeviceTable` and expose a
|
|
12
|
+
simple API in terms of the domain model `Device`.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
session (Session): SQLAlchemy session used for
|
|
16
|
+
database operations
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, session: Session) -> None:
|
|
20
|
+
"""
|
|
21
|
+
Initialize the repository.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
session (Session): SQLAlchemy session used for all database operations.
|
|
25
|
+
"""
|
|
26
|
+
self.session: Session = session
|
|
27
|
+
|
|
28
|
+
def upsert(self, dev: Device) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Insert or update a device into the database.
|
|
31
|
+
|
|
32
|
+
If `dev.ip` is present, the existing record is looked up by IP
|
|
33
|
+
address, otherwise it is looked up by MAC address. If the record
|
|
34
|
+
is found, its IP and latency fields are updated. The `last_seen`
|
|
35
|
+
timestamp is set to `dev.last_seen` or the current time, and is only
|
|
36
|
+
updated when device is reachable (`dev.latency > 0`).
|
|
37
|
+
|
|
38
|
+
All changes are committed immediatly.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
dev (Device): Domain `Device` instance.
|
|
42
|
+
"""
|
|
43
|
+
existing = self.session.query(DeviceTable).filter(DeviceTable.ip == dev.ip).first()
|
|
44
|
+
if existing is None and dev.mac:
|
|
45
|
+
existing = self.session.query(DeviceTable).filter(DeviceTable.mac == dev.mac).first()
|
|
46
|
+
now = datetime.now()
|
|
47
|
+
|
|
48
|
+
if existing:
|
|
49
|
+
existing.ip = dev.ip
|
|
50
|
+
existing.latency = dev.latency
|
|
51
|
+
if dev.latency > 0:
|
|
52
|
+
existing.last_seen = dev.last_seen or now
|
|
53
|
+
existing.packet_loss = dev.packet_loss
|
|
54
|
+
else:
|
|
55
|
+
existing = DeviceTable(
|
|
56
|
+
ip=dev.ip,
|
|
57
|
+
mac=dev.mac,
|
|
58
|
+
latency=dev.latency,
|
|
59
|
+
packet_loss=dev.packet_loss,
|
|
60
|
+
last_seen=dev.last_seen or now,
|
|
61
|
+
)
|
|
62
|
+
self.session.add(existing)
|
|
63
|
+
|
|
64
|
+
def fetch_devices(self) -> list[Device]:
|
|
65
|
+
"""
|
|
66
|
+
Return all existing devices from the database.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
list[Device]: A list of `Device` instances from `DeviceTable` in the database.
|
|
70
|
+
"""
|
|
71
|
+
rows = self.session.query(DeviceTable).all()
|
|
72
|
+
result = []
|
|
73
|
+
for row in rows:
|
|
74
|
+
dev = Device(ip=row.ip)
|
|
75
|
+
dev.mac = row.mac
|
|
76
|
+
dev.latency = row.latency
|
|
77
|
+
dev.packet_loss = row.packet_loss
|
|
78
|
+
dev.last_seen = row.last_seen
|
|
79
|
+
result.append(dev)
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
def fetch_device(self, ip: str) -> Device | None:
|
|
83
|
+
"""
|
|
84
|
+
Return a device from the database for the given IP address or
|
|
85
|
+
`None` if not found.
|
|
86
|
+
"""
|
|
87
|
+
row = self.session.query(DeviceTable).filter_by(ip=ip).one_or_none()
|
|
88
|
+
if row is not None:
|
|
89
|
+
device = Device(ip=row.ip)
|
|
90
|
+
device.mac = row.mac
|
|
91
|
+
device.latency = row.latency
|
|
92
|
+
device.last_seen = row.last_seen
|
|
93
|
+
device.packet_loss = row.packet_loss
|
|
94
|
+
return device
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
def count_devices(self) -> int:
|
|
98
|
+
"""
|
|
99
|
+
Return the number of existing devices in the database.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
int: Number of devices in the database.
|
|
103
|
+
"""
|
|
104
|
+
total = self.session.query(DeviceTable).count()
|
|
105
|
+
return total
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from sqlalchemy.orm import Session
|
|
3
|
+
from woninet.database.tables import MetricTable
|
|
4
|
+
from woninet.core.models import MetricRecord
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MetricRepository:
|
|
8
|
+
"""
|
|
9
|
+
Repository for persisting and retrieving MetricRevord entities.
|
|
10
|
+
|
|
11
|
+
Encapsulate OEM operations on `MetricTable` and expose a
|
|
12
|
+
simple API in terms of the domain model `MetricRecord`.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
session (Session): SQLAlchemy session used for
|
|
16
|
+
database operations
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, session: Session) -> None:
|
|
20
|
+
"""
|
|
21
|
+
Initialize the repository.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
session (Session): SQLAlchemy session used for all database operations.
|
|
25
|
+
"""
|
|
26
|
+
self.session: Session = session
|
|
27
|
+
|
|
28
|
+
def insert(self, metric: MetricRecord) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Insert new metric in the database.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
metric (MetricRecord): An object of domain `MetricRecord` instance.
|
|
34
|
+
"""
|
|
35
|
+
self.session.add(
|
|
36
|
+
MetricTable(
|
|
37
|
+
device_ip=metric.device_ip,
|
|
38
|
+
metric=metric.metric,
|
|
39
|
+
value=metric.value,
|
|
40
|
+
timestamp=metric.timestamp or datetime.now(),
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def fetch_metrics(self) -> list[MetricRecord]:
|
|
45
|
+
"""
|
|
46
|
+
Return all metric records from database.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
list[MetricRecord]: A list of `MetricRecord` instances from `MetricTable` in the database.
|
|
50
|
+
"""
|
|
51
|
+
rows = self.session.query(MetricTable).all()
|
|
52
|
+
result = []
|
|
53
|
+
for row in rows:
|
|
54
|
+
metric = MetricTable(
|
|
55
|
+
device_ip=row.device_ip, metric=row.metric, value=row.value
|
|
56
|
+
)
|
|
57
|
+
result.append(metric)
|
|
58
|
+
return result
|
|
59
|
+
|
|
60
|
+
def remove_db_metrics(self) -> None:
|
|
61
|
+
"""
|
|
62
|
+
Remove all rows from the `MetricTable`.
|
|
63
|
+
"""
|
|
64
|
+
self.session.query(MetricTable).delete()
|
|
65
|
+
self.session.commit()
|
|
66
|
+
|
|
67
|
+
def count_metrics(self) -> int:
|
|
68
|
+
"""
|
|
69
|
+
Return the number of metrics in the database.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
int: Number of metrics in the database.
|
|
73
|
+
"""
|
|
74
|
+
total = self.session.query(MetricTable).count()
|
|
75
|
+
return total
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from sqlalchemy import Integer, String, DateTime, Float, UniqueConstraint
|
|
2
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from woninet.database.base import Base
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DeviceTable(Base):
|
|
8
|
+
"""
|
|
9
|
+
ORM model representing a discovered network device.
|
|
10
|
+
|
|
11
|
+
Each row stores the most recently observed state for a device,
|
|
12
|
+
including IP address, MAC address, latency, and timestamp
|
|
13
|
+
of the last time device was reachable.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
__tablename__ = "devices"
|
|
17
|
+
|
|
18
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
19
|
+
ip: Mapped[str] = mapped_column(String, unique=True, index=True)
|
|
20
|
+
mac: Mapped[str | None] = mapped_column(
|
|
21
|
+
String, unique=True, index=True, nullable=True
|
|
22
|
+
)
|
|
23
|
+
latency: Mapped[float] = mapped_column(Float)
|
|
24
|
+
packet_loss: Mapped[int] = mapped_column(Integer)
|
|
25
|
+
last_seen: Mapped[datetime] = mapped_column(
|
|
26
|
+
DateTime(timezone=False), default=lambda: datetime.now(), index=True
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class MetricTable(Base):
|
|
31
|
+
"""
|
|
32
|
+
ORM model representing a metric record of a device model.
|
|
33
|
+
|
|
34
|
+
Each row stores the metric record of device, including IP address,
|
|
35
|
+
metric name, value and timestamp of the time the metric was recorded.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
__tablename__ = "metrics"
|
|
39
|
+
|
|
40
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
41
|
+
device_ip: Mapped[str] = mapped_column(String, index=True)
|
|
42
|
+
metric: Mapped[str] = mapped_column(String, index=True)
|
|
43
|
+
value: Mapped[float] = mapped_column(Float)
|
|
44
|
+
timestamp: Mapped[datetime] = mapped_column(
|
|
45
|
+
DateTime(timezone=False), default=lambda: datetime.now(), index=True
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AlertStateTable(Base):
|
|
50
|
+
"""
|
|
51
|
+
ORM model representing an alert state for a specific device.
|
|
52
|
+
|
|
53
|
+
Each row stores the alert state of a device, including device IP address,
|
|
54
|
+
metric name, last state, and last time alert triggered.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
__tablename__ = "alert_state"
|
|
58
|
+
__table_args__ = (
|
|
59
|
+
UniqueConstraint("device_ip", "metric", name="uix_device_metric"),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
63
|
+
device_ip: Mapped[str] = mapped_column(String, index=True)
|
|
64
|
+
metric: Mapped[str] = mapped_column(String, index=True)
|
|
65
|
+
state: Mapped[str] = mapped_column(String, default="ok", index=True)
|
|
66
|
+
consecutive_checks: Mapped[int] = mapped_column(Integer, index=True, nullable=False)
|
|
67
|
+
triggered_at: Mapped[datetime] = mapped_column(
|
|
68
|
+
DateTime(timezone=False), default=lambda: datetime.now(), index=True
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class AlertEventTable(Base):
|
|
73
|
+
"""
|
|
74
|
+
ORM model representing an alert event occured.
|
|
75
|
+
|
|
76
|
+
Each row stores the alert event of a device, including device IP address,
|
|
77
|
+
metric name, captured value of metric, event type, and timestamp.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
__tablename__ = "alert_event"
|
|
81
|
+
|
|
82
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
83
|
+
device_ip: Mapped[str] = mapped_column(String, index=True)
|
|
84
|
+
metric: Mapped[str] = mapped_column(String, index=True)
|
|
85
|
+
value: Mapped[float] = mapped_column(Float)
|
|
86
|
+
event_type: Mapped[str] = mapped_column(String, index=True)
|
|
87
|
+
timestamp: Mapped[datetime] = mapped_column(
|
|
88
|
+
DateTime(timezone=False), default=lambda: datetime.now(), index=True
|
|
89
|
+
)
|