initial commit
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,310 @@
|
|||||||
|
from flask import Flask, render_template, request, jsonify, redirect, url_for
|
||||||
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
|
from datetime import datetime
|
||||||
|
import subprocess
|
||||||
|
import re
|
||||||
|
import database as db
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# Modem configuration profiles
|
||||||
|
MODEM_PROFILES = {
|
||||||
|
'rv50': {
|
||||||
|
'oid_sent': '.1.3.6.1.4.1.20542.9.1.1.2.284.0',
|
||||||
|
'oid_recv': '.1.3.6.1.4.1.20542.9.1.1.2.283.0',
|
||||||
|
'type': 'counter64'
|
||||||
|
},
|
||||||
|
'bullet-lte': {
|
||||||
|
'oid_sent': '.1.3.6.1.4.1.21703.6030.9.4.3.0',
|
||||||
|
'oid_recv': '.1.3.6.1.4.1.21703.6030.9.4.1.0',
|
||||||
|
'type': 'displaystring'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Store last readings for rate calculation
|
||||||
|
last_readings = {}
|
||||||
|
|
||||||
|
scheduler = BackgroundScheduler()
|
||||||
|
scheduler.start()
|
||||||
|
|
||||||
|
def format_bytes(bytes_value):
|
||||||
|
"""Convert bytes to human-readable format"""
|
||||||
|
if bytes_value is None or bytes_value == 0:
|
||||||
|
return "N/A", 0
|
||||||
|
|
||||||
|
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
||||||
|
if abs(bytes_value) < 1024.0:
|
||||||
|
return f"{bytes_value:.2f} {unit}", bytes_value
|
||||||
|
bytes_value /= 1024.0
|
||||||
|
return f"{bytes_value:.2f} PB", bytes_value
|
||||||
|
|
||||||
|
def parse_snmp_value(value, data_type):
|
||||||
|
"""Parse SNMP response based on data type"""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if data_type == 'counter64':
|
||||||
|
# Counter64: "Counter64: 1234567890"
|
||||||
|
match = re.search(r'Counter64:\s*(\d+)', value)
|
||||||
|
if match:
|
||||||
|
return int(match.group(1))
|
||||||
|
|
||||||
|
elif data_type == 'displaystring':
|
||||||
|
# DisplayString: "STRING: 1234567890" or just the number
|
||||||
|
match = re.search(r'STRING:\s*"?([^"]+)"?', value)
|
||||||
|
if match:
|
||||||
|
str_value = match.group(1).strip()
|
||||||
|
# Remove any non-digit characters (commas, spaces, etc.)
|
||||||
|
digits = re.sub(r'[^\d]', '', str_value)
|
||||||
|
if digits:
|
||||||
|
return int(digits)
|
||||||
|
# Fallback: try to find any number in the output
|
||||||
|
digits = re.sub(r'[^\d]', '', value)
|
||||||
|
if digits:
|
||||||
|
return int(digits)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def snmp_get(ip, port, community, oid):
|
||||||
|
"""Execute snmpget command and return raw output"""
|
||||||
|
try:
|
||||||
|
cmd = [
|
||||||
|
'snmpget', '-v', '2c', '-c', community,
|
||||||
|
'-t', '5', '-r', '2',
|
||||||
|
f'{ip}:{port}', oid
|
||||||
|
]
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
return result.stdout.strip()
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"SNMP error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def poll_target(target_id, ip, port, community, modem_type):
|
||||||
|
"""Poll a target and store the reading"""
|
||||||
|
global last_readings
|
||||||
|
|
||||||
|
profile = MODEM_PROFILES.get(modem_type, MODEM_PROFILES['rv50'])
|
||||||
|
data_type = profile['type']
|
||||||
|
|
||||||
|
# Get raw SNMP responses
|
||||||
|
raw_sent = snmp_get(ip, port, community, profile['oid_sent'])
|
||||||
|
raw_recv = snmp_get(ip, port, community, profile['oid_recv'])
|
||||||
|
|
||||||
|
# Parse values based on data type
|
||||||
|
bytes_sent = parse_snmp_value(raw_sent, data_type)
|
||||||
|
bytes_recv = parse_snmp_value(raw_recv, data_type)
|
||||||
|
|
||||||
|
if bytes_sent is not None and bytes_recv is not None:
|
||||||
|
db.add_reading(target_id, bytes_sent, bytes_recv)
|
||||||
|
|
||||||
|
# Calculate rates (bytes per second)
|
||||||
|
rate_sent = 0
|
||||||
|
rate_recv = 0
|
||||||
|
|
||||||
|
if target_id in last_readings:
|
||||||
|
last_sent, last_recv, last_time = last_readings[target_id]
|
||||||
|
time_diff = (datetime.now() - last_time).total_seconds()
|
||||||
|
if time_diff > 0 and bytes_sent >= last_sent and bytes_recv >= last_recv:
|
||||||
|
rate_sent = (bytes_sent - last_sent) / time_diff
|
||||||
|
rate_recv = (bytes_recv - last_recv) / time_diff
|
||||||
|
|
||||||
|
last_readings[target_id] = (bytes_sent, bytes_recv, datetime.now())
|
||||||
|
|
||||||
|
return {
|
||||||
|
'bytes_sent': bytes_sent,
|
||||||
|
'bytes_recv': bytes_recv,
|
||||||
|
'rate_sent': rate_sent,
|
||||||
|
'rate_recv': rate_recv,
|
||||||
|
'success': True
|
||||||
|
}
|
||||||
|
|
||||||
|
return {'success': False, 'error': 'Failed to parse SNMP response'}
|
||||||
|
|
||||||
|
def scheduled_poll():
|
||||||
|
"""Background job to poll all enabled targets"""
|
||||||
|
targets = db.get_all_targets()
|
||||||
|
for target in targets:
|
||||||
|
if target['enabled']:
|
||||||
|
poll_target(
|
||||||
|
target['id'],
|
||||||
|
target['ip_address'],
|
||||||
|
target['port'],
|
||||||
|
target['community'],
|
||||||
|
target['modem_type']
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
targets = db.get_all_targets()
|
||||||
|
target_data = []
|
||||||
|
|
||||||
|
for target in targets:
|
||||||
|
latest = db.get_latest_reading(target['id'])
|
||||||
|
|
||||||
|
if latest:
|
||||||
|
sent_formatted, _ = format_bytes(latest['bytes_sent'])
|
||||||
|
recv_formatted, _ = format_bytes(latest['bytes_recv'])
|
||||||
|
|
||||||
|
# Calculate rate if we have previous reading
|
||||||
|
rate_sent = 0
|
||||||
|
rate_recv = 0
|
||||||
|
if target['id'] in last_readings:
|
||||||
|
last_sent, last_recv, last_time = last_readings[target['id']]
|
||||||
|
time_diff = (datetime.now() - last_time).total_seconds()
|
||||||
|
if time_diff > 0 and latest['bytes_sent'] >= last_sent:
|
||||||
|
rate_sent = (latest['bytes_sent'] - last_sent) / time_diff
|
||||||
|
rate_recv = (latest['bytes_recv'] - last_recv) / time_diff
|
||||||
|
|
||||||
|
rate_sent_fmt, _ = format_bytes(rate_sent)
|
||||||
|
rate_recv_fmt, _ = format_bytes(rate_recv)
|
||||||
|
else:
|
||||||
|
sent_formatted = recv_formatted = rate_sent_fmt = rate_recv_fmt = "N/A"
|
||||||
|
|
||||||
|
target_data.append({
|
||||||
|
**target,
|
||||||
|
'bytes_sent_fmt': sent_formatted,
|
||||||
|
'bytes_recv_fmt': recv_formatted,
|
||||||
|
'rate_sent_fmt': f"{rate_sent_fmt}/s",
|
||||||
|
'rate_recv_fmt': f"{rate_recv_fmt}/s",
|
||||||
|
'last_poll': latest['timestamp'] if latest else None
|
||||||
|
})
|
||||||
|
|
||||||
|
return render_template('index.html', targets=target_data)
|
||||||
|
|
||||||
|
@app.route('/add', methods=['POST'])
|
||||||
|
def add_target():
|
||||||
|
name = request.form.get('name', 'Unnamed')
|
||||||
|
ip_address = request.form.get('ip_address')
|
||||||
|
port = int(request.form.get('port', 161))
|
||||||
|
community = request.form.get('community', 'public')
|
||||||
|
period_minutes = int(request.form.get('period_minutes', 5))
|
||||||
|
modem_type = request.form.get('modem_type', 'rv50')
|
||||||
|
|
||||||
|
if ip_address:
|
||||||
|
target_id = db.add_target(name, ip_address, port, community, period_minutes, modem_type)
|
||||||
|
|
||||||
|
scheduler.add_job(
|
||||||
|
scheduled_poll,
|
||||||
|
'interval',
|
||||||
|
minutes=period_minutes,
|
||||||
|
id=f'target_{target_id}',
|
||||||
|
replace_existing=True
|
||||||
|
)
|
||||||
|
|
||||||
|
poll_target(target_id, ip_address, port, community, modem_type)
|
||||||
|
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
@app.route('/edit/<int:target_id>', methods=['POST'])
|
||||||
|
def edit_target(target_id):
|
||||||
|
name = request.form.get('name', 'Unnamed')
|
||||||
|
ip_address = request.form.get('ip_address')
|
||||||
|
port = int(request.form.get('port', 161))
|
||||||
|
community = request.form.get('community', 'public')
|
||||||
|
period_minutes = int(request.form.get('period_minutes', 5))
|
||||||
|
enabled = 1 if request.form.get('enabled') else 0
|
||||||
|
modem_type = request.form.get('modem_type', 'rv50')
|
||||||
|
|
||||||
|
db.update_target(target_id, name, ip_address, port, community, period_minutes, enabled, modem_type)
|
||||||
|
|
||||||
|
if enabled:
|
||||||
|
scheduler.add_job(
|
||||||
|
scheduled_poll,
|
||||||
|
'interval',
|
||||||
|
minutes=period_minutes,
|
||||||
|
id=f'target_{target_id}',
|
||||||
|
replace_existing=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
scheduler.remove_job(f'target_{target_id}')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
@app.route('/delete/<int:target_id>', methods=['POST'])
|
||||||
|
def delete_target(target_id):
|
||||||
|
try:
|
||||||
|
scheduler.remove_job(f'target_{target_id}')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
db.delete_target(target_id)
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
@app.route('/poll/<int:target_id>', methods=['POST'])
|
||||||
|
def manual_poll(target_id):
|
||||||
|
target = db.get_target(target_id)
|
||||||
|
if target:
|
||||||
|
result = poll_target(
|
||||||
|
target['id'],
|
||||||
|
target['ip_address'],
|
||||||
|
target['port'],
|
||||||
|
target['community'],
|
||||||
|
target['modem_type']
|
||||||
|
)
|
||||||
|
return jsonify(result)
|
||||||
|
return jsonify({'success': False, 'error': 'Target not found'}), 404
|
||||||
|
|
||||||
|
@app.route('/graph/<int:target_id>')
|
||||||
|
def graph(target_id):
|
||||||
|
target = db.get_target(target_id)
|
||||||
|
hours = request.args.get('hours', 24, type=int)
|
||||||
|
readings = db.get_readings(target_id, hours)
|
||||||
|
|
||||||
|
labels = []
|
||||||
|
sent_data = []
|
||||||
|
recv_data = []
|
||||||
|
|
||||||
|
for r in readings:
|
||||||
|
labels.append(r['timestamp'])
|
||||||
|
sent_data.append(r['bytes_sent'])
|
||||||
|
recv_data.append(r['bytes_recv'])
|
||||||
|
|
||||||
|
return render_template('graph.html',
|
||||||
|
target=target,
|
||||||
|
labels=labels,
|
||||||
|
sent_data=sent_data,
|
||||||
|
recv_data=recv_data,
|
||||||
|
hours=hours)
|
||||||
|
|
||||||
|
@app.route('/api/readings/<int:target_id>')
|
||||||
|
def api_readings(target_id):
|
||||||
|
hours = request.args.get('hours', 24, type=int)
|
||||||
|
readings = db.get_readings(target_id, hours)
|
||||||
|
|
||||||
|
data = []
|
||||||
|
for r in readings:
|
||||||
|
data.append({
|
||||||
|
'timestamp': r['timestamp'],
|
||||||
|
'bytes_sent': r['bytes_sent'],
|
||||||
|
'bytes_recv': r['bytes_recv']
|
||||||
|
})
|
||||||
|
|
||||||
|
return jsonify(data)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
db.init_db()
|
||||||
|
|
||||||
|
targets = db.get_all_targets()
|
||||||
|
for target in targets:
|
||||||
|
if target['enabled']:
|
||||||
|
scheduler.add_job(
|
||||||
|
scheduled_poll,
|
||||||
|
'interval',
|
||||||
|
minutes=target['period_minutes'],
|
||||||
|
id=f'target_{target["id"]}',
|
||||||
|
replace_existing=True
|
||||||
|
)
|
||||||
|
poll_target(
|
||||||
|
target['id'],
|
||||||
|
target['ip_address'],
|
||||||
|
target['port'],
|
||||||
|
target['community'],
|
||||||
|
target['modem_type']
|
||||||
|
)
|
||||||
|
|
||||||
|
app.run(debug=True, host='0.0.0.0', port=5500)
|
||||||
+132
@@ -0,0 +1,132 @@
|
|||||||
|
import sqlite3
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
DB_PATH = 'snmp_monitor.db'
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
c = conn.cursor()
|
||||||
|
|
||||||
|
# Targets table (devices to monitor)
|
||||||
|
c.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS targets (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT,
|
||||||
|
ip_address TEXT NOT NULL,
|
||||||
|
port INTEGER DEFAULT 161,
|
||||||
|
community TEXT NOT NULL,
|
||||||
|
period_minutes INTEGER DEFAULT 5,
|
||||||
|
modem_type TEXT DEFAULT 'rv50',
|
||||||
|
enabled INTEGER DEFAULT 1,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Readings table (stored SNMP data)
|
||||||
|
c.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS readings (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
target_id INTEGER NOT NULL,
|
||||||
|
bytes_sent INTEGER,
|
||||||
|
bytes_recv INTEGER,
|
||||||
|
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (target_id) REFERENCES targets(id)
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Check if modem_type column exists, add it if not (for existing databases)
|
||||||
|
try:
|
||||||
|
c.execute('SELECT modem_type FROM targets LIMIT 1')
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
c.execute('ALTER TABLE targets ADD COLUMN modem_type TEXT DEFAULT "rv50"')
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def add_target(name, ip_address, port, community, period_minutes, modem_type='rv50'):
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute('''
|
||||||
|
INSERT INTO targets (name, ip_address, port, community, period_minutes, modem_type)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
''', (name, ip_address, port, community, period_minutes, modem_type))
|
||||||
|
conn.commit()
|
||||||
|
target_id = c.lastrowid
|
||||||
|
conn.close()
|
||||||
|
return target_id
|
||||||
|
|
||||||
|
def get_all_targets():
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute('SELECT * FROM targets ORDER BY id')
|
||||||
|
targets = c.fetchall()
|
||||||
|
conn.close()
|
||||||
|
return targets
|
||||||
|
|
||||||
|
def get_target(target_id):
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute('SELECT * FROM targets WHERE id = ?', (target_id,))
|
||||||
|
target = c.fetchone()
|
||||||
|
conn.close()
|
||||||
|
return target
|
||||||
|
|
||||||
|
def update_target(target_id, name, ip_address, port, community, period_minutes, enabled, modem_type='rv50'):
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute('''
|
||||||
|
UPDATE targets
|
||||||
|
SET name=?, ip_address=?, port=?, community=?, period_minutes=?, enabled=?, modem_type=?
|
||||||
|
WHERE id=?
|
||||||
|
''', (name, ip_address, port, community, period_minutes, enabled, modem_type, target_id))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def delete_target(target_id):
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute('DELETE FROM readings WHERE target_id = ?', (target_id,))
|
||||||
|
c.execute('DELETE FROM targets WHERE id = ?', (target_id,))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def add_reading(target_id, bytes_sent, bytes_recv):
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute('''
|
||||||
|
INSERT INTO readings (target_id, bytes_sent, bytes_recv)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
''', (target_id, bytes_sent, bytes_recv))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def get_readings(target_id, hours=24):
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute('''
|
||||||
|
SELECT * FROM readings
|
||||||
|
WHERE target_id = ?
|
||||||
|
AND timestamp >= datetime('now', '-{} hours')
|
||||||
|
ORDER BY timestamp
|
||||||
|
'''.format(hours), (target_id,))
|
||||||
|
readings = c.fetchall()
|
||||||
|
conn.close()
|
||||||
|
return readings
|
||||||
|
|
||||||
|
def get_latest_reading(target_id):
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute('''
|
||||||
|
SELECT * FROM readings
|
||||||
|
WHERE target_id = ?
|
||||||
|
ORDER BY timestamp DESC
|
||||||
|
LIMIT 1
|
||||||
|
''', (target_id,))
|
||||||
|
reading = c.fetchone()
|
||||||
|
conn.close()
|
||||||
|
return reading
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
from flask import Flask, render_template, request, jsonify, redirect, url_for
|
||||||
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
|
from datetime import datetime
|
||||||
|
import subprocess
|
||||||
|
import re
|
||||||
|
import database as db
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# OIDs for Sierra Wireless RV50
|
||||||
|
OID_BYTES_SENT = '.1.3.6.1.4.1.20542.9.1.1.2.284.0'
|
||||||
|
OID_BYTES_RECV = '.1.3.6.1.4.1.20542.9.1.1.2.283.0'
|
||||||
|
|
||||||
|
# Store last readings for rate calculation
|
||||||
|
last_readings = {}
|
||||||
|
|
||||||
|
scheduler = BackgroundScheduler()
|
||||||
|
scheduler.start()
|
||||||
|
|
||||||
|
def format_bytes(bytes_value):
|
||||||
|
"""Convert bytes to human-readable format"""
|
||||||
|
if bytes_value is None:
|
||||||
|
return "N/A", 0
|
||||||
|
|
||||||
|
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
||||||
|
if abs(bytes_value) < 1024.0:
|
||||||
|
return f"{bytes_value:.2f} {unit}", bytes_value
|
||||||
|
bytes_value /= 1024.0
|
||||||
|
return f"{bytes_value:.2f} PB", bytes_value
|
||||||
|
|
||||||
|
def snmp_get(ip, port, community, oid):
|
||||||
|
"""Execute snmpget command and return value"""
|
||||||
|
try:
|
||||||
|
cmd = [
|
||||||
|
'snmpget', '-v', '2c', '-c', community,
|
||||||
|
'-t', '5', '-r', '2',
|
||||||
|
f'{ip}:{port}', oid
|
||||||
|
]
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
# Parse output: "SNMPv2-SMI::enterprises.20542.9.1.1.2.284.0 = Counter64: 123456"
|
||||||
|
match = re.search(r'Counter64:\s*(\d+)', result.stdout)
|
||||||
|
if match:
|
||||||
|
return int(match.group(1))
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"SNMP error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def poll_target(target_id, ip, port, community):
|
||||||
|
"""Poll a target and store the reading"""
|
||||||
|
global last_readings
|
||||||
|
|
||||||
|
bytes_sent = snmp_get(ip, port, community, OID_BYTES_SENT)
|
||||||
|
bytes_recv = snmp_get(ip, port, community, OID_BYTES_RECV)
|
||||||
|
|
||||||
|
if bytes_sent is not None and bytes_recv is not None:
|
||||||
|
db.add_reading(target_id, bytes_sent, bytes_recv)
|
||||||
|
|
||||||
|
# Calculate rates (bytes per second)
|
||||||
|
rate_sent = 0
|
||||||
|
rate_recv = 0
|
||||||
|
|
||||||
|
if target_id in last_readings:
|
||||||
|
last_sent, last_recv, last_time = last_readings[target_id]
|
||||||
|
time_diff = (datetime.now() - last_time).total_seconds()
|
||||||
|
if time_diff > 0:
|
||||||
|
rate_sent = (bytes_sent - last_sent) / time_diff
|
||||||
|
rate_recv = (bytes_recv - last_recv) / time_diff
|
||||||
|
|
||||||
|
last_readings[target_id] = (bytes_sent, bytes_recv, datetime.now())
|
||||||
|
|
||||||
|
return {
|
||||||
|
'bytes_sent': bytes_sent,
|
||||||
|
'bytes_recv': bytes_recv,
|
||||||
|
'rate_sent': rate_sent,
|
||||||
|
'rate_recv': rate_recv,
|
||||||
|
'success': True
|
||||||
|
}
|
||||||
|
|
||||||
|
return {'success': False}
|
||||||
|
|
||||||
|
def scheduled_poll():
|
||||||
|
"""Background job to poll all enabled targets"""
|
||||||
|
targets = db.get_all_targets()
|
||||||
|
for target in targets:
|
||||||
|
if target['enabled']:
|
||||||
|
poll_target(target['id'], target['ip_address'],
|
||||||
|
target['port'], target['community'])
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
targets = db.get_all_targets()
|
||||||
|
target_data = []
|
||||||
|
|
||||||
|
for target in targets:
|
||||||
|
latest = db.get_latest_reading(target['id'])
|
||||||
|
|
||||||
|
if latest:
|
||||||
|
sent_formatted, _ = format_bytes(latest['bytes_sent'])
|
||||||
|
recv_formatted, _ = format_bytes(latest['bytes_recv'])
|
||||||
|
|
||||||
|
# Calculate rate if we have previous reading
|
||||||
|
rate_sent = 0
|
||||||
|
rate_recv = 0
|
||||||
|
if target['id'] in last_readings:
|
||||||
|
last_sent, last_recv, last_time = last_readings[target['id']]
|
||||||
|
time_diff = (datetime.now() - last_time).total_seconds()
|
||||||
|
if time_diff > 0 and latest['bytes_sent'] >= last_sent:
|
||||||
|
rate_sent = (latest['bytes_sent'] - last_sent) / time_diff
|
||||||
|
rate_recv = (latest['bytes_recv'] - last_recv) / time_diff
|
||||||
|
|
||||||
|
rate_sent_fmt, _ = format_bytes(rate_sent)
|
||||||
|
rate_recv_fmt, _ = format_bytes(rate_recv)
|
||||||
|
else:
|
||||||
|
sent_formatted = recv_formatted = rate_sent_fmt = rate_recv_fmt = "N/A"
|
||||||
|
|
||||||
|
target_data.append({
|
||||||
|
**target,
|
||||||
|
'bytes_sent_fmt': sent_formatted,
|
||||||
|
'bytes_recv_fmt': recv_formatted,
|
||||||
|
'rate_sent_fmt': f"{rate_sent_fmt}/s",
|
||||||
|
'rate_recv_fmt': f"{rate_recv_fmt}/s",
|
||||||
|
'last_poll': latest['timestamp'] if latest else None
|
||||||
|
})
|
||||||
|
|
||||||
|
return render_template('index.html', targets=target_data)
|
||||||
|
|
||||||
|
@app.route('/add', methods=['POST'])
|
||||||
|
def add_target():
|
||||||
|
name = request.form.get('name', 'Unnamed')
|
||||||
|
ip_address = request.form.get('ip_address')
|
||||||
|
port = int(request.form.get('port', 161))
|
||||||
|
community = request.form.get('community', 'public')
|
||||||
|
period_minutes = int(request.form.get('period_minutes', 5))
|
||||||
|
|
||||||
|
if ip_address:
|
||||||
|
target_id = db.add_target(name, ip_address, port, community, period_minutes)
|
||||||
|
|
||||||
|
# Schedule polling for this target
|
||||||
|
scheduler.add_job(
|
||||||
|
scheduled_poll,
|
||||||
|
'interval',
|
||||||
|
minutes=period_minutes,
|
||||||
|
id=f'target_{target_id}',
|
||||||
|
replace_existing=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Do initial poll
|
||||||
|
poll_target(target_id, ip_address, port, community)
|
||||||
|
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
@app.route('/edit/<int:target_id>', methods=['POST'])
|
||||||
|
def edit_target(target_id):
|
||||||
|
name = request.form.get('name', 'Unnamed')
|
||||||
|
ip_address = request.form.get('ip_address')
|
||||||
|
port = int(request.form.get('port', 161))
|
||||||
|
community = request.form.get('community', 'public')
|
||||||
|
period_minutes = int(request.form.get('period_minutes', 5))
|
||||||
|
enabled = 1 if request.form.get('enabled') else 0
|
||||||
|
|
||||||
|
db.update_target(target_id, name, ip_address, port, community, period_minutes, enabled)
|
||||||
|
|
||||||
|
# Update scheduler
|
||||||
|
if enabled:
|
||||||
|
scheduler.add_job(
|
||||||
|
scheduled_poll,
|
||||||
|
'interval',
|
||||||
|
minutes=period_minutes,
|
||||||
|
id=f'target_{target_id}',
|
||||||
|
replace_existing=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
scheduler.remove_job(f'target_{target_id}')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
@app.route('/delete/<int:target_id>', methods=['POST'])
|
||||||
|
def delete_target(target_id):
|
||||||
|
try:
|
||||||
|
scheduler.remove_job(f'target_{target_id}')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
db.delete_target(target_id)
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
@app.route('/poll/<int:target_id>', methods=['POST'])
|
||||||
|
def manual_poll(target_id):
|
||||||
|
target = db.get_target(target_id)
|
||||||
|
if target:
|
||||||
|
result = poll_target(target['id'], target['ip_address'],
|
||||||
|
target['port'], target['community'])
|
||||||
|
return jsonify(result)
|
||||||
|
return jsonify({'success': False, 'error': 'Target not found'}), 404
|
||||||
|
|
||||||
|
@app.route('/graph/<int:target_id>')
|
||||||
|
def graph(target_id):
|
||||||
|
target = db.get_target(target_id)
|
||||||
|
hours = request.args.get('hours', 24, type=int)
|
||||||
|
readings = db.get_readings(target_id, hours)
|
||||||
|
|
||||||
|
labels = []
|
||||||
|
sent_data = []
|
||||||
|
recv_data = []
|
||||||
|
|
||||||
|
for r in readings:
|
||||||
|
labels.append(r['timestamp'])
|
||||||
|
sent_data.append(r['bytes_sent'])
|
||||||
|
recv_data.append(r['bytes_recv'])
|
||||||
|
|
||||||
|
return render_template('graph.html',
|
||||||
|
target=target,
|
||||||
|
labels=labels,
|
||||||
|
sent_data=sent_data,
|
||||||
|
recv_data=recv_data,
|
||||||
|
hours=hours)
|
||||||
|
|
||||||
|
@app.route('/api/readings/<int:target_id>')
|
||||||
|
def api_readings(target_id):
|
||||||
|
hours = request.args.get('hours', 24, type=int)
|
||||||
|
readings = db.get_readings(target_id, hours)
|
||||||
|
|
||||||
|
data = []
|
||||||
|
for r in readings:
|
||||||
|
data.append({
|
||||||
|
'timestamp': r['timestamp'],
|
||||||
|
'bytes_sent': r['bytes_sent'],
|
||||||
|
'bytes_recv': r['bytes_recv']
|
||||||
|
})
|
||||||
|
|
||||||
|
return jsonify(data)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
db.init_db()
|
||||||
|
|
||||||
|
# Start with initial poll for any existing targets
|
||||||
|
targets = db.get_all_targets()
|
||||||
|
for target in targets:
|
||||||
|
if target['enabled']:
|
||||||
|
scheduler.add_job(
|
||||||
|
scheduled_poll,
|
||||||
|
'interval',
|
||||||
|
minutes=target['period_minutes'],
|
||||||
|
id=f'target_{target["id"]}',
|
||||||
|
replace_existing=True
|
||||||
|
)
|
||||||
|
poll_target(target['id'], target['ip_address'],
|
||||||
|
target['port'], target['community'])
|
||||||
|
|
||||||
|
app.run(debug=True, host='0.0.0.0', port=5500)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
flask==3.0.0
|
||||||
|
apscheduler==3.10.4
|
||||||
Binary file not shown.
@@ -0,0 +1,41 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}SNMP Monitor{% endblock %}</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #1a1a2e; color: #eee; padding: 20px; }
|
||||||
|
.container { max-width: 1200px; margin: 0 auto; }
|
||||||
|
h1 { margin-bottom: 20px; color: #00d9ff; }
|
||||||
|
.card { background: #16213e; border-radius: 8px; padding: 20px; margin-bottom: 20px; }
|
||||||
|
.btn { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer;
|
||||||
|
font-size: 14px; margin-right: 5px; }
|
||||||
|
.btn-primary { background: #00d9ff; color: #000; }
|
||||||
|
.btn-danger { background: #e94560; color: #fff; }
|
||||||
|
.btn-success { background: #0f3460; color: #00d9ff; border: 1px solid #00d9ff; }
|
||||||
|
input, select { padding: 8px; border-radius: 4px; border: 1px solid #0f3460;
|
||||||
|
background: #1a1a2e; color: #eee; margin-right: 10px; }
|
||||||
|
table { width: 100%; border-collapse: collapse; }
|
||||||
|
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #0f3460; }
|
||||||
|
th { color: #00d9ff; }
|
||||||
|
.status-ok { color: #4ade80; }
|
||||||
|
.status-error { color: #e94560; }
|
||||||
|
.form-row { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
|
||||||
|
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||||||
|
background: rgba(0,0,0,0.7); justify-content: center; align-items: center; }
|
||||||
|
.modal.active { display: flex; }
|
||||||
|
.modal-content { background: #16213e; padding: 30px; border-radius: 8px;
|
||||||
|
max-width: 500px; width: 90%; }
|
||||||
|
</style>
|
||||||
|
{% block extra_css %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
{% block extra_js %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Graph - {{ target.name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>📊 {{ target.name }} - Data Usage</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div style="margin-bottom: 15px;">
|
||||||
|
<a href="/graph/{{ target.id }}?hours=6" class="btn btn-success">6h</a>
|
||||||
|
<a href="/graph/{{ target.id }}?hours=24" class="btn btn-success">24h</a>
|
||||||
|
<a href="/graph/{{ target.id }}?hours=168" class="btn btn-success">7d</a>
|
||||||
|
<a href="/" class="btn btn-primary">← Back</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<canvas id="usageChart" style="max-height: 400px;"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3 style="margin-bottom: 15px; color: #00d9ff;">Device Info</h3>
|
||||||
|
<p><strong>Name:</strong> {{ target.name }}</p>
|
||||||
|
<p><strong>Modem Type:</strong> {{ target.modem_type or 'rv50' }}</p>
|
||||||
|
<p><strong>IP:</strong> {{ target.ip_address }}:{{ target.port }}</p>
|
||||||
|
<p><strong>Community:</strong> {{ target.community }}</p>
|
||||||
|
<p><strong>Poll Period:</strong> {{ target.period_minutes }} minutes</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
const labels = {{ labels | tojson }};
|
||||||
|
const sentData = {{ sent_data | tojson }};
|
||||||
|
const recvData = {{ recv_data | tojson }};
|
||||||
|
|
||||||
|
// Convert to KB for readability
|
||||||
|
const sentKB = sentData.map(v => v ? (v / 1024).toFixed(2) : 0);
|
||||||
|
const recvKB = recvData.map(v => v ? (v / 1024).toFixed(2) : 0);
|
||||||
|
|
||||||
|
new Chart(document.getElementById('usageChart'), {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Bytes Sent (KB)',
|
||||||
|
data: sentKB,
|
||||||
|
borderColor: '#00d9ff',
|
||||||
|
backgroundColor: 'rgba(0, 217, 255, 0.1)',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Bytes Received (KB)',
|
||||||
|
data: recvKB,
|
||||||
|
borderColor: '#4ade80',
|
||||||
|
backgroundColor: 'rgba(74, 222, 128, 0.1)',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: true,
|
||||||
|
plugins: {
|
||||||
|
legend: { labels: { color: '#eee' } }
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: { ticks: { color: '#888' }, grid: { color: '#0f3460' } },
|
||||||
|
y: { ticks: { color: '#888' }, grid: { color: '#0f3460' } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}SNMP Monitor - Dashboard{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>📡 Cellular Data Usage Monitor</h1>
|
||||||
|
|
||||||
|
<!-- Add Target Form -->
|
||||||
|
<div class="card">
|
||||||
|
<h3 style="margin-bottom: 15px; color: #00d9ff;">Add New Target</h3>
|
||||||
|
<form action="/add" method="POST">
|
||||||
|
<div class="form-row">
|
||||||
|
<input type="text" name="name" placeholder="Device Name" required style="flex: 1;">
|
||||||
|
<select name="modem_type" style="width: 160px;">
|
||||||
|
<option value="rv50">Sierra RV50</option>
|
||||||
|
<option value="bullet-lte">Microhard Bullet-LTE</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" name="ip_address" placeholder="IP Address" required style="flex: 1;">
|
||||||
|
<input type="number" name="port" value="161" placeholder="Port" style="width: 80px;">
|
||||||
|
<input type="text" name="community" value="public" placeholder="Community" style="flex: 1;">
|
||||||
|
<input type="number" name="period_minutes" value="5" placeholder="Minutes" style="width: 80px;">
|
||||||
|
<button type="submit" class="btn btn-primary">Add Target</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Targets Table -->
|
||||||
|
<div class="card">
|
||||||
|
<h3 style="margin-bottom: 15px; color: #00d9ff;">Monitored Devices</h3>
|
||||||
|
{% if targets %}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Modem</th>
|
||||||
|
<th>IP Address</th>
|
||||||
|
<th>Bytes Sent</th>
|
||||||
|
<th>Bytes Received</th>
|
||||||
|
<th>Rate Sent</th>
|
||||||
|
<th>Rate Recv</th>
|
||||||
|
<th>Period</th>
|
||||||
|
<th>Last Poll</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for target in targets %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ target.name }}</td>
|
||||||
|
<td>{{ target.modem_type or 'rv50' }}</td>
|
||||||
|
<td>{{ target.ip_address }}:{{ target.port }}</td>
|
||||||
|
<td>{{ target.bytes_sent_fmt }}</td>
|
||||||
|
<td>{{ target.bytes_recv_fmt }}</td>
|
||||||
|
<td>{{ target.rate_sent_fmt }}</td>
|
||||||
|
<td>{{ target.rate_recv_fmt }}</td>
|
||||||
|
<td>{{ target.period_minutes }} min</td>
|
||||||
|
<td>{{ target.last_poll or 'Never' }}</td>
|
||||||
|
<td class="{{ 'status-ok' if target.enabled else 'status-error' }}">
|
||||||
|
{{ 'Active' if target.enabled else 'Paused' }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="/graph/{{ target.id }}?hours=24" class="btn btn-success">📊 Graph</a>
|
||||||
|
<button onclick="manualPoll({{ target.id }})" class="btn btn-primary">🔄 Poll</button>
|
||||||
|
<button onclick="editTarget({{ target.id }}, '{{ target.name }}', '{{ target.ip_address }}', {{ target.port }}, '{{ target.community }}', {{ target.period_minutes }}, {{ target.enabled }}, '{{ target.modem_type or 'rv50' }}')" class="btn btn-success">✏️ Edit</button>
|
||||||
|
<form action="/delete/{{ target.id }}" method="POST" style="display: inline;"
|
||||||
|
onsubmit="return confirm('Delete this target?')">
|
||||||
|
<button type="submit" class="btn btn-danger">🗑️</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p style="color: #888;">No targets configured. Add one above!</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Modal -->
|
||||||
|
<div id="editModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h3 style="margin-bottom: 15px; color: #00d9ff;">Edit Target</h3>
|
||||||
|
<form id="editForm" method="POST">
|
||||||
|
<div class="form-row">
|
||||||
|
<input type="text" name="name" id="edit_name" placeholder="Device Name" required style="flex: 1;">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<select name="modem_type" id="edit_modem_type" style="width: 160px;">
|
||||||
|
<option value="rv50">Sierra RV50</option>
|
||||||
|
<option value="bullet-lte">Microhard Bullet-LTE</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<input type="text" name="ip_address" id="edit_ip" placeholder="IP Address" required style="flex: 1;">
|
||||||
|
<input type="number" name="port" id="edit_port" value="161" style="width: 80px;">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<input type="text" name="community" id="edit_community" placeholder="Community" style="flex: 1;">
|
||||||
|
<input type="number" name="period_minutes" id="edit_period" value="5" style="width: 80px;">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label><input type="checkbox" name="enabled" id="edit_enabled" checked> Enabled</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||||
|
<button type="button" onclick="closeEditModal()" class="btn btn-danger">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
function manualPoll(targetId) {
|
||||||
|
fetch(`/poll/${targetId}`, { method: 'POST' })
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert('Poll successful!');
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Poll failed: ' + (data.error || 'Unknown error'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => alert('Error: ' + err));
|
||||||
|
}
|
||||||
|
|
||||||
|
function editTarget(id, name, ip, port, community, period, enabled, modemType) {
|
||||||
|
document.getElementById('edit_name').value = name;
|
||||||
|
document.getElementById('edit_ip').value = ip;
|
||||||
|
document.getElementById('edit_port').value = port;
|
||||||
|
document.getElementById('edit_community').value = community;
|
||||||
|
document.getElementById('edit_period').value = period;
|
||||||
|
document.getElementById('edit_enabled').checked = enabled == 1;
|
||||||
|
document.getElementById('edit_modem_type').value = modemType;
|
||||||
|
|
||||||
|
document.getElementById('editForm').action = `/edit/${id}`;
|
||||||
|
document.getElementById('editModal').classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeEditModal() {
|
||||||
|
document.getElementById('editModal').classList.remove('active');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user