#!/usr/bin/env python3 """ Docker Dashboard API Fetches container data from Docker instances and provides endpoints for the frontend """ import json import subprocess import time import logging import platform import glob from datetime import datetime from flask import Flask, jsonify, render_template, request, send_from_directory from flask_cors import CORS import docker import psutil import requests import os from dotenv import load_dotenv from portainer_integration import PortainerClient from database import db # Load environment variables load_dotenv() app = Flask(__name__) CORS(app) class DockerService: def __init__(self): try: self.client = docker.from_env() print("Docker client initialized successfully") except Exception as e: logging.error(f"Failed to initialize Docker client: {e}") self.client = None def get_containers(self): """Get all containers with their status""" if not self.client: return [] containers = [] try: for container in self.client.containers.list(all=True): container_data = { 'id': container.id[:12], 'name': container.name, 'status': container.status, 'image': container.image.tags[0] if container.image.tags else container.image.short_id, 'ports': self._get_container_ports(container), 'created': container.attrs['Created'], 'uptime': self._calculate_uptime(container), 'labels': container.labels, 'environment': container.attrs['Config'].get('Env', []), 'mounts': self._get_mounts(container), 'networks': self._get_networks(container) } containers.append(container_data) except Exception as e: logging.error(f"Error fetching containers: {e}") return [] return containers def _get_container_ports(self, container): """Extract port mappings from container""" ports = [] try: port_bindings = container.attrs['NetworkSettings']['Ports'] for container_port, host_configs in port_bindings.items(): if host_configs: for host_config in host_configs: ports.append({ 'container_port': container_port, 'host_ip': host_config.get('HostIp', '0.0.0.0'), 'host_port': host_config.get('HostPort') }) except Exception: pass return ports def _calculate_uptime(self, container): """Calculate container uptime""" if container.status != 'running': return "Down" try: started_at = container.attrs['State']['StartedAt'] started_time = datetime.fromisoformat(started_at.replace('Z', '+00:00')) uptime = datetime.now(started_time.tzinfo) - started_time days = uptime.days hours, remainder = divmod(uptime.seconds, 3600) minutes, seconds = divmod(remainder, 60) if days > 0: return f"{days}d {hours}h" elif hours > 0: return f"{hours}h {minutes}m" else: return f"{minutes}m" except Exception: return "Unknown" def _get_mounts(self, container): """Get container volume mounts""" mounts = [] try: for mount in container.attrs['Mounts']: mounts.append({ 'type': mount.get('Type'), 'source': mount.get('Source'), 'destination': mount.get('Destination'), 'mode': mount.get('Mode') }) except Exception: pass return mounts def _get_networks(self, container): """Get container network information""" networks = [] try: networks_config = container.attrs['NetworkSettings']['Networks'] for network_name, network_config in networks_config.items(): networks.append({ 'name': network_name, 'ip_address': network_config.get('IPAddress'), 'gateway': network_config.get('Gateway'), 'mac_address': network_config.get('MacAddress') }) except Exception: pass return networks def get_system_info(self): """Get system information optimized for Raspberry Pi""" try: # CPU usage - reduced interval for Pi try: cpu_percent = psutil.cpu_percent(interval=0.1) # Even shorter for debugging logging.info(f"CPU percent: {cpu_percent}") except Exception as e: logging.error(f"CPU error: {e}") cpu_percent = 0 # Memory usage try: memory = psutil.virtual_memory() logging.info(f"Memory: total={memory.total}, used={memory.used}, percent={memory.percent}") except Exception as e: logging.error(f"Memory error: {e}") memory = type('Memory', (), { 'total': 0, 'available': 0, 'used': 0, 'percent': 0 })() # Disk usage - optimized for Pi filesystem disk_info = None # Always use root for debugging try: disk_info = psutil.disk_usage('/') logging.info(f"Disk: total={disk_info.total}, used={disk_info.used}, free={disk_info.free}") except Exception as e: logging.error(f"Disk error: {e}") disk_info = type('DiskInfo', (), { 'total': 0, 'used': 0, 'free': 0 })() # Memory optimization for Pi def format_bytes_pi(bytes_value): """Format bytes optimized for Pi display""" if bytes_value < 1024: return f"{bytes_value} B" elif bytes_value < 1024 * 1024: return f"{bytes_value / 1024:.0f} KB" elif bytes_value < 1024 * 1024 * 1024: return f"{bytes_value / (1024 * 1024):.0f} MB" else: return f"{bytes_value / (1024 * 1024 * 1024):.1f} GB" result = { 'cpu_percent': cpu_percent, 'memory_percent': memory.percent, 'disk_percent': round((disk_info.used / disk_info.total) * 100, 1) if disk_info.total > 0 else 0, 'uptime': self._get_system_uptime(), 'cpu': { 'percent': cpu_percent, 'cores': psutil.cpu_count() or 1 }, 'memory': { 'total': memory.total, 'available': memory.available, 'used': memory.used, 'percent': memory.percent, 'formatted_total': format_bytes_pi(memory.total), 'formatted_used': format_bytes_pi(memory.used), 'formatted_available': format_bytes_pi(memory.available) }, 'disk': { 'total': disk_info.total, 'used': disk_info.used, 'free': disk_info.free, 'percent': round((disk_info.used / disk_info.total) * 100, 1) if disk_info.total > 0 else 0, 'formatted_total': format_bytes_pi(disk_info.total), 'formatted_used': format_bytes_pi(disk_info.used), 'formatted_free': format_bytes_pi(disk_info.free) } } logging.info(f"System info result: {result}") return result except Exception as e: logging.error(f"System info error: {e}") return { 'error': str(e), 'cpu': {'percent': 0, 'cores': 1}, 'memory': {'total': 0, 'available': 0, 'used': 0, 'percent': 0}, 'disk': {'total': 0, 'used': 0, 'free': 0, 'percent': 0}, 'uptime': 'Unknown', 'platform': {'system': 'Linux'} } def _get_system_uptime(self): """Get system uptime optimized for Pi""" try: with open('/proc/uptime', 'r') as f: uptime_seconds = float(f.readline().split()[0]) days = int(uptime_seconds // 86400) hours = int((uptime_seconds % 86400) // 3600) minutes = int((uptime_seconds % 3600) // 60) if days > 0: return f"{days}d {hours}h" elif hours > 0: return f"{hours}h {minutes}m" else: return f"{minutes}m" except Exception: return "Unknown" def _is_raspberry_pi(self): """Check if running on Raspberry Pi""" try: with open('/proc/cpuinfo', 'r') as f: cpuinfo = f.read() return any(x in cpuinfo.lower() for x in ['raspberry', 'bcm', 'broadcom']) except: return False def check_service_health(self, url, port=None): """Check if a service is responding""" try: if port: url = f"http://{url}:{port}" response = requests.get(url, timeout=5) return response.status_code == 200 except Exception: return False # Configuration from environment variables - optimized for Raspberry Pi PORT = int(os.getenv('PORT', 8080)) # Use 8080 as default for Pi HOST = os.getenv('HOST', '0.0.0.0') DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' REFRESH_INTERVAL = int(os.getenv('REFRESH_INTERVAL', 60)) # Longer refresh for Pi # Raspberry Pi specific optimizations PI_OPTIMIZATIONS = os.getenv('PI_OPTIMIZATIONS', 'True').lower() == 'true' # Initialize Docker service docker_service = DockerService() # Initialize Portainer client portainer_client = None portainer_url = os.getenv('PORTAINER_URL', 'http://localhost:9000') portainer_username = os.getenv('PORTAINER_USERNAME') portainer_password = os.getenv('PORTAINER_PASSWORD') if portainer_username and portainer_password: portainer_client = PortainerClient(portainer_url, portainer_username, portainer_password) if portainer_client.authenticate(): logging.info("Portainer client authenticated successfully") else: logging.error("Portainer authentication failed - using standard Docker API") portainer_client = None @app.route('/') def dashboard(): """Serve the dashboard""" return send_from_directory(os.path.dirname(os.path.abspath(__file__)), 'template.html') @app.route('/api/containers') def get_containers(): """Get all containers""" containers = docker_service.get_containers() return jsonify(containers) @app.route('/api/system') def get_system_info(): """Get system information""" system_info = docker_service.get_system_info() return jsonify(system_info) @app.route('/api/health/') def check_container_health(container_name): """Check health of a specific container""" try: # Get application from database for custom health check URL app = db.get_application(container_name) if app and app.get('url') != '#': url = app['url'] else: url = f'http://{container_name}.local' try: response = requests.get(url, timeout=5) return jsonify({ 'healthy': response.status_code < 400, 'status_code': response.status_code, 'url': url }) except requests.RequestException: return jsonify({ 'healthy': False, 'error': 'Service not responding', 'url': url }) except Exception as e: return jsonify({ 'healthy': False, 'error': str(e) }), 500 @app.route('/api/portainer/containers') def get_portainer_containers(): """Get containers from Portainer if available""" try: # This would need Portainer API configuration # For now, return standard Docker containers return get_containers() except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/portainer/containers') def get_portainer_containers_list(): """Get containers from Portainer""" if not portainer_client: return jsonify({'error': 'Portainer not configured'}), 503 try: containers = portainer_client.get_containers() return jsonify(containers) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/portainer/endpoints') def get_portainer_endpoints(): """Get Portainer endpoints""" if not portainer_client: return jsonify({'error': 'Portainer not configured'}), 503 try: endpoints = portainer_client.get_endpoints() return jsonify(endpoints) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/portainer/stacks') def get_portainer_stacks(): """Get Docker stacks from Portainer""" if not portainer_client: return jsonify({'error': 'Portainer not configured'}), 503 try: stacks = portainer_client.get_stacks() return jsonify(stacks) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/portainer/container//', methods=['POST']) def manage_portainer_container(action, container_id): """Manage containers via Portainer""" if not portainer_client: return jsonify({'error': 'Portainer not configured'}), 503 if action not in ['start', 'stop', 'restart']: return jsonify({'error': 'Invalid action'}), 400 try: success = False if action == 'start': success = portainer_client.start_container(container_id) elif action == 'stop': success = portainer_client.stop_container(container_id) elif action == 'restart': success = portainer_client.restart_container(container_id) return jsonify({'success': success}) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/applications') def get_applications(): """Get all applications from database""" try: applications = db.get_applications() # Sync with current containers containers = docker_service.get_containers() container_map = {c['id']: c for c in containers} # Update applications with live container data for app in applications: container = container_map.get(app['container_id']) if container: app.update({ 'status': container.get('status', 'unknown'), 'uptime': container.get('uptime', 'unknown'), 'image': container.get('image', ''), 'ports': container.get('ports', []) }) return jsonify({'applications': applications}) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/applications/', methods=['GET', 'PUT', 'DELETE']) def manage_application(container_id): """Manage individual application""" try: if request.method == 'GET': app = db.get_application(container_id) if app: return jsonify(app) return jsonify({'error': 'Application not found'}), 404 elif request.method == 'PUT': data = request.json db.update_application(container_id, data) return jsonify({'success': True, 'message': 'Application updated'}) elif request.method == 'DELETE': db.delete_application(container_id) return jsonify({'success': True, 'message': 'Application deleted'}) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/applications/sync', methods=['POST']) def sync_applications(): """Sync applications with current containers""" try: containers = docker_service.get_containers() # Sync each container with database for container in containers: db.upsert_application({ 'id': container['id'], 'name': container['name'], 'description': container['image'], 'url': f"http://localhost:{container['ports'][0]['host_port']}" if container['ports'] else '#' }) return jsonify({'success': True, 'message': 'Applications synced successfully'}) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/containers//start', methods=['POST']) def start_container(container_id): """Start a specific container""" try: if docker_service.client is None: return jsonify({'success': False, 'error': 'Docker client not available'}), 500 container = docker_service.client.containers.get(container_id) container.start() # Update database with new status db.update_application(container_id, {'status': 'running'}) return jsonify({'success': True, 'message': 'Container started successfully'}) except Exception as e: logging.error(f"Error starting container {container_id}: {e}") return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/api/containers//stop', methods=['POST']) def stop_container(container_id): """Stop a specific container""" try: if docker_service.client is None: return jsonify({'success': False, 'error': 'Docker client not available'}), 500 container = docker_service.client.containers.get(container_id) container.stop() # Update database with new status db.update_application(container_id, {'status': 'exited'}) return jsonify({'success': True, 'message': 'Container stopped successfully'}) except Exception as e: logging.error(f"Error stopping container {container_id}: {e}") return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/api/containers//restart', methods=['POST']) def restart_container(container_id): """Restart a specific container""" try: if docker_service.client is None: return jsonify({'success': False, 'error': 'Docker client not available'}), 500 container = docker_service.client.containers.get(container_id) container.restart() # Update database with new status db.update_application(container_id, {'status': 'running'}) return jsonify({'success': True, 'message': 'Container restarted successfully'}) except Exception as e: logging.error(f"Error restarting container {container_id}: {e}") return jsonify({'success': False, 'error': str(e)}), 500 if __name__ == '__main__': # Raspberry Pi optimizations if PI_OPTIMIZATIONS: # Reduce logging for production logging.getLogger('werkzeug').setLevel(logging.ERROR) logging.getLogger('urllib3').setLevel(logging.ERROR) # Use threaded mode for better performance on Pi app.run(host=HOST, port=PORT, debug=DEBUG, threaded=True) else: app.run(host=HOST, port=PORT, debug=DEBUG)