Initial Commit
This commit is contained in:
392
api.py
Normal file
392
api.py
Normal file
@@ -0,0 +1,392 @@
|
||||
#!/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
|
||||
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:
|
||||
memory = psutil.virtual_memory()
|
||||
disk = psutil.disk_usage('/')
|
||||
|
||||
return {
|
||||
'cpu_percent': psutil.cpu_percent(),
|
||||
'memory': {
|
||||
'total': memory.total,
|
||||
'available': memory.available,
|
||||
'percent': memory.percent,
|
||||
'used': memory.used
|
||||
},
|
||||
'disk': {
|
||||
'total': disk.total,
|
||||
'used': disk.used,
|
||||
'free': disk.free,
|
||||
'percent': (disk.used / disk.total) * 100
|
||||
},
|
||||
'uptime': self._get_system_uptime()
|
||||
}
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting system info: {e}")
|
||||
return {}
|
||||
|
||||
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 render_template('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
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host=HOST, port=PORT, debug=DEBUG)
|
||||
Reference in New Issue
Block a user