Features: - Device management and monitoring dashboard - Remote command execution on devices via port 80 - Auto-update coordination for multiple devices - Database reset functionality with safety confirmations - Server logs filtering and dedicated logging interface - Device status monitoring and management - SQLite database for comprehensive logging - Web interface with Bootstrap styling - Comprehensive error handling and logging Key components: - server.py: Main Flask application with all routes - templates/: Complete web interface templates - data/database.db: SQLite database for device logs - UPDATE_SUMMARY.md: Development progress documentation
462 lines
17 KiB
Python
462 lines
17 KiB
Python
from flask import Flask, request, render_template, jsonify, redirect, url_for
|
|
import sqlite3
|
|
from datetime import datetime
|
|
from urllib.parse import unquote
|
|
import requests
|
|
import threading
|
|
|
|
app = Flask(__name__)
|
|
DATABASE = 'data/database.db' # Updated path for the database
|
|
# Route to handle log submissions
|
|
@app.route('/logs', methods=['POST'])
|
|
@app.route('/log', methods=['POST'])
|
|
def log_event():
|
|
try:
|
|
#print(f"Connecting to database at: {DATABASE}")
|
|
|
|
# Get the JSON payload
|
|
data = request.json
|
|
if not data:
|
|
return {"error": "Invalid or missing JSON payload"}, 400
|
|
|
|
#print(f"Received request data: {data}")
|
|
|
|
# Extract fields from the JSON payload
|
|
hostname = data.get('hostname')
|
|
device_ip = data.get('device_ip')
|
|
nume_masa = data.get('nume_masa')
|
|
log_message = data.get('log_message')
|
|
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
|
|
# Validate required fields
|
|
if not hostname or not device_ip or not nume_masa or not log_message:
|
|
print("Validation failed: Missing required fields")
|
|
return {"error": "Missing required fields"}, 400
|
|
|
|
# Save the log to the database
|
|
with sqlite3.connect(DATABASE) as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute('''
|
|
INSERT INTO logs (hostname, device_ip, nume_masa, timestamp, event_description)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
''', (hostname, device_ip, nume_masa, timestamp, log_message))
|
|
conn.commit()
|
|
print("Log saved successfully")
|
|
|
|
return {"message": "Log saved successfully"}, 201
|
|
|
|
except sqlite3.Error as e:
|
|
print(f"Database error: {e}")
|
|
return {"error": f"Database connection failed: {e}"}, 500
|
|
|
|
except Exception as e:
|
|
print(f"Unexpected error: {e}")
|
|
return {"error": "An unexpected error occurred"}, 500
|
|
|
|
# Route to display the dashboard (excluding server logs)
|
|
@app.route('/dashboard', methods=['GET'])
|
|
def dashboard():
|
|
with sqlite3.connect(DATABASE) as conn:
|
|
cursor = conn.cursor()
|
|
# Fetch the last 60 logs excluding server logs, ordered by timestamp in descending order
|
|
cursor.execute('''
|
|
SELECT hostname, device_ip, nume_masa, timestamp, event_description
|
|
FROM logs
|
|
WHERE hostname != 'SERVER'
|
|
ORDER BY timestamp DESC
|
|
LIMIT 60
|
|
''')
|
|
logs = cursor.fetchall()
|
|
return render_template('dashboard.html', logs=logs)
|
|
# Route to display logs for a specific device (excluding server logs)
|
|
@app.route('/device_logs/<nume_masa>', methods=['GET'])
|
|
def device_logs(nume_masa):
|
|
nume_masa = unquote(nume_masa) # Decode URL-encoded value
|
|
with sqlite3.connect(DATABASE) as conn:
|
|
cursor = conn.cursor()
|
|
# Order logs by timestamp in descending order, excluding server logs
|
|
cursor.execute('''
|
|
SELECT hostname, nume_masa, timestamp, event_description
|
|
FROM logs
|
|
WHERE nume_masa = ? AND hostname != 'SERVER'
|
|
ORDER BY timestamp DESC
|
|
''', (nume_masa,))
|
|
logs = cursor.fetchall()
|
|
return render_template('device_logs.html', logs=logs, nume_masa=nume_masa)
|
|
|
|
@app.route('/unique_devices', methods=['GET'])
|
|
def unique_devices():
|
|
with sqlite3.connect(DATABASE) as conn:
|
|
cursor = conn.cursor()
|
|
# Query to get unique devices with their most recent log (excluding server logs)
|
|
cursor.execute('''
|
|
SELECT hostname, device_ip, MAX(timestamp) AS last_log, event_description
|
|
FROM logs
|
|
WHERE hostname != 'SERVER'
|
|
GROUP BY hostname, device_ip
|
|
ORDER BY last_log DESC
|
|
''')
|
|
devices = cursor.fetchall()
|
|
return render_template('unique_devices.html', devices=devices)
|
|
|
|
@app.route('/hostname_logs/<hostname>', methods=['GET'])
|
|
def hostname_logs(hostname):
|
|
with sqlite3.connect(DATABASE) as conn:
|
|
cursor = conn.cursor()
|
|
# Fetch logs for the specified hostname (excluding server logs)
|
|
cursor.execute('''
|
|
SELECT hostname, nume_masa, timestamp, event_description
|
|
FROM logs
|
|
WHERE hostname = ? AND hostname != 'SERVER'
|
|
ORDER BY timestamp DESC
|
|
''', (hostname,))
|
|
logs = cursor.fetchall()
|
|
return render_template('hostname_logs.html', logs=logs, hostname=hostname)
|
|
|
|
# Route to display server logs only
|
|
@app.route('/server_logs', methods=['GET'])
|
|
def server_logs():
|
|
with sqlite3.connect(DATABASE) as conn:
|
|
cursor = conn.cursor()
|
|
# Fetch only server logs, ordered by timestamp in descending order
|
|
cursor.execute('''
|
|
SELECT hostname, device_ip, nume_masa, timestamp, event_description
|
|
FROM logs
|
|
WHERE hostname = 'SERVER'
|
|
ORDER BY timestamp DESC
|
|
''')
|
|
logs = cursor.fetchall()
|
|
return render_template('server_logs.html', logs=logs)
|
|
|
|
# Function to execute command on a remote device
|
|
def execute_command_on_device(device_ip, command):
|
|
"""
|
|
Send command to a device for execution
|
|
"""
|
|
try:
|
|
url = f"http://{device_ip}:80/execute_command"
|
|
payload = {"command": command}
|
|
|
|
response = requests.post(url, json=payload, timeout=30)
|
|
|
|
if response.status_code == 200:
|
|
result = response.json()
|
|
return {"success": True, "result": result}
|
|
else:
|
|
error_data = response.json() if response.content else {"error": "Unknown error"}
|
|
return {"success": False, "error": error_data}
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
return {"success": False, "error": f"Connection error: {str(e)}"}
|
|
except Exception as e:
|
|
return {"success": False, "error": f"Unexpected error: {str(e)}"}
|
|
|
|
# Function to get device status
|
|
def get_device_status(device_ip):
|
|
"""
|
|
Get status information from a device
|
|
"""
|
|
try:
|
|
url = f"http://{device_ip}:80/status"
|
|
response = requests.get(url, timeout=10)
|
|
|
|
if response.status_code == 200:
|
|
return {"success": True, "status": response.json()}
|
|
else:
|
|
return {"success": False, "error": "Failed to get device status"}
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
return {"success": False, "error": f"Connection error: {str(e)}"}
|
|
except Exception as e:
|
|
return {"success": False, "error": f"Unexpected error: {str(e)}"}
|
|
|
|
# Route to display device management page (excluding server)
|
|
@app.route('/device_management', methods=['GET'])
|
|
def device_management():
|
|
with sqlite3.connect(DATABASE) as conn:
|
|
cursor = conn.cursor()
|
|
# Get unique devices excluding server
|
|
cursor.execute('''
|
|
SELECT hostname, device_ip, MAX(timestamp) AS last_log
|
|
FROM logs
|
|
WHERE hostname != 'SERVER'
|
|
GROUP BY hostname, device_ip
|
|
ORDER BY last_log DESC
|
|
''')
|
|
devices = cursor.fetchall()
|
|
return render_template('device_management.html', devices=devices)
|
|
|
|
# Route to execute command on a specific device
|
|
@app.route('/execute_command', methods=['POST'])
|
|
def execute_command():
|
|
try:
|
|
data = request.json
|
|
device_ip = data.get('device_ip')
|
|
command = data.get('command')
|
|
|
|
if not device_ip or not command:
|
|
return jsonify({"error": "device_ip and command are required"}), 400
|
|
|
|
# Execute command on device
|
|
result = execute_command_on_device(device_ip, command)
|
|
|
|
# Log the command execution attempt
|
|
with sqlite3.connect(DATABASE) as conn:
|
|
cursor = conn.cursor()
|
|
status = "SUCCESS" if result['success'] else "FAILED"
|
|
log_message = f"Command '{command}' {status}"
|
|
if not result['success']:
|
|
log_message += f" - Error: {result['error']}"
|
|
|
|
cursor.execute('''
|
|
INSERT INTO logs (hostname, device_ip, nume_masa, timestamp, event_description)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
''', ("SERVER", device_ip, "COMMAND", datetime.now().strftime('%Y-%m-%d %H:%M:%S'), log_message))
|
|
conn.commit()
|
|
|
|
return jsonify(result), 200 if result['success'] else 400
|
|
|
|
except Exception as e:
|
|
return jsonify({"error": f"Server error: {str(e)}"}), 500
|
|
|
|
# Route to get device status
|
|
@app.route('/device_status/<device_ip>', methods=['GET'])
|
|
def device_status(device_ip):
|
|
result = get_device_status(device_ip)
|
|
return jsonify(result), 200 if result['success'] else 400
|
|
|
|
# Route to execute command on multiple devices
|
|
@app.route('/execute_command_bulk', methods=['POST'])
|
|
def execute_command_bulk():
|
|
try:
|
|
data = request.json
|
|
device_ips = data.get('device_ips', [])
|
|
command = data.get('command')
|
|
|
|
if not device_ips or not command:
|
|
return jsonify({"error": "device_ips and command are required"}), 400
|
|
|
|
results = {}
|
|
threads = []
|
|
|
|
def execute_on_device(ip):
|
|
results[ip] = execute_command_on_device(ip, command)
|
|
|
|
# Execute commands in parallel
|
|
for ip in device_ips:
|
|
thread = threading.Thread(target=execute_on_device, args=(ip,))
|
|
threads.append(thread)
|
|
thread.start()
|
|
|
|
# Wait for all threads to complete
|
|
for thread in threads:
|
|
thread.join()
|
|
|
|
# Log bulk command execution
|
|
with sqlite3.connect(DATABASE) as conn:
|
|
cursor = conn.cursor()
|
|
for ip, result in results.items():
|
|
status = "SUCCESS" if result['success'] else "FAILED"
|
|
log_message = f"Bulk command '{command}' {status}"
|
|
if not result['success']:
|
|
log_message += f" - Error: {result['error']}"
|
|
|
|
cursor.execute('''
|
|
INSERT INTO logs (hostname, device_ip, nume_masa, timestamp, event_description)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
''', ("SERVER", ip, "BULK_COMMAND", datetime.now().strftime('%Y-%m-%d %H:%M:%S'), log_message))
|
|
conn.commit()
|
|
|
|
return jsonify({"results": results}), 200
|
|
|
|
except Exception as e:
|
|
return jsonify({"error": f"Server error: {str(e)}"}), 500
|
|
|
|
@app.route('/auto_update_devices', methods=['POST'])
|
|
def auto_update_devices():
|
|
"""
|
|
Trigger auto-update on selected devices
|
|
"""
|
|
try:
|
|
data = request.json
|
|
device_ips = data.get('device_ips', [])
|
|
|
|
if not device_ips:
|
|
return jsonify({"error": "device_ips list is required"}), 400
|
|
|
|
results = []
|
|
|
|
for ip in device_ips:
|
|
try:
|
|
# Send auto-update command to device
|
|
response = requests.post(
|
|
f'http://{ip}:80/auto_update',
|
|
json={},
|
|
timeout=10
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
result_data = response.json()
|
|
results.append({
|
|
"device_ip": ip,
|
|
"success": True,
|
|
"status": result_data.get('status'),
|
|
"message": result_data.get('message'),
|
|
"old_version": result_data.get('old_version'),
|
|
"new_version": result_data.get('new_version')
|
|
})
|
|
|
|
# Log successful update
|
|
with sqlite3.connect(DATABASE) as conn:
|
|
cursor = conn.cursor()
|
|
log_message = f"Auto-update: {result_data.get('message', 'Update initiated')}"
|
|
cursor.execute('''
|
|
INSERT INTO logs (hostname, device_ip, nume_masa, timestamp, event_description)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
''', ("SERVER", ip, "AUTO_UPDATE", datetime.now().strftime('%Y-%m-%d %H:%M:%S'), log_message))
|
|
conn.commit()
|
|
|
|
else:
|
|
error_msg = f"HTTP {response.status_code}"
|
|
try:
|
|
error_data = response.json()
|
|
error_msg = error_data.get('error', error_msg)
|
|
except:
|
|
pass
|
|
|
|
results.append({
|
|
"device_ip": ip,
|
|
"success": False,
|
|
"error": error_msg
|
|
})
|
|
|
|
except requests.exceptions.Timeout:
|
|
results.append({
|
|
"device_ip": ip,
|
|
"success": False,
|
|
"error": "Request timeout"
|
|
})
|
|
except requests.exceptions.ConnectionError:
|
|
results.append({
|
|
"device_ip": ip,
|
|
"success": False,
|
|
"error": "Connection failed - device may be offline"
|
|
})
|
|
except Exception as e:
|
|
results.append({
|
|
"device_ip": ip,
|
|
"success": False,
|
|
"error": str(e)
|
|
})
|
|
|
|
# Log failed update attempt
|
|
if not results[-1]['success']:
|
|
with sqlite3.connect(DATABASE) as conn:
|
|
cursor = conn.cursor()
|
|
log_message = f"Auto-update failed: {results[-1]['error']}"
|
|
cursor.execute('''
|
|
INSERT INTO logs (hostname, device_ip, nume_masa, timestamp, event_description)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
''', ("SERVER", ip, "AUTO_UPDATE", datetime.now().strftime('%Y-%m-%d %H:%M:%S'), log_message))
|
|
conn.commit()
|
|
|
|
return jsonify({"results": results}), 200
|
|
|
|
except Exception as e:
|
|
return jsonify({"error": f"Server error: {str(e)}"}), 500
|
|
|
|
# Route to clear and reset the database
|
|
@app.route('/reset_database', methods=['POST'])
|
|
def reset_database():
|
|
"""
|
|
Clear all data from the database and reinitialize with fresh schema
|
|
"""
|
|
try:
|
|
with sqlite3.connect(DATABASE) as conn:
|
|
cursor = conn.cursor()
|
|
|
|
# Get the count of logs before deletion for logging
|
|
cursor.execute('SELECT COUNT(*) FROM logs')
|
|
log_count = cursor.fetchone()[0]
|
|
|
|
# Drop the existing logs table
|
|
cursor.execute('DROP TABLE IF EXISTS logs')
|
|
|
|
# Recreate the logs table with fresh schema
|
|
cursor.execute('''
|
|
CREATE TABLE logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
hostname TEXT NOT NULL,
|
|
device_ip TEXT NOT NULL,
|
|
nume_masa TEXT NOT NULL,
|
|
timestamp TEXT NOT NULL,
|
|
event_description TEXT NOT NULL
|
|
)
|
|
''')
|
|
|
|
# Insert a system log entry to mark the database reset
|
|
reset_timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
cursor.execute('''
|
|
INSERT INTO logs (hostname, device_ip, nume_masa, timestamp, event_description)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
''', ("SERVER", "127.0.0.1", "SYSTEM", reset_timestamp, f"Database cleared and reinitialized - {log_count} logs deleted"))
|
|
|
|
conn.commit()
|
|
|
|
return jsonify({
|
|
"success": True,
|
|
"message": "Database successfully cleared and reinitialized",
|
|
"timestamp": reset_timestamp,
|
|
"deleted_count": log_count
|
|
}), 200
|
|
|
|
except sqlite3.Error as e:
|
|
return jsonify({
|
|
"success": False,
|
|
"error": f"Database error: {str(e)}"
|
|
}), 500
|
|
except Exception as e:
|
|
return jsonify({
|
|
"success": False,
|
|
"error": f"Unexpected error: {str(e)}"
|
|
}), 500
|
|
|
|
# Route to get database statistics
|
|
@app.route('/database_stats', methods=['GET'])
|
|
def database_stats():
|
|
"""
|
|
Get database statistics including log count
|
|
"""
|
|
try:
|
|
with sqlite3.connect(DATABASE) as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute('SELECT COUNT(*) FROM logs')
|
|
total_logs = cursor.fetchone()[0]
|
|
|
|
cursor.execute('SELECT COUNT(DISTINCT hostname) FROM logs')
|
|
unique_devices = cursor.fetchone()[0]
|
|
|
|
cursor.execute('SELECT MIN(timestamp), MAX(timestamp) FROM logs')
|
|
date_range = cursor.fetchone()
|
|
|
|
return jsonify({
|
|
"success": True,
|
|
"total_logs": total_logs,
|
|
"unique_devices": unique_devices,
|
|
"earliest_log": date_range[0],
|
|
"latest_log": date_range[1]
|
|
}), 200
|
|
|
|
except sqlite3.Error as e:
|
|
return jsonify({
|
|
"success": False,
|
|
"error": f"Database error: {str(e)}"
|
|
}), 500
|
|
except Exception as e:
|
|
return jsonify({
|
|
"success": False,
|
|
"error": f"Unexpected error: {str(e)}"
|
|
}), 500
|
|
|
|
if __name__ == '__main__':
|
|
app.run(host='0.0.0.0', port=80) |