Files
Server_Monitorizare_v2/app/web/main.py
2026-04-23 15:55:46 +03:00

578 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Main web routes for dashboard and device management
"""
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
from app.models import Device, LogEntry, MessageTemplate, AnsibleExecution, WMTUpdateRequest, InventoryGroup, device_inventory_association
from config.database_config import get_db
from app.services.log_service import LogCompressionService
from datetime import datetime, timedelta
from pathlib import Path
from sqlalchemy import text, func
import logging
import yaml
# Create blueprint
main_bp = Blueprint('main', __name__)
# Initialize services
log_service = LogCompressionService()
@main_bp.route('/')
def index():
"""Redirect root to devices page"""
return redirect(url_for('main.devices'))
@main_bp.route('/dashboard')
def dashboard():
"""Redirect /dashboard to devices"""
return redirect(url_for('main.devices'))
@main_bp.route('/devices')
def devices():
"""Device management page"""
try:
with get_db().get_session() as session:
devices = session.query(Device).order_by(Device.last_seen.desc()).all()
# Get log count per device
device_log_counts = {}
for device in devices:
log_count = session.query(LogEntry).filter_by(device_id=device.id).count()
device_log_counts[device.id] = log_count
pending_count = session.query(WMTUpdateRequest).filter_by(status='pending').count()
return render_template('device_management.html',
devices=devices,
device_log_counts=device_log_counts,
pending_count=pending_count)
except Exception as e:
logging.error(f"Error loading devices: {e}")
flash(f'Error loading devices: {e}', 'error')
return render_template('device_management.html', devices=[], device_log_counts={}, pending_count=0)
@main_bp.route('/device/<int:device_id>')
def device_detail(device_id):
"""Device detail page with logs and stats"""
try:
with get_db().get_session() as session:
device = session.query(Device).get(device_id)
if not device:
flash('Device not found', 'error')
return redirect(url_for('main.devices'))
# Get device logs (last 100)
logs = session.query(LogEntry).filter_by(device_id=device_id).order_by(
LogEntry.timestamp.desc()
).limit(100).all()
# Get log statistics
log_stats = {
'total': len(logs),
'by_severity': {},
'last_24h': 0
}
last_24h = datetime.utcnow() - timedelta(hours=24)
for log in logs:
# Count by severity
severity = log.severity
log_stats['by_severity'][severity] = log_stats['by_severity'].get(severity, 0) + 1
# Count last 24h
if log.timestamp >= last_24h:
log_stats['last_24h'] += 1
return render_template('device_detail.html',
device=device,
logs=logs,
log_stats=log_stats)
except Exception as e:
logging.error(f"Error loading device detail: {e}")
flash(f'Error loading device detail: {e}', 'error')
return redirect(url_for('main.devices'))
@main_bp.route('/logs')
def logs():
"""Log viewer with filtering"""
try:
# Get filter parameters
device_id = request.args.get('device_id', type=int)
severity = request.args.get('severity')
search = request.args.get('search', '')
page = request.args.get('page', 1, type=int)
per_page = min(request.args.get('per_page', 50, type=int), 200)
with get_db().get_session() as session:
# Build query
query = session.query(LogEntry).join(Device)
# Apply filters
if device_id:
query = query.filter(LogEntry.device_id == device_id)
if severity:
query = query.filter(LogEntry.severity == severity)
if search:
# Search in resolved message or full message
query = query.filter(
# This is simplified - in production you'd want full-text search
LogEntry.full_message.contains(search)
)
# Order by timestamp desc
query = query.order_by(LogEntry.timestamp.desc())
# Get total count for pagination
total = query.count()
# Apply pagination
offset = (page - 1) * per_page
logs = query.offset(offset).limit(per_page).all()
# Calculate pagination info
total_pages = (total + per_page - 1) // per_page
has_prev = page > 1
has_next = page < total_pages
# Get device list for filter dropdown
devices = session.query(Device).order_by(Device.hostname).all()
pagination = {
'page': page,
'per_page': per_page,
'total': total,
'total_pages': total_pages,
'has_prev': has_prev,
'has_next': has_next,
'prev_num': page - 1 if has_prev else None,
'next_num': page + 1 if has_next else None
}
return render_template('logs.html',
logs=logs,
pagination=pagination,
devices=devices,
current_device_id=device_id,
current_severity=severity,
current_search=search)
except Exception as e:
logging.error(f"Error loading logs: {e}")
flash(f'Error loading logs: {e}', 'error')
return render_template('logs.html',
logs=[],
pagination={},
devices=[],
current_device_id=None,
current_severity=None,
current_search='')
@main_bp.route('/templates')
def templates():
"""Message templates management"""
try:
with get_db().get_session() as session:
templates = session.query(MessageTemplate).order_by(
MessageTemplate.usage_count.desc()
).all()
# Get template statistics
template_stats = {
'total': len(templates),
'by_category': {},
'total_usage': sum(t.usage_count for t in templates)
}
for template in templates:
category = template.category
template_stats['by_category'][category] = template_stats['by_category'].get(category, 0) + 1
return render_template('templates.html',
templates=templates,
template_stats=template_stats)
except Exception as e:
logging.error(f"Error loading templates: {e}")
flash(f'Error loading templates: {e}', 'error')
return render_template('templates.html', templates=[], template_stats={})
@main_bp.route('/stats')
def stats():
"""System statistics and analytics"""
try:
with get_db().get_session() as session:
# Get compression stats
compression_stats = log_service.get_compression_stats()
# Get device statistics
device_stats = {
'total': session.query(Device).count(),
'active': session.query(Device).filter_by(status='active').count(),
'inactive': session.query(Device).filter_by(status='inactive').count(),
'maintenance': session.query(Device).filter_by(status='maintenance').count()
}
# Get log statistics by time periods
now = datetime.utcnow()
periods = {
'last_hour': now - timedelta(hours=1),
'last_24h': now - timedelta(hours=24),
'last_week': now - timedelta(days=7),
'last_month': now - timedelta(days=30)
}
log_stats = {}
for period_name, period_start in periods.items():
count = session.query(LogEntry).filter(
LogEntry.timestamp >= period_start
).count()
log_stats[period_name] = count
# Get execution statistics
exec_stats = {
'total': session.query(AnsibleExecution).count(),
'successful': session.query(AnsibleExecution).filter_by(status='completed').count(),
'failed': session.query(AnsibleExecution).filter_by(status='failed').count(),
'running': session.query(AnsibleExecution).filter_by(status='running').count()
}
return render_template('stats.html',
compression_stats=compression_stats,
device_stats=device_stats,
log_stats=log_stats,
exec_stats=exec_stats)
except Exception as e:
logging.error(f"Error loading stats: {e}")
flash(f'Error loading stats: {e}', 'error')
return render_template('stats.html',
compression_stats={},
device_stats={},
log_stats={},
exec_stats={})
# API Endpoints for Device Management
@main_bp.route('/api/devices/add', methods=['POST'])
def api_add_device():
"""API endpoint to add a device manually"""
try:
data = request.get_json()
# Validate required fields
required_fields = ['hostname', 'device_ip', 'nume_masa']
for field in required_fields:
if not data.get(field):
return jsonify({
'success': False,
'message': f'Missing required field: {field}'
}), 400
# Check if device already exists (MAC-first, then hostname/IP)
with get_db().get_session() as session:
mac_input = data.get('mac_address', '').strip().lower() or None
existing_device = None
if mac_input:
existing_device = session.query(Device).filter_by(mac_address=mac_input).first()
if not existing_device:
existing_device = session.query(Device).filter(
(Device.hostname == data['hostname']) |
(Device.device_ip == data['device_ip'])
).first()
if existing_device:
# If found by MAC or hostname/IP, update it rather than reject
if mac_input and not existing_device.mac_address:
existing_device.mac_address = mac_input
existing_device.hostname = data['hostname']
existing_device.device_ip = data['device_ip']
existing_device.nume_masa = data['nume_masa']
if data.get('device_type'):
existing_device.device_type = data['device_type']
if data.get('os_version'):
existing_device.os_version = data['os_version']
if data.get('location'):
existing_device.location = data['location']
if data.get('status'):
existing_device.status = data['status']
existing_device.config_updated_at = datetime.utcnow()
existing_device.info_reviewed_at = datetime.utcnow()
session.flush()
return jsonify({
'success': True,
'message': 'Device already existed record updated',
'device_id': existing_device.id
}), 200
# Create new device
new_device = Device(
hostname=data['hostname'],
device_ip=data['device_ip'],
nume_masa=data['nume_masa'],
mac_address=data.get('mac_address', '').strip().lower() or None,
device_type=data.get('device_type', 'unknown'),
os_version=data.get('os_version'),
status=data.get('status', 'active'),
location=data.get('location'),
description=data.get('description'),
config_updated_at=datetime.utcnow(),
info_reviewed_at=datetime.utcnow(),
last_seen=datetime.utcnow()
)
session.add(new_device)
session.commit()
# Refresh ansible inventory
try:
from app.services.ansible_service import AnsibleService
ansible_service = AnsibleService()
ansible_service.generate_dynamic_inventory()
except Exception as e:
logging.warning(f"Failed to update ansible inventory: {e}")
return jsonify({
'success': True,
'message': 'Device added successfully',
'device_id': new_device.id
}), 201
except Exception as e:
logging.error(f"Error adding device: {e}")
return jsonify({
'success': False,
'message': f'Error adding device: {str(e)}'
}), 500
@main_bp.route('/api/devices/<int:device_id>/execute', methods=['POST'])
def api_execute_device_command(device_id):
"""API endpoint to execute commands on devices"""
try:
data = request.get_json()
command = data.get('command')
if not command:
return jsonify({
'success': False,
'message': 'Command is required'
}), 400
with get_db().get_session() as session:
device = session.query(Device).get(device_id)
if not device:
return jsonify({
'success': False,
'message': 'Device not found'
}), 404
# Mock implementation - in production this would execute actual commands
if command == 'ping':
# Simulate ping command
import subprocess
try:
result = subprocess.run(['ping', '-c', '1', device.device_ip],
capture_output=True, text=True, timeout=5)
success = result.returncode == 0
output = result.stdout if success else result.stderr
return jsonify({
'success': success,
'command': command,
'output': output,
'device': device.hostname
})
except subprocess.TimeoutExpired:
return jsonify({
'success': False,
'message': 'Command timed out',
'command': command
})
else:
# For other commands, return placeholder
return jsonify({
'success': True,
'message': f'Command "{command}" would be executed on {device.hostname}',
'command': command,
'note': 'This is a placeholder implementation'
})
except Exception as e:
logging.error(f"Error executing device command: {e}")
return jsonify({
'success': False,
'message': f'Error executing command: {str(e)}'
}), 500
# ---------------------------------------------------------------------------
# Device edit / delete (unified includes WMT fields)
# ---------------------------------------------------------------------------
@main_bp.route('/devices/<int:device_id>/edit', methods=['GET', 'POST'])
def device_edit(device_id):
"""Edit a device record (monitoring + WMT fields)."""
try:
with get_db().get_session() as session:
device = session.query(Device).filter_by(id=device_id).first()
if not device:
flash('Device not found.', 'error')
return redirect(url_for('main.devices'))
if request.method == 'POST':
device.hostname = request.form.get('hostname', '').strip() or device.hostname
device.device_ip = request.form.get('device_ip', '').strip() or device.device_ip
device.nume_masa = request.form.get('nume_masa', '').strip() or device.nume_masa
mac_raw = request.form.get('mac_address', '').strip().lower() or None
# Only assign MAC if no other device owns it
if mac_raw and mac_raw != device.mac_address:
conflict = session.query(Device).filter(
Device.mac_address == mac_raw, Device.id != device_id
).first()
if conflict:
flash(f'MAC {mac_raw} is already assigned to {conflict.hostname}.', 'error')
return render_template('device_edit.html', device=device)
device.mac_address = mac_raw
device.status = request.form.get('status', 'active')
device.location = request.form.get('location', '').strip() or None
device.device_type = request.form.get('device_type', '').strip() or 'unknown'
device.description = request.form.get('description', '').strip() or None
device.os_version = request.form.get('os_version', '').strip() or None
device.config_updated_at = datetime.utcnow()
device.info_reviewed_at = datetime.utcnow()
flash('Device updated.', 'success')
return redirect(url_for('main.devices'))
return render_template(
'device_edit.html',
device=device,
breadcrumbs=[
{'url': url_for('main.dashboard'), 'title': 'Dashboard'},
{'url': url_for('main.devices'), 'title': 'Devices'},
{'url': '#', 'title': f'Edit {device.hostname}'},
],
)
except Exception as e:
logging.error(f'Device edit error: {e}')
flash(f'Error: {e}', 'error')
return redirect(url_for('main.devices'))
@main_bp.route('/devices/<int:device_id>/delete', methods=['POST'])
def device_delete(device_id):
"""Delete a device and all its logs."""
try:
with get_db().get_session() as session:
device = session.query(Device).filter_by(id=device_id).first()
if device:
name = device.hostname
session.delete(device)
flash(f'Device {name} deleted.', 'success')
else:
flash('Device not found.', 'error')
except Exception as e:
logging.error(f'Device delete error: {e}')
flash(f'Error deleting device: {e}', 'error')
return redirect(url_for('main.devices'))
# ── Admin page ────────────────────────────────────────────────────────
INVENTORY_FILE = Path('ansible/inventory/dynamic_inventory.yaml')
@main_bp.route('/admin')
def admin():
"""Admin / maintenance page with DB and inventory stats."""
stats = {}
try:
with get_db().get_session() as session:
stats['devices'] = session.query(func.count(Device.id)).scalar()
stats['logs'] = session.query(func.count(LogEntry.id)).scalar()
stats['templates'] = session.query(func.count(MessageTemplate.id)).scalar()
stats['inventory_groups'] = session.query(func.count(InventoryGroup.id)).scalar()
stats['wmt_requests'] = session.query(func.count(WMTUpdateRequest.id)).scalar()
except Exception as e:
logging.error(f'Admin stats error: {e}')
# Inventory host count
try:
if INVENTORY_FILE.exists():
data = yaml.safe_load(INVENTORY_FILE.read_text()) or {}
all_children = data.get('all', {}).get('children', {})
inv_hosts = set()
for g in all_children.values():
inv_hosts.update((g or {}).get('hosts', {}).keys())
stats['inventory_hosts'] = len(inv_hosts)
stats['inventory_groups_yaml'] = len(all_children)
else:
stats['inventory_hosts'] = 0
stats['inventory_groups_yaml'] = 0
except Exception as e:
logging.error(f'Admin inventory stats error: {e}')
stats['inventory_hosts'] = '?'
stats['inventory_groups_yaml'] = '?'
return render_template('admin.html', stats=stats)
@main_bp.route('/admin/clear/logs', methods=['POST'])
def admin_clear_logs():
"""Delete all log entries from the database."""
try:
with get_db().get_session() as session:
count = session.query(LogEntry).delete()
session.commit()
return jsonify({'success': True, 'deleted': count})
except Exception as e:
logging.error(f'Admin clear logs error: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
@main_bp.route('/admin/clear/devices', methods=['POST'])
def admin_clear_devices():
"""Delete all devices (and their log entries) from the database."""
try:
with get_db().get_session() as session:
session.execute(text('DELETE FROM device_inventory_groups'))
logs = session.query(LogEntry).delete()
devices = session.query(Device).delete()
session.commit()
return jsonify({'success': True, 'deleted_devices': devices, 'deleted_logs': logs})
except Exception as e:
logging.error(f'Admin clear devices error: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
@main_bp.route('/admin/clear/inventory', methods=['POST'])
def admin_clear_inventory():
"""Reset the Ansible inventory file to a completely empty state."""
try:
empty = {'_meta': {'hostvars': {}}, 'all': {'hosts': {}, 'children': {}}}
INVENTORY_FILE.parent.mkdir(parents=True, exist_ok=True)
INVENTORY_FILE.write_text(yaml.dump(empty, default_flow_style=False))
# Also clear inventory_groups table
with get_db().get_session() as session:
session.execute(text('DELETE FROM device_inventory_groups'))
groups = session.query(InventoryGroup).delete()
session.commit()
return jsonify({'success': True, 'groups_deleted': groups})
except Exception as e:
logging.error(f'Admin clear inventory error: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
@main_bp.route('/admin/clear/wmt', methods=['POST'])
def admin_clear_wmt():
"""Delete all WMT update requests."""
try:
with get_db().get_session() as session:
count = session.query(WMTUpdateRequest).delete()
session.commit()
return jsonify({'success': True, 'deleted': count})
except Exception as e:
logging.error(f'Admin clear WMT requests error: {e}')
return jsonify({'success': False, 'error': str(e)}), 500