Files
Homepage/api.py
Aodhan Collins f6528b2590 Stats fix
2025-07-27 02:10:52 +01:00

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)