commit 6319df7d2983d490f9ce5d59c01e9090f62f75be Author: Aodhan Collins Date: Sun Jul 27 01:17:19 2025 +0100 Initial Commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..14675c9 --- /dev/null +++ b/.env.example @@ -0,0 +1,19 @@ +# Docker Dashboard Configuration +# Copy this file to .env and customize the values + +# Server Configuration +PORT=5000 +HOST=0.0.0.0 +DEBUG=True + +# Portainer Integration (Optional) +PORTAINER_URL=https://port.liveaodh.com +PORTAINER_USERNAME=aodhan +PORTAINER_PASSWORD=8yFdXkx274aSRX + +# Docker Configuration +DOCKER_HOST=unix:///var/run/docker.sock + +# Application Settings +REFRESH_INTERVAL=30 +MAX_CONTAINERS=50 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..04f235b --- /dev/null +++ b/.gitignore @@ -0,0 +1,121 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Virtual Environment +venv/ +env/ +ENV/ +env.bak/ +venv.bak/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Database +*.db +*.sqlite +*.sqlite3 + +# Logs +*.log +logs/ + +# Environment Variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Runtime files +*.pid +*.sock + +# Docker +.dockerignore + +# Node.js (if any) +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# Project specific +dashboard.db +dashboard.pid +stop.sh +logs/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..7208f96 --- /dev/null +++ b/README.md @@ -0,0 +1,234 @@ +# Docker Dashboard + +A modern, responsive web dashboard for managing and monitoring Docker containers with SQLite database integration and manual customization capabilities. + +## Features + +- **Dynamic Container Tiles**: Automatically discovers and displays all Docker containers +- **SQLite Database**: Persistent storage for custom tile configurations +- **Manual Customization**: Edit display names, icons, colors, descriptions, and URLs +- **Real-time Monitoring**: Live status updates and system metrics +- **Responsive Design**: Works on desktop, tablet, and mobile devices +- **Portainer Integration**: Optional enhanced functionality with Portainer API +- **Container Management**: Start, stop, and restart containers directly from the dashboard + +## Quick Start + +### Prerequisites +- Docker installed and running +- Python 3.7+ +- Docker socket access (typically requires sudo) + +### Installation + +1. **Clone or download the project** +2. **Install dependencies**: + ```bash + pip3 install -r requirements.txt + ``` + +3. **Configure environment**: + ```bash + cp .env.example .env + # Edit .env with your specific settings + ``` + +4. **Run the setup script** (recommended): + ```bash + chmod +x setup.sh + ./setup.sh + ``` + +5. **Or run manually**: + ```bash + sudo python3 api.py + ``` + +6. **Access the dashboard**: + Open your browser to `http://localhost:5000` + +### Manual Setup + +If you prefer to set up manually: + +1. **Install Python dependencies**: + ```bash + pip3 install flask flask-cors docker requests python-dotenv + ``` + +2. **Initialize database**: + ```bash + python3 -c "import database; db = database.DatabaseManager(); db.init_db()" + ``` + +3. **Start the application**: + ```bash + sudo python3 api.py + ``` + +## Configuration + +### Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `PORT` | Server port | 5000 | +| `HOST` | Server host | 0.0.0.0 | +| `DEBUG` | Debug mode | False | +| `REFRESH_INTERVAL` | Auto-refresh interval (seconds) | 30 | +| `PORTAINER_URL` | Portainer URL (optional) | - | +| `PORTAINER_USERNAME` | Portainer username | - | +| `PORTAINER_PASSWORD` | Portainer password | - | + +### Portainer Integration (Optional) + +To use Portainer for enhanced container management: + +1. Set up Portainer on your Docker host +2. Configure the environment variables: + ```bash + PORTAINER_URL=https://your-portainer-instance.com + PORTAINER_USERNAME=your-username + PORTAINER_PASSWORD=your-password + ``` + +## Usage + +### Dashboard Overview + +- **Server Information**: CPU, memory, and disk usage +- **Container Tiles**: Visual representation of all Docker containers +- **Live Status**: Real-time container status (running/stopped) +- **Uptime Display**: Container uptime information +- **Quick Actions**: Open, edit, or manage containers + +### Customizing Tiles + +1. **Click the "Edit" button** on any container tile +2. **Modify the fields**: + - **Display Name**: Custom name shown on the tile + - **Icon**: Font Awesome icon class (e.g., `fas fa-cube`) + - **Color**: Tailwind CSS color (e.g., `blue`, `green`, `red`) + - **Description**: Additional description text + - **URL**: Custom URL for the application +3. **Save changes** - updates are immediately reflected + +### Syncing Containers + +- **Automatic**: Containers sync every 30 seconds +- **Manual**: Click "Sync Apps" button to force refresh +- **New containers**: Automatically appear as new tiles +- **Removed containers**: Tiles are removed from display + +## API Endpoints + +### System Information +- `GET /api/system` - System metrics and uptime + +### Containers +- `GET /api/containers` - List all Docker containers +- `GET /api/health/` - Check container health + +### Applications (Database) +- `GET /api/applications` - Get all applications from database +- `GET /api/applications/` - Get specific application +- `PUT /api/applications/` - Update application data +- `DELETE /api/applications/` - Delete application +- `POST /api/applications/sync` - Sync with current containers + +### Portainer (Optional) +- `GET /api/portainer/containers` - Get containers via Portainer +- `POST /api/portainer/container//` - Manage containers + +## Database Schema + +The SQLite database (`dashboard.db`) contains the following table: + +```sql +CREATE TABLE applications ( + container_id TEXT PRIMARY KEY, + container_name TEXT NOT NULL, + display_name TEXT, + image TEXT, + status TEXT, + ports TEXT, + uptime TEXT, + icon TEXT DEFAULT 'fas fa-cube', + color TEXT DEFAULT 'gray', + description TEXT, + url TEXT, + custom_data TEXT +); +``` + +## File Structure + +``` +├── api.py # Main Flask API server +├── database.py # SQLite database manager +├── template.html # Frontend dashboard +├── portainer_integration.py # Portainer API integration +├── setup.sh # Setup and launch script +├── stop.sh # Stop script (generated) +├── .env # Environment variables (create from .env.example) +├── .env.example # Environment template +├── requirements.txt # Python dependencies +├── .gitignore # Git ignore rules +├── dashboard.db # SQLite database (created automatically) +└── logs/ + └── dashboard.log # Application logs +``` + +## Troubleshooting + +### Docker Permission Issues +If you see "Permission denied" errors: +```bash +# Add your user to docker group +sudo usermod -aG docker $USER +# Log out and back in +``` + +### Port Issues +If port 5000 is already in use: +```bash +# Edit .env file and change PORT +PORT=5001 +``` + +### Database Issues +To reset the database: +```bash +rm dashboard.db +python3 -c "import database; db = database.DatabaseManager(); db.init_db()" +``` + +### View Logs +```bash +tail -f logs/dashboard.log +``` + +## Development + +### Adding New Features +1. **Backend**: Modify `api.py` for new endpoints +2. **Database**: Update `database.py` for schema changes +3. **Frontend**: Edit `template.html` for UI changes + +### Contributing +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Test thoroughly +5. Submit a pull request + +## License + +This project is open source and available under the [MIT License](LICENSE). + +## Support + +For issues or questions: +1. Check the troubleshooting section above +2. Review the logs in `logs/dashboard.log` +3. Open an issue on the project repository \ No newline at end of file diff --git a/api.py b/api.py new file mode 100644 index 0000000..7c08c73 --- /dev/null +++ b/api.py @@ -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/') +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 + +if __name__ == '__main__': + app.run(host=HOST, port=PORT, debug=DEBUG) diff --git a/database.py b/database.py new file mode 100644 index 0000000..b5bdbfe --- /dev/null +++ b/database.py @@ -0,0 +1,194 @@ +import sqlite3 +import os +import json +from datetime import datetime + +class DatabaseManager: + def __init__(self, db_path='dashboard.db'): + self.db_path = db_path + self.init_database() + + def init_database(self): + """Initialize the database with required tables""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # Create applications table for storing custom app data + cursor.execute(''' + CREATE TABLE IF NOT EXISTS applications ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + container_id TEXT UNIQUE, + container_name TEXT NOT NULL, + display_name TEXT, + icon TEXT DEFAULT 'fas fa-cube', + color TEXT DEFAULT 'gray', + description TEXT, + url TEXT, + custom_data TEXT, -- JSON for additional custom fields + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # Create settings table for general configuration + cursor.execute(''' + CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + conn.commit() + conn.close() + + def get_connection(self): + """Get database connection""" + return sqlite3.connect(self.db_path) + + def upsert_application(self, container_data): + """Insert or update application data""" + conn = self.get_connection() + cursor = conn.cursor() + + container_id = container_data.get('id') + container_name = container_data.get('name') + display_name = container_data.get('display_name', container_name) + icon = container_data.get('icon', 'fas fa-cube') + color = container_data.get('color', 'gray') + description = container_data.get('description', container_data.get('image', '')) + url = container_data.get('url', '#') + custom_data = json.dumps(container_data.get('custom_data', {})) + + cursor.execute(''' + INSERT OR REPLACE INTO applications + (container_id, container_name, display_name, icon, color, description, url, custom_data, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + ''', (container_id, container_name, display_name, icon, color, description, url, custom_data)) + + conn.commit() + conn.close() + + def get_applications(self): + """Get all applications""" + conn = self.get_connection() + cursor = conn.cursor() + + cursor.execute(''' + SELECT container_id, container_name, display_name, icon, color, description, url, custom_data + FROM applications + ORDER BY display_name + ''') + + results = cursor.fetchall() + conn.close() + + applications = [] + for row in results: + app = { + 'container_id': row[0], + 'container_name': row[1], + 'display_name': row[2], + 'icon': row[3], + 'color': row[4], + 'description': row[5], + 'url': row[6], + 'custom_data': json.loads(row[7]) if row[7] else {} + } + applications.append(app) + + return applications + + def get_application(self, container_id): + """Get specific application by container_id""" + conn = self.get_connection() + cursor = conn.cursor() + + cursor.execute(''' + SELECT container_id, container_name, display_name, icon, color, description, url, custom_data + FROM applications + WHERE container_id = ? + ''', (container_id,)) + + result = cursor.fetchone() + conn.close() + + if result: + return { + 'container_id': result[0], + 'container_name': result[1], + 'display_name': result[2], + 'icon': result[3], + 'color': result[4], + 'description': result[5], + 'url': result[6], + 'custom_data': json.loads(result[7]) if result[7] else {} + } + + return None + + def update_application(self, container_id, updates): + """Update application data""" + conn = self.get_connection() + cursor = conn.cursor() + + set_clause = [] + values = [] + + for key, value in updates.items(): + if key == 'custom_data': + value = json.dumps(value) + set_clause.append(f"{key} = ?") + values.append(value) + + if set_clause: + set_clause.append("updated_at = CURRENT_TIMESTAMP") + values.append(container_id) + + query = f"UPDATE applications SET {', '.join(set_clause)} WHERE container_id = ?" + cursor.execute(query, values) + conn.commit() + + conn.close() + + def delete_application(self, container_id): + """Delete application""" + conn = self.get_connection() + cursor = conn.cursor() + + cursor.execute('DELETE FROM applications WHERE container_id = ?', (container_id,)) + conn.commit() + conn.close() + + def sync_with_containers(self, containers): + """Sync applications with current containers""" + conn = self.get_connection() + cursor = conn.cursor() + + # Get existing container IDs + cursor.execute('SELECT container_id FROM applications') + existing_ids = {row[0] for row in cursor.fetchall()} + + # Insert new containers + for container in containers: + container_id = container.get('id') + if container_id not in existing_ids: + self.upsert_application({ + 'id': container_id, + 'name': container.get('name', ''), + 'description': container.get('image', ''), + 'url': self._generate_url_from_ports(container.get('ports', [])) + }) + + conn.close() + + def _generate_url_from_ports(self, ports): + """Generate URL from container ports""" + if ports and len(ports) > 0: + port = ports[0] + return f"http://localhost:{port.get('host_port', port.get('private_port', 80))}" + return '#' + +# Global database instance +db = DatabaseManager() diff --git a/portainer_integration.py b/portainer_integration.py new file mode 100644 index 0000000..04a15ee --- /dev/null +++ b/portainer_integration.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +""" +Portainer Integration Module +Provides enhanced Docker management through Portainer API +""" + +import requests +import json +from typing import Dict, List, Optional +import os + +class PortainerClient: + def __init__(self, base_url: str, username: str = None, password: str = None): + self.base_url = base_url.rstrip('/') + self.username = username or os.getenv('PORTAINER_USERNAME') + self.password = password or os.getenv('PORTAINER_PASSWORD') + self.token = None + self.endpoint_id = None + + def authenticate(self) -> bool: + """Authenticate with Portainer API""" + try: + auth_url = f"{self.base_url}/api/auth" + payload = { + "username": self.username, + "password": self.password + } + + response = requests.post(auth_url, json=payload) + if response.status_code == 200: + self.token = response.json().get('jwt') + return True + return False + except Exception as e: + print(f"Portainer authentication failed: {e}") + return False + + def get_endpoints(self) -> List[Dict]: + """Get available endpoints""" + if not self.token: + return [] + + try: + headers = {'Authorization': f'Bearer {self.token}'} + response = requests.get(f"{self.base_url}/api/endpoints", headers=headers) + + if response.status_code == 200: + return response.json() + return [] + except Exception as e: + print(f"Error fetching endpoints: {e}") + return [] + + def get_containers(self, endpoint_id: int = None) -> List[Dict]: + """Get containers from Portainer""" + if not self.token: + return [] + + try: + if not endpoint_id: + endpoints = self.get_endpoints() + if endpoints: + endpoint_id = endpoints[0]['Id'] + else: + return [] + + headers = {'Authorization': f'Bearer {self.token}'} + url = f"{self.base_url}/api/endpoints/{endpoint_id}/docker/containers/json?all=true" + + response = requests.get(url, headers=headers) + + if response.status_code == 200: + containers = response.json() + return self._format_portainer_containers(containers) + return [] + except Exception as e: + print(f"Error fetching containers from Portainer: {e}") + return [] + + def _format_portainer_containers(self, containers: List[Dict]) -> List[Dict]: + """Format Portainer containers to match our standard format""" + formatted = [] + + for container in containers: + formatted_container = { + 'id': container.get('Id', '')[:12], + 'name': container.get('Names', [''])[0].lstrip('/'), + 'status': container.get('State', 'unknown'), + 'image': container.get('Image', ''), + 'ports': self._format_ports(container.get('Ports', [])), + 'created': container.get('Created', ''), + 'uptime': self._calculate_uptime(container), + 'labels': container.get('Labels', {}), + 'environment': [], + 'mounts': self._format_mounts(container.get('Mounts', [])), + 'networks': self._format_networks(container.get('NetworkSettings', {}).get('Networks', {})) + } + formatted.append(formatted_container) + + return formatted + + def _format_ports(self, ports: List[Dict]) -> List[Dict]: + """Format port mappings""" + formatted_ports = [] + for port in ports: + formatted_ports.append({ + 'container_port': f"{port.get('PrivatePort', '')}/{port.get('Type', 'tcp')}", + 'host_ip': port.get('IP', '0.0.0.0'), + 'host_port': str(port.get('PublicPort', '')) + }) + return formatted_ports + + def _format_mounts(self, mounts: List[Dict]) -> List[Dict]: + """Format volume mounts""" + formatted_mounts = [] + for mount in mounts: + formatted_mounts.append({ + 'type': mount.get('Type'), + 'source': mount.get('Source'), + 'destination': mount.get('Destination'), + 'mode': mount.get('Mode') + }) + return formatted_mounts + + def _format_networks(self, networks: Dict) -> List[Dict]: + """Format network information""" + formatted_networks = [] + for network_name, network_config in networks.items(): + formatted_networks.append({ + 'name': network_name, + 'ip_address': network_config.get('IPAddress'), + 'gateway': network_config.get('Gateway'), + 'mac_address': network_config.get('MacAddress') + }) + return formatted_networks + + def _calculate_uptime(self, container: Dict) -> str: + """Calculate uptime from container info""" + status = container.get('State') + if status != 'running': + return 'Down' + + # For now, return simplified uptime + # In a real implementation, you'd parse the Created field + return 'Running' + + def get_stacks(self, endpoint_id: int = None) -> List[Dict]: + """Get Docker stacks from Portainer""" + if not self.token: + return [] + + try: + if not endpoint_id: + endpoints = self.get_endpoints() + if endpoints: + endpoint_id = endpoints[0]['Id'] + else: + return [] + + headers = {'Authorization': f'Bearer {self.token}'} + url = f"{self.base_url}/api/endpoints/{endpoint_id}/docker/stacks" + + response = requests.get(url, headers=headers) + + if response.status_code == 200: + return response.json() + return [] + except Exception as e: + print(f"Error fetching stacks from Portainer: {e}") + return [] + + def get_volumes(self, endpoint_id: int = None) -> List[Dict]: + """Get Docker volumes from Portainer""" + if not self.token: + return [] + + try: + if not endpoint_id: + endpoints = self.get_endpoints() + if endpoints: + endpoint_id = endpoints[0]['Id'] + else: + return [] + + headers = {'Authorization': f'Bearer {self.token}'} + url = f"{self.base_url}/api/endpoints/{endpoint_id}/docker/volumes" + + response = requests.get(url, headers=headers) + + if response.status_code == 200: + volumes_data = response.json() + return volumes_data.get('Volumes', []) + return [] + except Exception as e: + print(f"Error fetching volumes from Portainer: {e}") + return [] + + def get_networks(self, endpoint_id: int = None) -> List[Dict]: + """Get Docker networks from Portainer""" + if not self.token: + return [] + + try: + if not endpoint_id: + endpoints = self.get_endpoints() + if endpoints: + endpoint_id = endpoints[0]['Id'] + else: + return [] + + headers = {'Authorization': f'Bearer {self.token}'} + url = f"{self.base_url}/api/endpoints/{endpoint_id}/docker/networks" + + response = requests.get(url, headers=headers) + + if response.status_code == 200: + return response.json() + return [] + except Exception as e: + print(f"Error fetching networks from Portainer: {e}") + return [] + + def start_container(self, container_id: str, endpoint_id: int = None) -> bool: + """Start a container""" + return self._container_action(container_id, 'start', endpoint_id) + + def stop_container(self, container_id: str, endpoint_id: int = None) -> bool: + """Stop a container""" + return self._container_action(container_id, 'stop', endpoint_id) + + def restart_container(self, container_id: str, endpoint_id: int = None) -> bool: + """Restart a container""" + return self._container_action(container_id, 'restart', endpoint_id) + + def _container_action(self, container_id: str, action: str, endpoint_id: int = None) -> bool: + """Perform container action""" + if not self.token: + return False + + try: + if not endpoint_id: + endpoints = self.get_endpoints() + if endpoints: + endpoint_id = endpoints[0]['Id'] + else: + return False + + headers = {'Authorization': f'Bearer {self.token}'} + url = f"{self.base_url}/api/endpoints/{endpoint_id}/docker/containers/{container_id}/{action}" + + response = requests.post(url, headers=headers) + return response.status_code == 204 + except Exception as e: + print(f"Error performing {action} on container: {e}") + return False diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..95317e8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +flask==2.3.3 +flask-cors==4.0.0 +docker==6.1.3 +psutil==5.9.5 +requests==2.31.0 +python-dotenv==1.0.0 diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..8c07f7f --- /dev/null +++ b/setup.sh @@ -0,0 +1,255 @@ +#!/bin/bash + +# Docker Dashboard Setup Script +# This script sets up and launches the Docker Dashboard as a background process + +set -e + +echo "🚀 Docker Dashboard Setup Script" +echo "================================" + +# Get script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Check if running as root for Docker access +check_permissions() { + if [[ $EUID -ne 0 ]]; then + print_warning "This script needs to run with sudo for Docker access" + print_warning "Rerunning with sudo..." + exec sudo "$0" "$@" + fi +} + +# Check if Docker is running +check_docker() { + if ! systemctl is-active --quiet docker; then + print_warning "Docker service is not running. Starting Docker..." + systemctl start docker + fi + + if ! docker info >/dev/null 2>&1; then + print_error "Docker is not accessible. Please check Docker installation" + exit 1 + fi + + print_status "Docker is running and accessible" +} + +# Install Python dependencies +install_dependencies() { + print_status "Installing Python dependencies..." + + # Check if pip is available + if ! command -v pip3 &> /dev/null; then + print_error "pip3 is not installed. Please install pip3 first" + exit 1 + fi + + # Install requirements + if [[ -f "requirements.txt" ]]; then + pip3 install -r requirements.txt + else + pip3 install flask flask-cors docker requests python-dotenv + fi + + print_status "Dependencies installed successfully" +} + +# Setup environment file +setup_environment() { + if [[ ! -f ".env" ]]; then + print_status "Creating .env file from template..." + cp .env.example .env + print_status "Please edit .env file with your specific configuration" + else + print_status ".env file already exists" + fi + + # Source the environment file + source .env + + # Set defaults if not provided + export PORT=${PORT:-5000} + export HOST=${HOST:-0.0.0.0} + export DEBUG=${DEBUG:-False} + export REFRESH_INTERVAL=${REFRESH_INTERVAL:-30} + + print_status "Environment configured" +} + +# Initialize database +initialize_database() { + print_status "Initializing database..." + + # Create database if it doesn't exist + if [[ ! -f "dashboard.db" ]]; then + python3 -c " +import database +db = database.DatabaseManager() +db.init_db() +print('Database initialized successfully') +" + else + print_status "Database already exists" + fi +} + +# Kill existing processes +kill_existing_processes() { + print_status "Checking for existing processes..." + + # Kill existing Python processes on the port + PID=$(lsof -t -i :${PORT} 2>/dev/null || echo "") + if [[ -n "$PID" ]]; then + print_warning "Killing existing process on port ${PORT} (PID: $PID)" + kill -TERM "$PID" 2>/dev/null || true + sleep 2 + kill -KILL "$PID" 2>/dev/null || true + fi +} + +# Launch application +launch_application() { + print_status "Launching Docker Dashboard..." + + # Create logs directory + mkdir -p logs + + # Launch the application in background + nohup python3 api.py > logs/dashboard.log 2>&1 & + + # Get the process ID + APP_PID=$! + + # Wait a moment for the app to start + sleep 3 + + # Check if the process is still running + if kill -0 "$APP_PID" 2>/dev/null; then + print_status "Application started successfully (PID: $APP_PID)" + print_status "Dashboard available at: http://localhost:${PORT}" + print_status "Logs available at: logs/dashboard.log" + + # Save PID to file + echo "$APP_PID" > dashboard.pid + + print_status "" + print_status "To stop the application, run:" + print_status " sudo ./stop.sh" + print_status "" + print_status "To view logs:" + print_status " tail -f logs/dashboard.log" + else + print_error "Failed to start application. Check logs/dashboard.log for details" + exit 1 + fi +} + +# Create stop script +create_stop_script() { + cat > stop.sh << 'EOF' +#!/bin/bash + +# Stop Docker Dashboard Script + +if [[ -f "dashboard.pid" ]]; then + PID=$(cat dashboard.pid) + if kill -0 "$PID" 2>/dev/null; then + echo "Stopping Docker Dashboard (PID: $PID)..." + kill -TERM "$PID" + sleep 2 + + if kill -0 "$PID" 2>/dev/null; then + echo "Force stopping..." + kill -KILL "$PID" + fi + + rm dashboard.pid + echo "Application stopped successfully" + else + echo "Application is not running" + rm -f dashboard.pid + fi +else + echo "PID file not found. Attempting to kill by port..." + PID=$(lsof -t -i :5000 2>/dev/null || echo "") + if [[ -n "$PID" ]]; then + echo "Stopping process on port 5000 (PID: $PID)..." + kill -TERM "$PID" + sleep 2 + + if kill -0 "$PID" 2>/dev/null; then + echo "Force stopping..." + kill -KILL "$PID" + fi + + echo "Application stopped successfully" + else + echo "No application found running on port 5000" + fi +fi +EOF + + chmod +x stop.sh + print_status "Stop script created: stop.sh" +} + +# Main execution +main() { + print_status "Starting Docker Dashboard setup..." + + check_permissions + check_docker + install_dependencies + setup_environment + initialize_database + kill_existing_processes + create_stop_script + launch_application + + print_status "" + print_status "🎉 Docker Dashboard setup complete!" + print_status "Dashboard is running at: http://localhost:${PORT}" + print_status "" + print_status "Next steps:" + print_status "1. Open your browser to http://localhost:${PORT}" + print_status "2. Click 'Sync Apps' to load your Docker containers" + print_status "3. Edit tiles by clicking the 'Edit' button on each tile" + print_status "4. Use ./stop.sh to stop the application" +} + +# Handle command line arguments +case "${1:-}" in + "--help"|"-h") + echo "Usage: $0 [--help]" + echo "" + echo "This script sets up and launches the Docker Dashboard" + echo "" + echo "Options:" + echo " --help, -h Show this help message" + exit 0 + ;; + *) + main + ;; +esac diff --git a/template.html b/template.html new file mode 100644 index 0000000..0c1c63f --- /dev/null +++ b/template.html @@ -0,0 +1,379 @@ + + + + + + Home Server Dashboard + + + + + +
+ +
+

Home Server Dashboard

+

Centralized access to your self-hosted applications

+
+ + +
+
+ +
+ +
+
+

Server Information

+
+
+
+
+ CPU Usage + -- +
+
+
+
+
+ +
+
+ Memory Usage + -- +
+
+
+
+
+ +
+
+ Disk Usage + -- +
+
+
+
+
+ +
+
+ Uptime + -- +
+
+
+
+ + +
+
+
+

Hosted Applications

+
+ + 0 apps +
+
+
+
+
+
+ +

Loading applications...

+
+
+
+
+
+ + +
+
+

Recent Activity

+
+
+
+
+ +
+
+

Dashboard initialized

+

Just now - System ready

+
+
+
+
+ + +
+

Home Server Dashboard v1.0.0 | Last updated: Just now

+

© 2023 Home Server | Server Settings

+
+
+ + + + \ No newline at end of file