mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2025-12-07 09:36:05 -08:00
api layer v0.2.3 - /device(s) endpoints work
This commit is contained in:
@@ -3,7 +3,7 @@ from flask import Flask, request, jsonify, Response
|
|||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
from .graphql_endpoint import devicesSchema
|
from .graphql_endpoint import devicesSchema
|
||||||
from .device_endpoint import get_device_data, set_device_data, delete_device, delete_device_events, reset_device_props, copy_device, update_device_column
|
from .device_endpoint import get_device_data, set_device_data, delete_device, delete_device_events, reset_device_props, copy_device, update_device_column
|
||||||
from .devices_endpoint import delete_unknown_devices, delete_all_with_empty_macs, delete_devices, export_devices, import_csv
|
from .devices_endpoint import delete_unknown_devices, delete_all_with_empty_macs, delete_devices, export_devices, import_csv, devices_totals, devices_by_status
|
||||||
from .events_endpoint import delete_events, delete_events_30, get_events
|
from .events_endpoint import delete_events, delete_events_30, get_events
|
||||||
from .history_endpoint import delete_online_history
|
from .history_endpoint import delete_online_history
|
||||||
from .prometheus_endpoint import getMetricStats
|
from .prometheus_endpoint import getMetricStats
|
||||||
@@ -150,12 +150,6 @@ def api_delete_unknown_devices():
|
|||||||
return jsonify({"error": "Forbidden"}), 403
|
return jsonify({"error": "Forbidden"}), 403
|
||||||
return delete_unknown_devices()
|
return delete_unknown_devices()
|
||||||
|
|
||||||
@app.route("/devices/totals", methods=["GET"])
|
|
||||||
def api_get_devices_totals():
|
|
||||||
if not is_authorized():
|
|
||||||
return jsonify({"error": "Forbidden"}), 403
|
|
||||||
return get_devices_totals()
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/devices/export", methods=["GET"])
|
@app.route("/devices/export", methods=["GET"])
|
||||||
@app.route("/devices/export/<format>", methods=["GET"])
|
@app.route("/devices/export/<format>", methods=["GET"])
|
||||||
@@ -172,6 +166,21 @@ def api_import_csv():
|
|||||||
return jsonify({"error": "Forbidden"}), 403
|
return jsonify({"error": "Forbidden"}), 403
|
||||||
return import_csv(request.files.get("file"))
|
return import_csv(request.files.get("file"))
|
||||||
|
|
||||||
|
@app.route("/devices/totals", methods=["GET"])
|
||||||
|
def api_devices_totals():
|
||||||
|
if not is_authorized():
|
||||||
|
return jsonify({"error": "Forbidden"}), 403
|
||||||
|
return devices_totals()
|
||||||
|
|
||||||
|
@app.route("/devices/by-status", methods=["GET"])
|
||||||
|
def api_devices_by_status():
|
||||||
|
if not is_authorized():
|
||||||
|
return jsonify({"error": "Forbidden"}), 403
|
||||||
|
|
||||||
|
status = request.args.get("status", "") if request.args else None
|
||||||
|
|
||||||
|
return devices_by_status(status)
|
||||||
|
|
||||||
# --------------------------
|
# --------------------------
|
||||||
# Online history
|
# Online history
|
||||||
# --------------------------
|
# --------------------------
|
||||||
|
|||||||
@@ -161,37 +161,39 @@ def set_device_data(mac, data):
|
|||||||
devSourcePlugin
|
devSourcePlugin
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
values = (
|
values = (
|
||||||
mac,
|
mac,
|
||||||
data.get("name", ""),
|
data.get("devName", ""),
|
||||||
data.get("owner", ""),
|
data.get("devOwner", ""),
|
||||||
data.get("type", ""),
|
data.get("devType", ""),
|
||||||
data.get("vendor", ""),
|
data.get("devVendor", ""),
|
||||||
data.get("icon", ""),
|
data.get("devIcon", ""),
|
||||||
data.get("favorite", 0),
|
data.get("devFavorite", 0),
|
||||||
data.get("group", ""),
|
data.get("devGroup", ""),
|
||||||
data.get("location", ""),
|
data.get("devLocation", ""),
|
||||||
data.get("comments", ""),
|
data.get("devComments", ""),
|
||||||
data.get("networknode", ""),
|
data.get("devParentMAC", ""),
|
||||||
data.get("networknodeport", ""),
|
data.get("devParentPort", ""),
|
||||||
data.get("ssid", ""),
|
data.get("devSSID", ""),
|
||||||
data.get("networksite", ""),
|
data.get("devSite", ""),
|
||||||
data.get("staticIP", 0),
|
data.get("devStaticIP", 0),
|
||||||
data.get("scancycle", 0),
|
data.get("devScan", 0),
|
||||||
data.get("alertevents", 0),
|
data.get("devAlertEvents", 0),
|
||||||
data.get("alertdown", 0),
|
data.get("devAlertDown", 0),
|
||||||
data.get("relType", "default"),
|
data.get("devParentRelType", "default"),
|
||||||
data.get("reqNics", 0),
|
data.get("devReqNicsOnline", 0),
|
||||||
data.get("skiprepeated", 0),
|
data.get("devSkipRepeated", 0),
|
||||||
data.get("newdevice", 0),
|
data.get("devIsNew", 0),
|
||||||
data.get("archived", 0),
|
data.get("devIsArchived", 0),
|
||||||
data.get("devLastConnection", datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
|
data.get("devLastConnection", datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
|
||||||
data.get("devFirstConnection", datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
|
data.get("devFirstConnection", datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
|
||||||
data.get("ip", ""),
|
data.get("devLastIP", ""),
|
||||||
data.get("devGUID", ""),
|
data.get("devGUID", ""),
|
||||||
data.get("devCustomProps", ""),
|
data.get("devCustomProps", ""),
|
||||||
"DUMMY"
|
data.get("devSourcePlugin", "DUMMY"),
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
sql = """
|
sql = """
|
||||||
UPDATE Devices SET
|
UPDATE Devices SET
|
||||||
@@ -204,31 +206,31 @@ def set_device_data(mac, data):
|
|||||||
WHERE devMac=?
|
WHERE devMac=?
|
||||||
"""
|
"""
|
||||||
values = (
|
values = (
|
||||||
data.get("name", ""),
|
data.get("devName", ""),
|
||||||
data.get("owner", ""),
|
data.get("devOwner", ""),
|
||||||
data.get("type", ""),
|
data.get("devType", ""),
|
||||||
data.get("vendor", ""),
|
data.get("devVendor", ""),
|
||||||
data.get("icon", ""),
|
data.get("devIcon", ""),
|
||||||
data.get("favorite", 0),
|
data.get("devFavorite", 0),
|
||||||
data.get("group", ""),
|
data.get("devGroup", ""),
|
||||||
data.get("location", ""),
|
data.get("devLocation", ""),
|
||||||
data.get("comments", ""),
|
data.get("devComments", ""),
|
||||||
data.get("networknode", ""),
|
data.get("devParentMAC", ""),
|
||||||
data.get("networknodeport", ""),
|
data.get("devParentPort", ""),
|
||||||
data.get("ssid", ""),
|
data.get("devSSID", ""),
|
||||||
data.get("networksite", ""),
|
data.get("devSite", ""),
|
||||||
data.get("staticIP", 0),
|
data.get("devStaticIP", 0),
|
||||||
data.get("scancycle", 0),
|
data.get("devScan", 0),
|
||||||
data.get("alertevents", 0),
|
data.get("devAlertEvents", 0),
|
||||||
data.get("alertdown", 0),
|
data.get("devAlertDown", 0),
|
||||||
data.get("relType", "default"),
|
data.get("devParentRelType", "default"),
|
||||||
data.get("reqNics", 0),
|
data.get("devReqNicsOnline", 0),
|
||||||
data.get("skiprepeated", 0),
|
data.get("devSkipRepeated", 0),
|
||||||
data.get("newdevice", 0),
|
data.get("devIsNew", 0),
|
||||||
data.get("archived", 0),
|
data.get("devIsArchived", 0),
|
||||||
data.get("devCustomProps", ""),
|
data.get("devCustomProps", ""),
|
||||||
mac
|
mac
|
||||||
)
|
)
|
||||||
|
|
||||||
conn = get_temp_db_connection()
|
conn = get_temp_db_connection()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
|
|||||||
|
|
||||||
from database import get_temp_db_connection
|
from database import get_temp_db_connection
|
||||||
from helper import is_random_mac, format_date, get_setting_value
|
from helper import is_random_mac, format_date, get_setting_value
|
||||||
from db.db_helper import get_table_json
|
from db.db_helper import get_table_json, get_device_condition_by_status
|
||||||
|
|
||||||
|
|
||||||
# --------------------------
|
# --------------------------
|
||||||
@@ -193,3 +193,57 @@ def import_csv(file_storage=None):
|
|||||||
"inserted": row_count,
|
"inserted": row_count,
|
||||||
"skipped_lines": skipped
|
"skipped_lines": skipped
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def devices_totals():
|
||||||
|
conn = get_temp_db_connection()
|
||||||
|
sql = conn.cursor()
|
||||||
|
|
||||||
|
# Build a combined query with sub-selects for each status
|
||||||
|
query = f"""
|
||||||
|
SELECT
|
||||||
|
(SELECT COUNT(*) FROM Devices {get_device_condition_by_status('my')}) AS devices,
|
||||||
|
(SELECT COUNT(*) FROM Devices {get_device_condition_by_status('connected')}) AS connected,
|
||||||
|
(SELECT COUNT(*) FROM Devices {get_device_condition_by_status('favorites')}) AS favorites,
|
||||||
|
(SELECT COUNT(*) FROM Devices {get_device_condition_by_status('new')}) AS new,
|
||||||
|
(SELECT COUNT(*) FROM Devices {get_device_condition_by_status('down')}) AS down,
|
||||||
|
(SELECT COUNT(*) FROM Devices {get_device_condition_by_status('archived')}) AS archived
|
||||||
|
"""
|
||||||
|
sql.execute(query)
|
||||||
|
row = sql.fetchone() # returns a tuple like (devices, connected, favorites, new, down, archived)
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Return counts as JSON array
|
||||||
|
return jsonify(list(row))
|
||||||
|
|
||||||
|
|
||||||
|
def devices_by_status(status=None):
|
||||||
|
"""
|
||||||
|
Return devices filtered by status.
|
||||||
|
"""
|
||||||
|
|
||||||
|
conn = get_temp_db_connection()
|
||||||
|
sql = conn.cursor()
|
||||||
|
|
||||||
|
# Build condition for SQL
|
||||||
|
condition = get_device_condition_by_status(status) if status else ""
|
||||||
|
|
||||||
|
query = f"SELECT * FROM Devices {condition}"
|
||||||
|
sql.execute(query)
|
||||||
|
|
||||||
|
table_data = []
|
||||||
|
for row in sql.fetchall():
|
||||||
|
r = dict(row) # Convert sqlite3.Row to dict for .get()
|
||||||
|
dev_name = r.get("devName", "")
|
||||||
|
if r.get("devFavorite") == 1:
|
||||||
|
dev_name = f'<span class="text-yellow">★</span> {dev_name}'
|
||||||
|
|
||||||
|
table_data.append({
|
||||||
|
"id": r.get("devMac", ""),
|
||||||
|
"title": dev_name,
|
||||||
|
"favorite": r.get("devFavorite", 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
return jsonify(table_data)
|
||||||
|
|
||||||
|
|||||||
@@ -32,10 +32,10 @@ def auth_headers(token):
|
|||||||
def test_create_device(client, api_token, test_mac):
|
def test_create_device(client, api_token, test_mac):
|
||||||
payload = {
|
payload = {
|
||||||
"createNew": True,
|
"createNew": True,
|
||||||
"name": "Test Device",
|
"devType": "Test Device",
|
||||||
"owner": "Unit Test",
|
"devOwner": "Unit Test",
|
||||||
"type": "Router",
|
"devType": "Router",
|
||||||
"vendor": "TestVendor",
|
"devVendor": "TestVendor",
|
||||||
}
|
}
|
||||||
resp = client.post(f"/device/{test_mac}", json=payload, headers=auth_headers(api_token))
|
resp = client.post(f"/device/{test_mac}", json=payload, headers=auth_headers(api_token))
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
@@ -69,7 +69,7 @@ def test_delete_device(client, api_token, test_mac):
|
|||||||
|
|
||||||
def test_copy_device(client, api_token, test_mac):
|
def test_copy_device(client, api_token, test_mac):
|
||||||
# Step 1: Create the source device
|
# Step 1: Create the source device
|
||||||
payload = {"createNew": True, "name": "Source Device"}
|
payload = {"createNew": True}
|
||||||
resp = client.post(f"/device/{test_mac}", json=payload, headers=auth_headers(api_token))
|
resp = client.post(f"/device/{test_mac}", json=payload, headers=auth_headers(api_token))
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert resp.json.get("success") is True
|
assert resp.json.get("success") is True
|
||||||
|
|||||||
@@ -34,10 +34,10 @@ def auth_headers(token):
|
|||||||
def create_dummy(client, api_token, test_mac):
|
def create_dummy(client, api_token, test_mac):
|
||||||
payload = {
|
payload = {
|
||||||
"createNew": True,
|
"createNew": True,
|
||||||
"name": "Test Device",
|
"devName": "Test Device",
|
||||||
"owner": "Unit Test",
|
"devOwner": "Unit Test",
|
||||||
"type": "Router",
|
"devType": "Router",
|
||||||
"vendor": "TestVendor",
|
"devVendor": "TestVendor",
|
||||||
}
|
}
|
||||||
resp = client.post(f"/device/{test_mac}", json=payload, headers=auth_headers(api_token))
|
resp = client.post(f"/device/{test_mac}", json=payload, headers=auth_headers(api_token))
|
||||||
|
|
||||||
@@ -105,6 +105,8 @@ def test_export_import_cycle_base64(client, api_token, test_mac):
|
|||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
csv_data = resp.data.decode("utf-8")
|
csv_data = resp.data.decode("utf-8")
|
||||||
|
|
||||||
|
print(csv_data)
|
||||||
|
|
||||||
# Ensure our dummy device is in the CSV
|
# Ensure our dummy device is in the CSV
|
||||||
assert test_mac in csv_data
|
assert test_mac in csv_data
|
||||||
assert "Test Device" in csv_data
|
assert "Test Device" in csv_data
|
||||||
@@ -126,6 +128,51 @@ def test_export_import_cycle_base64(client, api_token, test_mac):
|
|||||||
assert resp.json.get("inserted") >= 1
|
assert resp.json.get("inserted") >= 1
|
||||||
assert resp.json.get("skipped_lines") == []
|
assert resp.json.get("skipped_lines") == []
|
||||||
|
|
||||||
|
def test_devices_totals(client, api_token, test_mac):
|
||||||
|
# 1. Create a dummy device
|
||||||
|
create_dummy(client, api_token, test_mac)
|
||||||
|
|
||||||
|
# 2. Call the totals endpoint
|
||||||
|
resp = client.get("/devices/totals", headers=auth_headers(api_token))
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
# 3. Ensure the response is a JSON list
|
||||||
|
data = resp.json
|
||||||
|
assert isinstance(data, list)
|
||||||
|
assert len(data) == 6 # devices, connected, favorites, new, down, archived
|
||||||
|
|
||||||
|
# 4. Check that at least 1 device exists
|
||||||
|
assert data[0] >= 1 # 'devices' count includes the dummy device
|
||||||
|
|
||||||
|
|
||||||
|
def test_devices_by_status(client, api_token, test_mac):
|
||||||
|
# 1. Create a dummy device
|
||||||
|
create_dummy(client, api_token, test_mac)
|
||||||
|
|
||||||
|
# 2. Request devices by a valid status
|
||||||
|
resp = client.get("/devices/by-status?status=my", headers=auth_headers(api_token))
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json
|
||||||
|
assert isinstance(data, list)
|
||||||
|
assert any(d["id"] == test_mac for d in data)
|
||||||
|
|
||||||
|
# 3. Request devices with an invalid/unknown status
|
||||||
|
resp_invalid = client.get("/devices/by-status?status=invalid_status", headers=auth_headers(api_token))
|
||||||
|
assert resp_invalid.status_code == 200
|
||||||
|
# Should return empty list for unknown status
|
||||||
|
assert resp_invalid.json == []
|
||||||
|
|
||||||
|
# 4. Check favorite formatting if devFavorite = 1
|
||||||
|
# Update dummy device to favorite
|
||||||
|
client.post(
|
||||||
|
f"/device/{test_mac}",
|
||||||
|
json={"devFavorite": 1},
|
||||||
|
headers=auth_headers(api_token)
|
||||||
|
)
|
||||||
|
resp_fav = client.get("/devices/by-status?status=my", headers=auth_headers(api_token))
|
||||||
|
fav_data = next((d for d in resp_fav.json if d["id"] == test_mac), None)
|
||||||
|
assert fav_data is not None
|
||||||
|
assert "★" in fav_data["title"]
|
||||||
|
|
||||||
def test_delete_test_devices(client, api_token, test_mac):
|
def test_delete_test_devices(client, api_token, test_mac):
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user