From f4c38625537a5c6a3bf542b24e786489e1b24cc2 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 8 Jan 2026 13:04:40 +0100 Subject: [PATCH] Added dashboard files --- .../etc_systemd_system_open5gs-logger.service | 11 + marius/opt_open5gs_dashboard-logger.py | 196 ++++++++++ marius/status.py | 357 ++++++++++++++++++ 3 files changed, 564 insertions(+) create mode 100644 marius/etc_systemd_system_open5gs-logger.service create mode 100755 marius/opt_open5gs_dashboard-logger.py create mode 100755 marius/status.py diff --git a/marius/etc_systemd_system_open5gs-logger.service b/marius/etc_systemd_system_open5gs-logger.service new file mode 100644 index 0000000..c9bda9b --- /dev/null +++ b/marius/etc_systemd_system_open5gs-logger.service @@ -0,0 +1,11 @@ +[Unit] +Description=Open5GS Dashboard Logger +After=network.target mongod.service + +[Service] +ExecStart=/usr/bin/python3 /opt/open5gs/dashboard-logger.py +Restart=always +User=root + +[Install] +WantedBy=multi-user.target diff --git a/marius/opt_open5gs_dashboard-logger.py b/marius/opt_open5gs_dashboard-logger.py new file mode 100755 index 0000000..00a4544 --- /dev/null +++ b/marius/opt_open5gs_dashboard-logger.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +import time +import subprocess +import re +import urllib.request +import pymongo +import os +from datetime import datetime + +# --- Config --- +MONGO_URI = "mongodb://127.0.0.1:27017/" +INTERFACE = "ogstun" +INTERVAL = 1 +LOG_FILE = "/var/log/open5gs/smf.log" + +# Global State +active_sessions = {} # { "10.45.0.2": "24288..." } + +def run_cmd(cmd): + subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + +def setup_iptables(): + run_cmd("iptables -N OGS_ACCT 2>/dev/null") + run_cmd(f"iptables -C FORWARD -i {INTERFACE} -j OGS_ACCT 2>/dev/null || iptables -I FORWARD -i {INTERFACE} -j OGS_ACCT") + run_cmd(f"iptables -C FORWARD -o {INTERFACE} -j OGS_ACCT 2>/dev/null || iptables -I FORWARD -o {INTERFACE} -j OGS_ACCT") + +def process_log_line(line): + global active_sessions + + # 1. Match Connection: SUPI[imsi-12345] ... IPv4[10.45.0.2] + m_add = re.search(r'SUPI\[(?:imsi-)?(\d+)\].*IPv4\[([0-9\.]+)\]', line) + if m_add: + imsi = m_add.group(1) + ip = m_add.group(2) + active_sessions[ip] = imsi + return + + # 2. Match Release + if "Release" in line and "SUPI" in line: + m_rel = re.search(r'SUPI\[(?:imsi-)?(\d+)\]', line) + if m_rel: + imsi_rel = m_rel.group(1) + ips_to_remove = [k for k,v in active_sessions.items() if v == imsi_rel] + for k in ips_to_remove: + del active_sessions[k] + +def sync_iptables_rules(): + try: + current = subprocess.check_output("iptables -L OGS_ACCT -n", shell=True).decode() + for ip in active_sessions.keys(): + if f" {ip} " not in current: + run_cmd(f"iptables -A OGS_ACCT -s {ip} -j RETURN") + run_cmd(f"iptables -A OGS_ACCT -d {ip} -j RETURN") + except: pass + +def read_traffic_counters(): + stats = {} + try: + out = subprocess.check_output("iptables -L OGS_ACCT -v -n -x", shell=True).decode() + for line in out.split('\n'): + parts = line.split() + if len(parts) < 8: continue + try: + bytes_count = int(parts[1]) + src, dst = parts[7], parts[8] + if src in active_sessions: + imsi = active_sessions[src] + if imsi not in stats: stats[imsi] = {"rx": 0, "tx": 0} + stats[imsi]["tx"] += bytes_count + if dst in active_sessions: + imsi = active_sessions[dst] + if imsi not in stats: stats[imsi] = {"rx": 0, "tx": 0} + stats[imsi]["rx"] += bytes_count + except: continue + except: pass + return stats + +def parse_metric(url, key): + total = 0 + try: + with urllib.request.urlopen(url, timeout=0.5) as r: + for line in r.read().decode().split('\n'): + if key in line and not line.startswith('#'): + try: total += int(float(line.split()[-1])) + except: pass + except: pass + return total + +def get_infra_counts(): + return { + "ue_4g": parse_metric("http://127.0.0.2:9090/metrics", "mme_session"), + "enb": parse_metric("http://127.0.0.2:9090/metrics", "enb"), + "ue_5g": parse_metric("http://127.0.0.5:9090/metrics", "fivegs_amffunction_rm_registeredsubnbr"), + "gnb": parse_metric("http://127.0.0.5:9090/metrics", "gnb") + } + +def main(): + client = pymongo.MongoClient(MONGO_URI) + db = client["open5gs_dashboard"] + history_col = db["history"] + traffic_col = db["imsi_traffic"] + + history_col.create_index("timestamp", expireAfterSeconds=604800) + setup_iptables() + + # Bootstrap from logs + print("Bootstrapping...") + try: + if os.path.exists(LOG_FILE): + cmd = f"tail -n 50000 {LOG_FILE}" + lines = subprocess.check_output(cmd, shell=True).decode('utf-8', errors='ignore').split('\n') + for line in lines: process_log_line(line) + except: pass + + f = open(LOG_FILE, 'r') + f.seek(0, 2) + cur_inode = os.fstat(f.fileno()).st_ino + last_run = time.time() + last_counters = {} + + print("Logger Running.") + while True: + start_time = time.time() + + # 1. Log Processing + while True: + line = f.readline() + if not line: break + process_log_line(line) + + # Log Rotation + try: + if os.stat(LOG_FILE).st_ino != cur_inode: + f.close(); f = open(LOG_FILE, 'r'); cur_inode = os.fstat(f.fileno()).st_ino + except: pass + + # 2. Sync + sync_iptables_rules() + current_counters = read_traffic_counters() + + # 3. Update DB with IP AND Traffic + # First, ensure all active IPs are recorded even if 0 traffic + for ip, imsi in active_sessions.items(): + data = current_counters.get(imsi, {"rx": 0, "tx": 0}) + traffic_col.update_one( + {"imsi": imsi}, + {"$set": { + "ip": ip, # <--- SAVING IP TO DB HERE + "status": "Connected", # <--- EXPLICIT STATUS + "total_rx": data["rx"], + "total_tx": data["tx"], + "last_seen": datetime.now() + }}, + upsert=True + ) + + # Mark disconnected users as Idle in DB (optional cleanup) + # We find anyone in DB who is NOT in active_sessions and set status=Idle + # This keeps the UI clean + all_active_imsis = list(active_sessions.values()) + if all_active_imsis: + traffic_col.update_many( + {"imsi": {"$nin": all_active_imsis}}, + {"$set": {"status": "Idle", "ip": "-"}} + ) + + # 4. History & Rates + time_diff = start_time - last_run + if time_diff < 0.1: time_diff = 0.1 + + imsi_rates = {} + glob_rx = 0; glob_tx = 0 + + for imsi, data in current_counters.items(): + if imsi in last_counters: + dr = (data["rx"] - last_counters[imsi]["rx"]) * 8 / time_diff + dt = (data["tx"] - last_counters[imsi]["tx"]) * 8 / time_diff + imsi_rates[imsi] = {"rx_bps": int(max(0, dr)), "tx_bps": int(max(0, dt))} + glob_rx += max(0, dr); glob_tx += max(0, dt) + + infra = get_infra_counts() + history_col.insert_one({ + "timestamp": datetime.now(), + "rx_bps": int(glob_rx), "tx_bps": int(glob_tx), + "ue_4g": infra["ue_4g"], "ue_5g": infra["ue_5g"], + "enb": infra["enb"], "gnb": infra["gnb"], + "streams": imsi_rates + }) + + last_counters = current_counters + last_run = start_time + elapsed = time.time() - start_time + time.sleep(max(0, INTERVAL - elapsed)) + +if __name__ == "__main__": + main() diff --git a/marius/status.py b/marius/status.py new file mode 100755 index 0000000..153c6cb --- /dev/null +++ b/marius/status.py @@ -0,0 +1,357 @@ +#!/usr/bin/env python3 +import cgitb +import cgi +import subprocess +import urllib.request +import re +import os +import sys +import pymongo +import json +from datetime import datetime +from pysnmp.hlapi import * # Enable Debugging +cgitb.enable() +sys.stdout.reconfigure(encoding='utf-8') + +MONGO_URI = "mongodb://127.0.0.1:27017/" +LOG_FILES = { + "SMF": "/var/log/open5gs/smf.log", + "AMF": "/var/log/open5gs/amf.log", + "MME": "/var/log/open5gs/mme.log" +} + +# --- CONFIGURATION --- +BTS_IP = "192.168.1.20" +BTS_COMMUNITY = "public" + +# Log Rules: Regex -> Human Message +TRIGGER_RULES = [ + {"pattern": r"Cannot find SUCI|Registration reject \[11\]", "msg": "⚠️ Unknown SIM: UE not in Database.", "solution": "Check IMSI in WebUI.", "color": "#ff9800", "prio": 2}, + {"pattern": r"No SMF Instance", "msg": "🚨 SMF Failure: AMF cannot contact SMF.", "solution": "Restart open5gs-smfd.", "color": "#ff5252", "prio": 1}, + {"pattern": r"Authentication failure|Authentication reject", "msg": "⛔ Auth Failed: Key (K) or OPc mismatch.", "solution": "Verify SIM secrets.", "color": "#ff5252", "prio": 1}, + {"pattern": r"HTTP response error \[(400|504)\]", "msg": "🔥 Core Error: HTTP 400/504.", "solution": "Check Disk/Restart Services.", "color": "#ff5252", "prio": 1}, + {"pattern": r"SCTP shutdown", "msg": "🔌 Radio Disconnect: eNB/gNB disconnected.", "solution": "Check radio connectivity.", "color": "#ff9800", "prio": 2}, + {"pattern": r"UE Context Release", "msg": "🔌 UE Context Release: No issue, UE going to idle", "solution": "none", "color": "#ff9800", "prio": 2} +] + +METRICS_ENDPOINTS = [ + {"name": "MME (4G)", "ip": "127.0.0.2", "port": 9090, "key": "mme_session", "proc": "open5gs-mmed"}, + {"name": "AMF (5G)", "ip": "127.0.0.5", "port": 9090, "key": "fivegs_amffunction_rm_registeredsubnbr", "proc": "open5gs-amfd"}, + {"name": "SMF (Sess)", "ip": "127.0.0.4", "port": 9090, "key": "fivegs_smffunction_sm_sessionnbr", "proc": "open5gs-smfd"}, + {"name": "UPF (Tun)", "ip": "127.0.0.7", "port": 9090, "key": "fivegs_upffunction_upf_sessionnbr", "proc": "open5gs-upfd"} +] + +# --- FUNCTIONS --- +def get_db(): return pymongo.MongoClient(MONGO_URI, serverSelectionTimeoutMS=500) + +def fmt_bytes(b): + if not b: return "0 B" + b = float(b) + if b < 1024: return f"{b:.0f} B" + if b < 1024**2: return f"{b/1024:.1f} KB" + if b < 1024**3: return f"{b/1024**2:.1f} MB" + return f"{b/1024**3:.2f} GB" + +def handle_post(form): + if "action" in form and form["action"].value == "rename": + client = get_db() + client["open5gs_dashboard"]["sim_names"].update_one( + {"imsi": form["imsi"].value}, + {"$set": {"name": form["nickname"].value}}, upsert=True) + rng = form.getvalue("current_range", "10m") + print(f"Content-Type: text/html\nLocation: status.py?range={rng}\n\n") + sys.exit() + +def get_history(time_range): + limit, step = 600, 1 + if time_range == "1h": limit, step = 3600, 6 + elif time_range == "24h": limit, step = 86400, 144 + elif time_range == "7d": limit, step = 604800, 1000 + client = get_db() + data = list(client["open5gs_dashboard"]["history"].find().sort("timestamp", -1).limit(limit)) + return list(reversed(data))[::step] + +def check_proc(name): + try: subprocess.check_output(f"pgrep -f {name}", shell=True); return True + except: return False + +def get_mongo_status(): + try: + client = get_db() + client.admin.command('ping') + return "UP" + except: return "DOWN" + +def get_process_list(): + try: + cmd = "ps -e -o cmd | grep -E 'open5gs|mongod' | grep -v grep | sort" + output = subprocess.check_output(cmd, shell=True).decode('utf-8') + processes = [] + for line in output.split('\n'): + if line.strip(): processes.append(line.split()[0].split('/')[-1]) + return processes + except: return [] + +def analyze_logs(): + alerts = [] + seen = set() + for name, filepath in LOG_FILES.items(): + if os.access(filepath, os.R_OK): + try: + cmd = f"tail -n 100 {filepath}" + out = subprocess.check_output(cmd, shell=True).decode('utf-8', errors='ignore') + for line in reversed(out.split('\n')): + for rule in TRIGGER_RULES: + if re.search(rule["pattern"], line): + # Dedup based on rule message + first 20 chars of log (timestamp) + key = rule["msg"] + line[:20] + if key not in seen: + alerts.append({ + "html": f'
{rule["msg"]}
{line[:120]}...
💡 {rule["solution"]}
', + "prio": rule["prio"] + }) + seen.add(key) + except: pass + alerts.sort(key=lambda x: x["prio"]) + return [a["html"] for a in alerts] + +def get_service_status(svc): + url = f"http://{svc['ip']}:{svc['port']}/metrics" + try: + proxy = urllib.request.ProxyHandler({}) + opener = urllib.request.build_opener(proxy) + with opener.open(urllib.request.Request(url), timeout=1.0) as r: + data = r.read().decode('utf-8') + total = 0.0 + found = False + for line in data.split('\n'): + if svc['key'] in line and not line.startswith('#'): + try: val = float(line.split()[-1]); total += val; found = True + except: pass + if found: return str(int(total)) + if check_proc(svc['proc']): return "Active (No Key)" + except: + if check_proc(svc['proc']): return "Active (No API)" + return "DOWN" + +def get_bts_snmp(): + data = {"status": "DOWN", "name": "Nokia BTS", "uptime": "-", "color": "#ff5252"} + try: + for oid_name, oid_val in [("name", "1.3.6.1.2.1.1.1.0"), ("uptime", "1.3.6.1.2.1.1.3.0")]: + errorIndication, errorStatus, errorIndex, varBinds = next( + getCmd(SnmpEngine(), CommunityData(BTS_COMMUNITY, mpModel=1), + UdpTransportTarget((BTS_IP, 161), timeout=0.5, retries=0), + ContextData(), ObjectType(ObjectIdentity(oid_val))) + ) + if not errorIndication and not errorStatus: + val = str(varBinds[0][1]) + data["status"] = "ONLINE"; data["color"] = "#00e676" + if oid_name == "name": + if "AirScale" in val: data["name"] = "Nokia AirScale" + elif "Flexi" in val: data["name"] = "Nokia Flexi" + else: data["name"] = val[:20] + if oid_name == "uptime": + seconds = int(val) / 100 + d = int(seconds // 86400); h = int((seconds % 86400) // 3600) + data["uptime"] = f"{d}d {h}h" + except: pass + return data + +def get_subs(): + client = get_db() + db_dash = client["open5gs_dashboard"] + subs = list(client["open5gs"]["subscribers"].find({}, {"imsi": 1, "security.k": 1})) + names = {d["imsi"]: d["name"] for d in db_dash["sim_names"].find()} + traffic = {d["imsi"]: d for d in db_dash["imsi_traffic"].find()} + final = [] + for s in subs: + imsi = s["imsi"] + traf_data = traffic.get(imsi, {}) + final.append({ + "imsi": imsi, "key": s.get("security", {}).get("k", "")[:6] + "...", + "name": names.get(imsi, ""), "ip": traf_data.get("ip", "-"), + "status": traf_data.get("status", "Idle"), + "rx": traf_data.get("total_rx", 0), "tx": traf_data.get("total_tx", 0) + }) + return final + +# --- RENDER --- +form = cgi.FieldStorage() +handle_post(form) +selected_range = form.getvalue("range", "10m") +history = get_history(selected_range) +subs = get_subs() +bts = get_bts_snmp() +log_alerts = analyze_logs() +procs = get_process_list() +mongo_stat = get_mongo_status() + +# Chart Data Prep +labels = [d["timestamp"].strftime("%H:%M:%S") if selected_range in ["10m","1h"] else d["timestamp"].strftime("%m-%d %H") for d in history] +tx_data = [d["tx_bps"] for d in history] +rx_data = [d["rx_bps"] for d in history] +gnb_data = [d.get("gnb", 0) for d in history] +enb_data = [d.get("enb", 0) for d in history] +ue_data = [d.get("ue_4g", 0) + d.get("ue_5g", 0) for d in history] +imsi_datasets = [] +imsi_names = {s["imsi"]: (s["name"] if s["name"] else s["imsi"]) for s in subs} +colors = ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40'] +temp = {s["imsi"]: [] for s in subs} +for rec in history: + streams = rec.get("streams", {}) + for imsi in temp.keys(): + val = streams[imsi].get("tx_bps", 0) + streams[imsi].get("rx_bps", 0) if imsi in streams else 0 + temp[imsi].append(val) +idx = 0 +for imsi, dp in temp.items(): + if max(dp) > 0: + imsi_datasets.append({"label": imsi_names.get(imsi, imsi), "data": dp, "borderColor": colors[idx%len(colors)], "borderWidth": 1, "pointRadius": 0, "tension": 0.4}) + idx += 1 + +print("Content-Type: text/html; charset=utf-8\n") +print(f""" +Open5GS Dashboard + + + + + +
+
+

Open5GS Dashboard

+ 10m + 1h + 24h + 7d +
+ 📊 Advanced Metrics +
+ +
+
+

Hardware Status

+
+
{bts['status']} {bts['name']}
+
{bts['uptime']}
+
+
+
MongoDB{mongo_stat}
+
+ {''.join([f'
✓ {p}
' for p in procs])} +
+
+
+ +
+

Core Services

+
+""") +for svc in METRICS_ENDPOINTS: + val = get_service_status(svc) + cls = "val-up" + if val == "DOWN": cls = "val-down" + elif "Active" in val: cls = "val-warn" + print(f'
{svc["name"]}{val}
') +print(f""" +
+
+ +
+

Smart Log Alerts

+
+ {''.join(log_alerts) if log_alerts else '
No Critical Issues Detected
'} +
+
+
+ +
+

Network Throughput

+

Infrastructure

+

User Throughput

+
+ +

Subscribers

+""") +for s in subs: + st = "conn" if s["status"]=="Connected" else "idle" + print(f""" + + + + + + """) +print(f"""
IMSINameIPDownUpStatusSave
{s['imsi']}
{s['ip']}⬇ {fmt_bytes(s['rx'])}⬆ {fmt_bytes(s['tx'])}{s['status']}
+""")