""" 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/') 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//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//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//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