mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-04-01 07:42:19 -07:00
Merge branch 'next_release' of https://github.com/netalertx/NetAlertX into next_release
This commit is contained in:
@@ -61,8 +61,8 @@ class DeviceInstance:
|
||||
|
||||
def getDown(self):
|
||||
return self._fetchall("""
|
||||
SELECT * FROM Devices
|
||||
WHERE devAlertDown = 1 AND devPresentLastScan = 0
|
||||
SELECT * FROM DevicesView
|
||||
WHERE devAlertDown = 1 AND devPresentLastScan = 0 AND devIsSleeping = 0
|
||||
""")
|
||||
|
||||
def getOffline(self):
|
||||
@@ -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 = []
|
||||
@@ -453,7 +454,9 @@ class DeviceInstance:
|
||||
"devPresenceHours": 0,
|
||||
"devFQDN": "",
|
||||
"devForceStatus" : "dont_force",
|
||||
"devVlan": ""
|
||||
"devVlan": "",
|
||||
"devCanSleep": 0,
|
||||
"devIsSleeping": 0
|
||||
}
|
||||
return device_data
|
||||
|
||||
@@ -462,44 +465,41 @@ class DeviceInstance:
|
||||
|
||||
# Fetch device info + computed fields
|
||||
sql = f"""
|
||||
SELECT
|
||||
d.*,
|
||||
CASE
|
||||
WHEN d.devAlertDown != 0 AND d.devPresentLastScan = 0 THEN 'Down'
|
||||
WHEN d.devPresentLastScan = 1 THEN 'On-line'
|
||||
ELSE 'Off-line'
|
||||
END AS devStatus,
|
||||
SELECT
|
||||
d.*,
|
||||
LOWER(d.devMac) AS devMac,
|
||||
LOWER(d.devParentMAC) AS devParentMAC,
|
||||
|
||||
(SELECT COUNT(*) FROM Sessions
|
||||
WHERE ses_MAC = d.devMac AND (
|
||||
ses_DateTimeConnection >= {period_date_sql} OR
|
||||
ses_DateTimeDisconnection >= {period_date_sql} OR
|
||||
ses_StillConnected = 1
|
||||
)) AS devSessions,
|
||||
(SELECT COUNT(*) FROM Sessions
|
||||
WHERE LOWER(ses_MAC) = LOWER(d.devMac) AND (
|
||||
ses_DateTimeConnection >= {period_date_sql} OR
|
||||
ses_DateTimeDisconnection >= {period_date_sql} OR
|
||||
ses_StillConnected = 1
|
||||
)) AS devSessions,
|
||||
|
||||
(SELECT COUNT(*) FROM Events
|
||||
WHERE eve_MAC = d.devMac AND eve_DateTime >= {period_date_sql}
|
||||
AND eve_EventType NOT IN ('Connected','Disconnected')) AS devEvents,
|
||||
(SELECT COUNT(*) FROM Events
|
||||
WHERE LOWER(eve_MAC) = LOWER(d.devMac) AND eve_DateTime >= {period_date_sql}
|
||||
AND eve_EventType NOT IN ('Connected','Disconnected')) AS devEvents,
|
||||
|
||||
(SELECT COUNT(*) FROM Events
|
||||
WHERE eve_MAC = d.devMac AND eve_DateTime >= {period_date_sql}
|
||||
AND eve_EventType = 'Device Down') AS devDownAlerts,
|
||||
(SELECT COUNT(*) FROM Events
|
||||
WHERE LOWER(eve_MAC) = LOWER(d.devMac) AND eve_DateTime >= {period_date_sql}
|
||||
AND eve_EventType = 'Device Down') AS devDownAlerts,
|
||||
|
||||
(SELECT CAST(MAX(0, SUM(
|
||||
julianday(IFNULL(ses_DateTimeDisconnection,'{now}')) -
|
||||
julianday(CASE WHEN ses_DateTimeConnection < {period_date_sql}
|
||||
THEN {period_date_sql} ELSE ses_DateTimeConnection END)
|
||||
) * 24) AS INT)
|
||||
FROM Sessions
|
||||
WHERE ses_MAC = d.devMac
|
||||
AND ses_DateTimeConnection IS NOT NULL
|
||||
AND (ses_DateTimeDisconnection IS NOT NULL OR ses_StillConnected = 1)
|
||||
AND (ses_DateTimeConnection >= {period_date_sql}
|
||||
OR ses_DateTimeDisconnection >= {period_date_sql} OR ses_StillConnected = 1)
|
||||
) AS devPresenceHours
|
||||
(SELECT CAST(MAX(0, SUM(
|
||||
julianday(IFNULL(ses_DateTimeDisconnection,'{now}')) -
|
||||
julianday(CASE WHEN ses_DateTimeConnection < {period_date_sql}
|
||||
THEN {period_date_sql} ELSE ses_DateTimeConnection END)
|
||||
) * 24) AS INT)
|
||||
FROM Sessions
|
||||
WHERE LOWER(ses_MAC) = LOWER(d.devMac)
|
||||
AND ses_DateTimeConnection IS NOT NULL
|
||||
AND (ses_DateTimeDisconnection IS NOT NULL OR ses_StillConnected = 1)
|
||||
AND (ses_DateTimeConnection >= {period_date_sql}
|
||||
OR ses_DateTimeDisconnection >= {period_date_sql} OR ses_StillConnected = 1)
|
||||
) AS devPresenceHours
|
||||
|
||||
FROM Devices d
|
||||
WHERE d.devMac = ? OR CAST(d.rowid AS TEXT) = ?
|
||||
FROM DevicesView d
|
||||
WHERE LOWER(d.devMac) = LOWER(?) OR CAST(d.rowid AS TEXT) = ?
|
||||
"""
|
||||
|
||||
conn = get_temp_db_connection()
|
||||
@@ -567,7 +567,8 @@ class DeviceInstance:
|
||||
"devIsArchived",
|
||||
"devCustomProps",
|
||||
"devForceStatus",
|
||||
"devVlan"
|
||||
"devVlan",
|
||||
"devCanSleep"
|
||||
}
|
||||
|
||||
# Only mark USER for tracked fields that this method actually updates.
|
||||
@@ -613,12 +614,12 @@ class DeviceInstance:
|
||||
devMac, devName, devOwner, devType, devVendor, devIcon,
|
||||
devFavorite, devGroup, devLocation, devComments,
|
||||
devParentMAC, devParentPort, devSSID, devSite,
|
||||
devStaticIP, devScan, devAlertEvents, devAlertDown,
|
||||
devStaticIP, devScan, devAlertEvents, devAlertDown, devCanSleep,
|
||||
devParentRelType, devReqNicsOnline, devSkipRepeated,
|
||||
devIsNew, devIsArchived, devLastConnection,
|
||||
devFirstConnection, devLastIP, devGUID, devCustomProps,
|
||||
devSourcePlugin, devForceStatus, devVlan
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
values = (
|
||||
@@ -640,6 +641,7 @@ class DeviceInstance:
|
||||
data.get("devScan") or 0,
|
||||
data.get("devAlertEvents") or 0,
|
||||
data.get("devAlertDown") or 0,
|
||||
data.get("devCanSleep") or 0,
|
||||
data.get("devParentRelType") or "default",
|
||||
data.get("devReqNicsOnline") or 0,
|
||||
data.get("devSkipRepeated") or 0,
|
||||
@@ -661,7 +663,7 @@ class DeviceInstance:
|
||||
devName=?, devOwner=?, devType=?, devVendor=?, devIcon=?,
|
||||
devFavorite=?, devGroup=?, devLocation=?, devComments=?,
|
||||
devParentMAC=?, devParentPort=?, devSSID=?, devSite=?,
|
||||
devStaticIP=?, devScan=?, devAlertEvents=?, devAlertDown=?,
|
||||
devStaticIP=?, devScan=?, devAlertEvents=?, devAlertDown=?, devCanSleep=?,
|
||||
devParentRelType=?, devReqNicsOnline=?, devSkipRepeated=?,
|
||||
devIsNew=?, devIsArchived=?, devCustomProps=?, devForceStatus=?, devVlan=?
|
||||
WHERE devMac=?
|
||||
@@ -684,6 +686,7 @@ class DeviceInstance:
|
||||
data.get("devScan") or 0,
|
||||
data.get("devAlertEvents") or 0,
|
||||
data.get("devAlertDown") or 0,
|
||||
data.get("devCanSleep") or 0,
|
||||
data.get("devParentRelType") or "default",
|
||||
data.get("devReqNicsOnline") or 0,
|
||||
data.get("devSkipRepeated") or 0,
|
||||
@@ -817,9 +820,9 @@ class DeviceInstance:
|
||||
conn = get_temp_db_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Build safe SQL with column name
|
||||
sql = f"UPDATE Devices SET {column_name}=? WHERE devMac=?"
|
||||
cur.execute(sql, (column_value, mac))
|
||||
# Convert the MAC to lowercase for comparison
|
||||
sql = f"UPDATE Devices SET {column_name}=? WHERE LOWER(devMac)=?"
|
||||
cur.execute(sql, (column_value, mac.lower()))
|
||||
conn.commit()
|
||||
|
||||
if cur.rowcount > 0:
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
import re
|
||||
import uuid
|
||||
import socket
|
||||
from yattag import indent
|
||||
@@ -307,8 +308,16 @@ def construct_notifications(JSON, section):
|
||||
build_direction = "TOP_TO_BOTTOM"
|
||||
text_line = "{}\t{}\n"
|
||||
|
||||
# Read template settings
|
||||
show_headers = get_setting_value("NTFPRCS_TEXT_SECTION_HEADERS")
|
||||
if show_headers is None or show_headers == "":
|
||||
show_headers = True
|
||||
text_template = get_setting_value(f"NTFPRCS_TEXT_TEMPLATE_{section}") or ""
|
||||
|
||||
if len(jsn) > 0:
|
||||
text = tableTitle + "\n---------\n"
|
||||
# Section header (text)
|
||||
if show_headers:
|
||||
text = tableTitle + "\n---------\n"
|
||||
|
||||
# Convert a JSON into an HTML table
|
||||
html = convert(
|
||||
@@ -325,13 +334,24 @@ def construct_notifications(JSON, section):
|
||||
)
|
||||
|
||||
# prepare text-only message
|
||||
for device in jsn:
|
||||
for header in headers:
|
||||
padding = ""
|
||||
if len(header) < 4:
|
||||
padding = "\t"
|
||||
text += text_line.format(header + ": " + padding, device[header])
|
||||
text += "\n"
|
||||
if text_template:
|
||||
# Custom template: replace {FieldName} placeholders per device
|
||||
for device in jsn:
|
||||
line = re.sub(
|
||||
r'\{(.+?)\}',
|
||||
lambda m: str(device.get(m.group(1), m.group(0))),
|
||||
text_template,
|
||||
)
|
||||
text += line + "\n"
|
||||
else:
|
||||
# Legacy fallback: vertical Header: Value list
|
||||
for device in jsn:
|
||||
for header in headers:
|
||||
padding = ""
|
||||
if len(header) < 4:
|
||||
padding = "\t"
|
||||
text += text_line.format(header + ": " + padding, device[header])
|
||||
text += "\n"
|
||||
|
||||
# Format HTML table headers
|
||||
for header in headers:
|
||||
|
||||
@@ -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