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