543 lines
20 KiB
Python
543 lines
20 KiB
Python
#!/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/<container_name>')
|
|
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/<action>/<container_id>', 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/<container_id>', 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/<container_id>/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/<container_id>/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/<container_id>/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)
|