mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-04-02 00:02:19 -07:00
BE+FE: Unstable devices list (3 status changes in 1h)
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
This commit is contained in:
@@ -100,6 +100,7 @@ class Device(ObjectType):
|
||||
devParentPortSource = String(description="Source tracking for devParentPort (USER, LOCKED, NEWDEV, or plugin prefix)")
|
||||
devParentRelTypeSource = String(description="Source tracking for devParentRelType (USER, LOCKED, NEWDEV, or plugin prefix)")
|
||||
devVlanSource = String(description="Source tracking for devVlan")
|
||||
devFlapping = String(description="ndicates flapping device (device changing between online/offline states frequently)")
|
||||
|
||||
|
||||
class DeviceResult(ObjectType):
|
||||
@@ -266,7 +267,7 @@ class Query(ObjectType):
|
||||
filtered.append(device)
|
||||
|
||||
devices_data = filtered
|
||||
# 🔻 START If you change anything here, also update get_device_condition_by_status
|
||||
# 🔻 START If you change anything here, also update get_device_conditions
|
||||
elif status == "connected":
|
||||
devices_data = [
|
||||
device
|
||||
@@ -323,7 +324,25 @@ class Query(ObjectType):
|
||||
for device in devices_data
|
||||
if device["devType"] in network_dev_types and device["devPresentLastScan"] == 0 and device["devIsArchived"] == 0
|
||||
]
|
||||
# 🔺 END If you change anything here, also update get_device_condition_by_status
|
||||
elif status == "unstable_devices":
|
||||
devices_data = [
|
||||
device
|
||||
for device in devices_data
|
||||
if device["devIsArchived"] == 0 and device["devFlapping"] == 1
|
||||
]
|
||||
elif status == "unstable_favorites":
|
||||
devices_data = [
|
||||
device
|
||||
for device in devices_data
|
||||
if device["devIsArchived"] == 0 and device["devFavorite"] == 1 and device["devFlapping"] == 1
|
||||
]
|
||||
elif status == "unstable_network_devices":
|
||||
devices_data = [
|
||||
device
|
||||
for device in devices_data
|
||||
if device["devIsArchived"] == 0 and device["devType"] in network_dev_types and device["devFlapping"] == 1
|
||||
]
|
||||
# 🔺 END If you change anything here, also update get_device_conditions
|
||||
elif status == "all_devices":
|
||||
devices_data = devices_data # keep all
|
||||
|
||||
|
||||
@@ -58,70 +58,13 @@ NULL_EQUIVALENTS = ["", "null", "(unknown)", "(Unknown)", "(name not found)"]
|
||||
# Convert list to SQL string: wrap each value in single quotes and escape single quotes if needed
|
||||
NULL_EQUIVALENTS_SQL = ",".join("'" + v.replace("'", "''") + "'" for v in NULL_EQUIVALENTS)
|
||||
|
||||
|
||||
# ===============================================================================
|
||||
# SQL queries
|
||||
# ===============================================================================
|
||||
sql_devices_all = """
|
||||
SELECT
|
||||
rowid,
|
||||
IFNULL(devMac, '') AS devMac,
|
||||
IFNULL(devName, '') AS devName,
|
||||
IFNULL(devOwner, '') AS devOwner,
|
||||
IFNULL(devType, '') AS devType,
|
||||
IFNULL(devVendor, '') AS devVendor,
|
||||
IFNULL(devFavorite, '') AS devFavorite,
|
||||
IFNULL(devGroup, '') AS devGroup,
|
||||
IFNULL(devComments, '') AS devComments,
|
||||
IFNULL(devFirstConnection, '') AS devFirstConnection,
|
||||
IFNULL(devLastConnection, '') AS devLastConnection,
|
||||
IFNULL(devLastIP, '') AS devLastIP,
|
||||
IFNULL(devPrimaryIPv4, '') AS devPrimaryIPv4,
|
||||
IFNULL(devPrimaryIPv6, '') AS devPrimaryIPv6,
|
||||
IFNULL(devVlan, '') AS devVlan,
|
||||
IFNULL(devForceStatus, '') AS devForceStatus,
|
||||
IFNULL(devStaticIP, '') AS devStaticIP,
|
||||
IFNULL(devScan, '') AS devScan,
|
||||
IFNULL(devLogEvents, '') AS devLogEvents,
|
||||
IFNULL(devAlertEvents, '') AS devAlertEvents,
|
||||
IFNULL(devAlertDown, '') AS devAlertDown,
|
||||
IFNULL(devSkipRepeated, '') AS devSkipRepeated,
|
||||
IFNULL(devLastNotification, '') AS devLastNotification,
|
||||
IFNULL(devPresentLastScan, 0) AS devPresentLastScan,
|
||||
IFNULL(devIsNew, '') AS devIsNew,
|
||||
IFNULL(devLocation, '') AS devLocation,
|
||||
IFNULL(devIsArchived, '') AS devIsArchived,
|
||||
IFNULL(devParentMAC, '') AS devParentMAC,
|
||||
IFNULL(devParentPort, '') AS devParentPort,
|
||||
IFNULL(devIcon, '') AS devIcon,
|
||||
IFNULL(devGUID, '') AS devGUID,
|
||||
IFNULL(devSite, '') AS devSite,
|
||||
IFNULL(devSSID, '') AS devSSID,
|
||||
IFNULL(devSyncHubNode, '') AS devSyncHubNode,
|
||||
IFNULL(devSourcePlugin, '') AS devSourcePlugin,
|
||||
IFNULL(devCustomProps, '') AS devCustomProps,
|
||||
IFNULL(devFQDN, '') AS devFQDN,
|
||||
IFNULL(devParentRelType, '') AS devParentRelType,
|
||||
IFNULL(devReqNicsOnline, '') AS devReqNicsOnline,
|
||||
IFNULL(devMacSource, '') AS devMacSource,
|
||||
IFNULL(devNameSource, '') AS devNameSource,
|
||||
IFNULL(devFQDNSource, '') AS devFQDNSource,
|
||||
IFNULL(devLastIPSource, '') AS devLastIPSource,
|
||||
IFNULL(devVendorSource, '') AS devVendorSource,
|
||||
IFNULL(devSSIDSource, '') AS devSSIDSource,
|
||||
IFNULL(devParentMACSource, '') AS devParentMACSource,
|
||||
IFNULL(devParentPortSource, '') AS devParentPortSource,
|
||||
IFNULL(devParentRelTypeSource, '') AS devParentRelTypeSource,
|
||||
IFNULL(devVlanSource, '') AS devVlanSource,
|
||||
CASE
|
||||
WHEN devIsNew = 1 THEN 'New'
|
||||
WHEN devPresentLastScan = 1 THEN 'On-line'
|
||||
WHEN devPresentLastScan = 0 AND devAlertDown != 0 THEN 'Down'
|
||||
WHEN devIsArchived = 1 THEN 'Archived'
|
||||
WHEN devPresentLastScan = 0 THEN 'Off-line'
|
||||
ELSE 'Unknown status'
|
||||
END AS devStatus
|
||||
FROM Devices
|
||||
sql_devices_all = """
|
||||
SELECT
|
||||
*
|
||||
FROM DevicesView
|
||||
"""
|
||||
|
||||
sql_appevents = """select * from AppEvents order by DateTimeCreated desc"""
|
||||
|
||||
@@ -14,22 +14,28 @@ from const import NULL_EQUIVALENTS_SQL # noqa: E402 [flake8 lint suppression]
|
||||
def get_device_conditions():
|
||||
network_dev_types = ",".join("'" + v.replace("'", "''") + "'" for v in get_setting_value("NETWORK_DEVICE_TYPES"))
|
||||
|
||||
# DO NOT CHANGE ORDER
|
||||
# Base archived condition
|
||||
base_active = "devIsArchived=0"
|
||||
|
||||
# DO NOT CHANGE ORDER - if you add or change something update graphql endpoint as well
|
||||
conditions = {
|
||||
"all": "WHERE devIsArchived=0",
|
||||
"my": "WHERE devIsArchived=0",
|
||||
"all": f"WHERE {base_active}",
|
||||
"my": f"WHERE {base_active}",
|
||||
"connected": "WHERE devPresentLastScan=1",
|
||||
"favorites": "WHERE devIsArchived=0 AND devFavorite=1",
|
||||
"new": "WHERE devIsArchived=0 AND devIsNew=1",
|
||||
"down": "WHERE devIsArchived=0 AND devAlertDown != 0 AND devPresentLastScan=0",
|
||||
"offline": "WHERE devIsArchived=0 AND devPresentLastScan=0",
|
||||
"favorites": f"WHERE {base_active} AND devFavorite=1",
|
||||
"new": f"WHERE {base_active} AND devIsNew=1",
|
||||
"down": f"WHERE {base_active} AND devAlertDown != 0 AND devPresentLastScan=0",
|
||||
"offline": f"WHERE {base_active} AND devPresentLastScan=0",
|
||||
"archived": "WHERE devIsArchived=1",
|
||||
"network_devices": f"WHERE devIsArchived=0 AND devType in ({network_dev_types})",
|
||||
"network_devices_down": f"WHERE devIsArchived=0 AND devType in ({network_dev_types}) AND devPresentLastScan=0",
|
||||
"unknown": f"WHERE devIsArchived=0 AND devName in ({NULL_EQUIVALENTS_SQL})",
|
||||
"known": f"WHERE devIsArchived=0 AND devName not in ({NULL_EQUIVALENTS_SQL})",
|
||||
"favorites_offline": "WHERE devIsArchived=0 AND devFavorite=1 AND devPresentLastScan=0",
|
||||
"new_online": "WHERE devIsArchived=0 AND devIsNew=1 AND devPresentLastScan=0",
|
||||
"network_devices": f"WHERE {base_active} AND devType IN ({network_dev_types})",
|
||||
"network_devices_down": f"WHERE {base_active} AND devType IN ({network_dev_types}) AND devPresentLastScan=0",
|
||||
"unknown": f"WHERE {base_active} AND devName IN ({NULL_EQUIVALENTS_SQL})",
|
||||
"known": f"WHERE {base_active} AND devName NOT IN ({NULL_EQUIVALENTS_SQL})",
|
||||
"favorites_offline": f"WHERE {base_active} AND devFavorite=1 AND devPresentLastScan=0",
|
||||
"new_online": f"WHERE {base_active} AND devIsNew=1 AND devPresentLastScan=0",
|
||||
"unstable_devices": f"WHERE {base_active} AND devFlapping=1",
|
||||
"unstable_favorites": f"WHERE {base_active} AND devFavorite=1 AND devFlapping=1",
|
||||
"unstable_network_devices": f"WHERE {base_active} AND devType IN ({network_dev_types}) AND devFlapping=1",
|
||||
}
|
||||
|
||||
return conditions
|
||||
|
||||
@@ -232,6 +232,87 @@ def ensure_views(sql) -> bool:
|
||||
|
||||
""")
|
||||
|
||||
FLAP_THRESHOLD = 3
|
||||
FLAP_WINDOW_HOURS = 1
|
||||
|
||||
sql.execute(""" DROP VIEW IF EXISTS DevicesView;""")
|
||||
sql.execute(f""" CREATE VIEW DevicesView AS
|
||||
SELECT
|
||||
rowid,
|
||||
IFNULL(devMac, '') AS devMac,
|
||||
IFNULL(devName, '') AS devName,
|
||||
IFNULL(devOwner, '') AS devOwner,
|
||||
IFNULL(devType, '') AS devType,
|
||||
IFNULL(devVendor, '') AS devVendor,
|
||||
IFNULL(devFavorite, '') AS devFavorite,
|
||||
IFNULL(devGroup, '') AS devGroup,
|
||||
IFNULL(devComments, '') AS devComments,
|
||||
IFNULL(devFirstConnection, '') AS devFirstConnection,
|
||||
IFNULL(devLastConnection, '') AS devLastConnection,
|
||||
IFNULL(devLastIP, '') AS devLastIP,
|
||||
IFNULL(devPrimaryIPv4, '') AS devPrimaryIPv4,
|
||||
IFNULL(devPrimaryIPv6, '') AS devPrimaryIPv6,
|
||||
IFNULL(devVlan, '') AS devVlan,
|
||||
IFNULL(devForceStatus, '') AS devForceStatus,
|
||||
IFNULL(devStaticIP, '') AS devStaticIP,
|
||||
IFNULL(devScan, '') AS devScan,
|
||||
IFNULL(devLogEvents, '') AS devLogEvents,
|
||||
IFNULL(devAlertEvents, '') AS devAlertEvents,
|
||||
IFNULL(devAlertDown, '') AS devAlertDown,
|
||||
IFNULL(devSkipRepeated, '') AS devSkipRepeated,
|
||||
IFNULL(devLastNotification, '') AS devLastNotification,
|
||||
IFNULL(devPresentLastScan, 0) AS devPresentLastScan,
|
||||
IFNULL(devIsNew, '') AS devIsNew,
|
||||
IFNULL(devLocation, '') AS devLocation,
|
||||
IFNULL(devIsArchived, '') AS devIsArchived,
|
||||
IFNULL(devParentMAC, '') AS devParentMAC,
|
||||
IFNULL(devParentPort, '') AS devParentPort,
|
||||
IFNULL(devIcon, '') AS devIcon,
|
||||
IFNULL(devGUID, '') AS devGUID,
|
||||
IFNULL(devSite, '') AS devSite,
|
||||
IFNULL(devSSID, '') AS devSSID,
|
||||
IFNULL(devSyncHubNode, '') AS devSyncHubNode,
|
||||
IFNULL(devSourcePlugin, '') AS devSourcePlugin,
|
||||
IFNULL(devCustomProps, '') AS devCustomProps,
|
||||
IFNULL(devFQDN, '') AS devFQDN,
|
||||
IFNULL(devParentRelType, '') AS devParentRelType,
|
||||
IFNULL(devReqNicsOnline, '') AS devReqNicsOnline,
|
||||
IFNULL(devMacSource, '') AS devMacSource,
|
||||
IFNULL(devNameSource, '') AS devNameSource,
|
||||
IFNULL(devFQDNSource, '') AS devFQDNSource,
|
||||
IFNULL(devLastIPSource, '') AS devLastIPSource,
|
||||
IFNULL(devVendorSource, '') AS devVendorSource,
|
||||
IFNULL(devSSIDSource, '') AS devSSIDSource,
|
||||
IFNULL(devParentMACSource, '') AS devParentMACSource,
|
||||
IFNULL(devParentPortSource, '') AS devParentPortSource,
|
||||
IFNULL(devParentRelTypeSource, '') AS devParentRelTypeSource,
|
||||
IFNULL(devVlanSource, '') AS devVlanSource,
|
||||
CASE
|
||||
WHEN devIsNew = 1 THEN 'New'
|
||||
WHEN devPresentLastScan = 1 THEN 'On-line'
|
||||
WHEN devPresentLastScan = 0 AND devAlertDown != 0 THEN 'Down'
|
||||
WHEN devIsArchived = 1 THEN 'Archived'
|
||||
WHEN devPresentLastScan = 0 THEN 'Off-line'
|
||||
ELSE 'Unknown status'
|
||||
END AS devStatus,
|
||||
CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM Events e
|
||||
WHERE e.eve_MAC = Devices.devMac
|
||||
AND e.eve_EventType IN ('Connected','Disconnected','Device Down','Down Reconnected')
|
||||
AND e.eve_DateTime >= datetime('now', '-{FLAP_WINDOW_HOURS} hours')
|
||||
GROUP BY e.eve_MAC
|
||||
HAVING COUNT(*) >= {FLAP_THRESHOLD}
|
||||
)
|
||||
THEN 1
|
||||
ELSE 0
|
||||
END AS devFlapping
|
||||
|
||||
FROM Devices
|
||||
|
||||
""")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -338,7 +338,7 @@ class DeviceInstance:
|
||||
for key, condition in conditions.items():
|
||||
# Make sure the alias is SQL-safe (no spaces or special chars)
|
||||
alias = key.replace(" ", "_").lower()
|
||||
sub_queries.append(f'(SELECT COUNT(*) FROM Devices {condition}) AS "{alias}"')
|
||||
sub_queries.append(f'(SELECT COUNT(*) FROM DevicesView {condition}) AS "{alias}"')
|
||||
|
||||
# Join all sub-selects with commas
|
||||
query = "SELECT\n " + ",\n ".join(sub_queries)
|
||||
@@ -360,7 +360,7 @@ class DeviceInstance:
|
||||
for key, condition in conditions.items():
|
||||
# Make sure the alias is SQL-safe (no spaces or special chars)
|
||||
alias = key.replace(" ", "_").lower()
|
||||
sub_queries.append(f'(SELECT COUNT(*) FROM Devices {condition}) AS "{alias}"')
|
||||
sub_queries.append(f'(SELECT COUNT(*) FROM DevicesView {condition}) AS "{alias}"')
|
||||
|
||||
# Join all sub-selects with commas
|
||||
query = "SELECT\n " + ",\n ".join(sub_queries)
|
||||
@@ -381,7 +381,8 @@ class DeviceInstance:
|
||||
# Build condition for SQL
|
||||
condition = get_device_condition_by_status(status) if status else ""
|
||||
|
||||
query = f"SELECT * FROM Devices {condition}"
|
||||
# Only DevicesView has devFlapping
|
||||
query = f"SELECT * FROM DevicesView {condition}"
|
||||
sql.execute(query)
|
||||
|
||||
table_data = []
|
||||
|
||||
@@ -218,3 +218,50 @@ class EventInstance:
|
||||
|
||||
# Return as list
|
||||
return [row[0], row[1], row[2], row[3], row[4], row[5]]
|
||||
|
||||
def get_unstable_devices(self, hours: int = 1, threshold: int = 3, macs_only: bool = True):
|
||||
"""
|
||||
Return unstable devices based on flap detection.
|
||||
|
||||
A device is considered unstable if it has >= threshold events within the last `hours`.
|
||||
|
||||
Events considered:
|
||||
- Connected
|
||||
- Disconnected
|
||||
- Device Down
|
||||
- Down Reconnected
|
||||
|
||||
Args:
|
||||
hours (int): Time window in hours (default: 1)
|
||||
threshold (int): Minimum number of events to be considered unstable (default: 3)
|
||||
macs_only (bool): If True, return only MAC addresses (set). Otherwise return full rows.
|
||||
|
||||
Returns:
|
||||
set[str] OR list[dict]
|
||||
"""
|
||||
|
||||
if hours <= 0 or threshold <= 0:
|
||||
mylog("warn", f"[Events] get_unstable_devices invalid params: hours={hours}, threshold={threshold}")
|
||||
return set() if macs_only else []
|
||||
|
||||
conn = self._conn()
|
||||
|
||||
sql = """
|
||||
SELECT eve_MAC, COUNT(*) as event_count
|
||||
FROM Events
|
||||
WHERE eve_EventType IN ('Connected','Disconnected','Device Down','Down Reconnected')
|
||||
AND eve_DateTime >= datetime('now', ?)
|
||||
GROUP BY eve_MAC
|
||||
HAVING COUNT(*) >= ?
|
||||
"""
|
||||
|
||||
# SQLite expects "-1 hours" format
|
||||
window = f"-{hours} hours"
|
||||
|
||||
rows = conn.execute(sql, (window, threshold)).fetchall()
|
||||
conn.close()
|
||||
|
||||
if macs_only:
|
||||
return {row["eve_MAC"] for row in rows}
|
||||
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
@@ -49,7 +49,7 @@ class PluginObjectInstance:
|
||||
"SELECT * FROM Plugins_Objects WHERE Plugin = ?", (plugin,)
|
||||
)
|
||||
|
||||
def getLastNCreatedPerPLugin(self, plugin, entries=1):
|
||||
def getLastNCreatedPerPlugin(self, plugin, entries=1):
|
||||
return self._fetchall(
|
||||
"""
|
||||
SELECT *
|
||||
|
||||
Reference in New Issue
Block a user