#!/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""" try: # CPU usage cpu_percent = psutil.cpu_percent(interval=1) # Memory usage memory = psutil.virtual_memory() # Disk usage - try multiple paths for different systems disk_info = None disk_paths = ['/'] # Add common mount points for different Linux distributions if os.path.exists('/home'): disk_paths.append('/home') if os.path.exists('/var'): disk_paths.append('/var') if os.path.exists('/opt'): disk_paths.append('/opt') # Try each path until we find one that works for path in disk_paths: try: disk_info = psutil.disk_usage(path) break except (OSError, IOError, PermissionError): continue # Fallback to root directory if disk_info is None: try: disk_info = psutil.disk_usage('/') except (OSError, IOError) as e: logging.error(f"Cannot get disk usage: {e}") # Return zero values as fallback disk_info = type('DiskInfo', (), { 'total': 0, 'used': 0, 'free': 0 })() # Get load average (works on both Debian and Fedora) try: import os load_avg = os.getloadavg() except (AttributeError, OSError): load_avg = (0.0, 0.0, 0.0) # Get swap memory swap = psutil.swap_memory() # Format disk usage for display def format_bytes(bytes_value): """Format bytes to human readable format""" for unit in ['B', 'KB', 'MB', 'GB', 'TB']: if bytes_value < 1024.0: return f"{bytes_value:.1f} {unit}" bytes_value /= 1024.0 return f"{bytes_value:.1f} PB" return { 'cpu': { 'percent': cpu_percent, 'cores': psutil.cpu_count(), 'load_avg': load_avg }, 'memory': { 'total': memory.total, 'available': memory.available, 'used': memory.used, 'percent': memory.percent, 'formatted_total': format_bytes(memory.total), 'formatted_used': format_bytes(memory.used), 'formatted_available': format_bytes(memory.available) }, 'swap': { 'total': swap.total, 'used': swap.used, 'percent': swap.percent }, 'disk': { 'total': disk_info.total, 'used': disk_info.used, 'free': disk_info.free, 'percent': (disk_info.used / disk_info.total) * 100 if disk_info.total > 0 else 0, 'formatted_total': format_bytes(disk_info.total), 'formatted_used': format_bytes(disk_info.used), 'formatted_free': format_bytes(disk_info.free) }, 'uptime': self._get_system_uptime(), 'platform': { 'system': platform.system(), 'release': platform.release(), 'machine': platform.machine() } } except Exception as e: logging.error(f"Error getting system info: {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' } def _get_system_uptime(self): """Get system uptime""" 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) return f"{days}d {hours}h {minutes}m" except Exception: return "Unknown" 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 PORT = int(os.getenv('PORT', 5000)) HOST = os.getenv('HOST', '0.0.0.0') DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' REFRESH_INTERVAL = int(os.getenv('REFRESH_INTERVAL', 30)) # 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__': app.run(host=HOST, port=PORT, debug=DEBUG)