Initial commit — Server_Monitorizare_v2

This commit is contained in:
ske087
2026-04-23 15:55:46 +03:00
commit d2485e4c66
61 changed files with 13861 additions and 0 deletions

578
app/web/main.py Normal file
View File

@@ -0,0 +1,578 @@
"""
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