From bb1a3c9400eee6331db9b58221b78ed062e856c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD?= Date: Fri, 6 Feb 2026 09:02:59 +0300 Subject: [PATCH] docker environment control improvement --- APP_CORE/Dockerfile.api | 13 ++- APP_CORE/Dockerfile.gatherer | 11 +- APP_CORE/config.ini | 22 +--- APP_CORE/db.py | 8 +- APP_CORE/openvpn_api_v3.py | 179 ++++---------------------------- APP_CORE/openvpn_gatherer_v3.py | 16 +-- README.md | 77 ++++++++------ docker-compose.yml | 13 ++- 8 files changed, 114 insertions(+), 225 deletions(-) diff --git a/APP_CORE/Dockerfile.api b/APP_CORE/Dockerfile.api index e7bfe51..085813b 100644 --- a/APP_CORE/Dockerfile.api +++ b/APP_CORE/Dockerfile.api @@ -1,3 +1,9 @@ +# APP_CORE API +# Supported ENV overrides (Format: OVPMON_{SECTION}_{KEY}): +# API: OVPMON_API_HOST, OVPMON_API_PORT, OVPMON_API_DEBUG, OVPMON_API_SECRET_KEY +# MONITOR: OVPMON_OPENVPN_MONITOR_DB_PATH, OVPMON_OPENVPN_MONITOR_LOG_PATH, OVPMON_OPENVPN_MONITOR_CHECK_INTERVAL +# LOGGING: OVPMON_LOGGING_LEVEL, OVPMON_LOGGING_LOG_FILE +# RETENTION: OVPMON_RETENTION_RAW_RETENTION_DAYS, OVPMON_RETENTION_AGG_5M_RETENTION_DAYS, etc. FROM python:3.12-alpine WORKDIR /app @@ -6,11 +12,12 @@ WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -# Copy source code +# Copy application COPY . . -# Expose the port +# Ensure DB directory exists +RUN mkdir -p /app/db + EXPOSE 5001 -# Run the API CMD ["python", "openvpn_api_v3.py"] diff --git a/APP_CORE/Dockerfile.gatherer b/APP_CORE/Dockerfile.gatherer index 946b310..3c1885d 100644 --- a/APP_CORE/Dockerfile.gatherer +++ b/APP_CORE/Dockerfile.gatherer @@ -1,3 +1,8 @@ +# APP_CORE Gatherer +# Supported ENV overrides (Format: OVPMON_{SECTION}_{KEY}): +# MONITOR: OVPMON_OPENVPN_MONITOR_DB_PATH, OVPMON_OPENVPN_MONITOR_LOG_PATH, OVPMON_OPENVPN_MONITOR_CHECK_INTERVAL +# LOGGING: OVPMON_LOGGING_LEVEL, OVPMON_LOGGING_LOG_FILE +# RETENTION: OVPMON_RETENTION_RAW_RETENTION_DAYS, OVPMON_RETENTION_AGG_5M_RETENTION_DAYS, etc. FROM python:3.12-alpine WORKDIR /app @@ -6,8 +11,10 @@ WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -# Copy source code +# Copy application COPY . . -# Run the gatherer +# Ensure DB directory exists +RUN mkdir -p /app/db + CMD ["python", "openvpn_gatherer_v3.py"] diff --git a/APP_CORE/config.ini b/APP_CORE/config.ini index 3e7e5aa..595e489 100644 --- a/APP_CORE/config.ini +++ b/APP_CORE/config.ini @@ -1,27 +1,17 @@ [api] host = 0.0.0.0 -port = 5000 +port = 5001 debug = false secret_key = ovpmon-secret-change-me [openvpn_monitor] -log_path = /etc/openvpn/openvpn-status.log -db_path = /opt/ovpmon/openvpn_monitor.db +log_path = /var/log/openvpn/openvpn-status.log +db_path = openvpn_monitor.db check_interval = 10 -data_retention_days = 90 -cleanup_interval_hours = 24 [logging] level = INFO -log_file = /opt/ovpmon/openvpn_monitor.log - -[visualization] -refresh_interval = 5 -max_display_rows = 50 - -[certificates] -certificates_path = /opt/ovpn/pki/issued -certificate_extensions = crt +log_file = openvpn_gatherer.log [retention] raw_retention_days = 7 @@ -30,7 +20,3 @@ agg_15m_retention_days = 28 agg_1h_retention_days = 90 agg_6h_retention_days = 180 agg_1d_retention_days = 365 - -[pki] -pki_path = /opt/ovpn/pki -easyrsa_path = /opt/ovpn/easy-rsa diff --git a/APP_CORE/db.py b/APP_CORE/db.py index eebd7e1..cd7a318 100644 --- a/APP_CORE/db.py +++ b/APP_CORE/db.py @@ -13,7 +13,13 @@ class DatabaseManager: def load_config(self): if os.path.exists(self.config_file): self.config.read(self.config_file) - self.db_path = self.config.get('openvpn_monitor', 'db_path', fallback='openvpn_monitor.db') + + # Priority: ENV > Config File > Fallback + env_db_path = os.getenv('OVPMON_OPENVPN_MONITOR_DB_PATH') + if env_db_path: + self.db_path = env_db_path + else: + self.db_path = self.config.get('openvpn_monitor', 'db_path', fallback='openvpn_monitor.db') def get_connection(self): """Get a database connection""" diff --git a/APP_CORE/openvpn_api_v3.py b/APP_CORE/openvpn_api_v3.py index 44019d2..21e98b0 100644 --- a/APP_CORE/openvpn_api_v3.py +++ b/APP_CORE/openvpn_api_v3.py @@ -4,10 +4,7 @@ from datetime import datetime, timedelta, timezone from flask import Flask, jsonify, request, send_file from flask_cors import CORS import logging -import subprocess import os -from pathlib import Path -import re import jwt import pyotp import bcrypt @@ -31,6 +28,17 @@ app = Flask(__name__) CORS(app, resources={r"/api/*": {"origins": "*"}}, supports_credentials=True) class OpenVPNAPI: + def get_config_value(self, section, key, fallback=None): + try: + # Priority: ENV > Config File > Fallback + env_key = f"OVPMON_{section.upper()}_{key.upper()}".replace('-', '_').replace(' ', '_') + env_val = os.getenv(env_key) + if env_val is not None: + return env_val + return self.config.get(section, key, fallback=fallback) + except: + return fallback + def __init__(self, config_file='config.ini'): self.db_manager = DatabaseManager(config_file) self.db_manager.init_database() @@ -38,21 +46,10 @@ class OpenVPNAPI: self.config.read(config_file) # Paths - self.certificates_path = self.config.get('certificates', 'certificates_path', fallback='/etc/openvpn/certs') - self.easyrsa_path = self.config.get('pki', 'easyrsa_path', fallback='/etc/openvpn/easy-rsa') - self.pki_path = self.config.get('pki', 'pki_path', fallback='/etc/openvpn/pki') # Fixed default to match Settings - self.templates_path = self.config.get('api', 'templates_path', fallback='templates') - self.server_config_dir = self.config.get('server', 'config_dir', fallback='/etc/openvpn') - self.server_config_path = self.config.get('server', 'config_path', fallback=os.path.join(self.server_config_dir, 'server.conf')) # Specific file - self.public_ip = self.config.get('openvpn_monitor', 'public_ip', fallback='') - - self.cert_extensions = self.config.get('certificates', 'certificate_extensions', fallback='crt,pem,key').split(',') - self._cert_cache = {} + self.public_ip = self.get_config_value('openvpn_monitor', 'public_ip', fallback='') # Security - # Priority 1: Environment Variable - # Priority 2: Config file - self.secret_key = os.getenv('OVPMON_SECRET_KEY') or self.config.get('api', 'secret_key', fallback='ovpmon-secret-change-me') + self.secret_key = self.get_config_value('api', 'secret_key', fallback='ovpmon-secret-change-me') app.config['SECRET_KEY'] = self.secret_key # Ensure at least one user exists @@ -130,140 +127,7 @@ class OpenVPNAPI: conn.close() # --- БЛОК РАБОТЫ С СЕРТИФИКАТАМИ (Оставлен без изменений) --- - def parse_openssl_date(self, date_str): - try: - parts = date_str.split() - if len(parts[1]) == 1: - parts[1] = f' {parts[1]}' - normalized_date = ' '.join(parts) - return datetime.strptime(normalized_date, '%b %d %H:%M:%S %Y GMT') - except ValueError: - try: - return datetime.strptime(date_str, '%b %d %H:%M:%S %Y %Z') - except ValueError: - logger.warning(f"Could not parse date: {date_str}") - return datetime.min - - def calculate_days_remaining(self, not_after_str): - if not_after_str == 'N/A': return 'N/A' - try: - expiration_date = self.parse_openssl_date(not_after_str) - if expiration_date == datetime.min: return 'N/A' - days_remaining = (expiration_date - datetime.now()).days - if days_remaining < 0: return f"Expired ({abs(days_remaining)} days ago)" - else: return f"{days_remaining} days" - except Exception: return 'N/A' - - def extract_cert_info(self, cert_file): - try: - result = subprocess.run(['openssl', 'x509', '-in', cert_file, '-noout', '-text'], - capture_output=True, text=True, check=True) - output = result.stdout - data = {'file': os.path.basename(cert_file), 'file_path': cert_file, 'subject': 'N/A', - 'issuer': 'N/A', 'not_after': 'N/A', 'not_before': 'N/A', 'serial': 'N/A', 'type': 'Unknown'} - - is_ca = False - extended_usage = "" - - for line in output.split('\n'): - line = line.strip() - if line.startswith('Subject:'): - data['subject'] = line.split('Subject:', 1)[1].strip() - cn_match = re.search(r'CN\s*=\s*([^,]+)', data['subject']) - if cn_match: data['common_name'] = cn_match.group(1).strip() - elif 'Not After' in line: - data['not_after'] = line.split(':', 1)[1].strip() - elif 'Not Before' in line: - data['not_before'] = line.split(':', 1)[1].strip() - elif 'Serial Number:' in line: - data['serial'] = line.split(':', 1)[1].strip() - elif 'CA:TRUE' in line: - is_ca = True - elif 'TLS Web Server Authentication' in line: - extended_usage += "Server " - elif 'TLS Web Client Authentication' in line: - extended_usage += "Client " - - # Determine Type - if is_ca: - data['type'] = 'CA' - elif 'Server' in extended_usage: - data['type'] = 'Server' - elif 'Client' in extended_usage: - data['type'] = 'Client' - elif 'server' in data.get('common_name', '').lower(): - data['type'] = 'Server' - else: - data['type'] = 'Client' # Default to client if ambiguous - - if data['not_after'] != 'N/A': - data['sort_date'] = self.parse_openssl_date(data['not_after']).isoformat() - else: - data['sort_date'] = datetime.min.isoformat() - - # Parse dates for UI - if data['not_after'] != 'N/A': - dt = self.parse_openssl_date(data['not_after']) - data['expires_iso'] = dt.isoformat() - - if data['not_before'] != 'N/A': - dt = self.parse_openssl_date(data['not_before']) - data['issued_iso'] = dt.isoformat() - - data['days_remaining'] = self.calculate_days_remaining(data['not_after']) - data['is_expired'] = 'Expired' in data['days_remaining'] - - # State for UI - if data['is_expired']: - data['state'] = 'Expired' - else: - data['state'] = 'Valid' - - return data - except Exception as e: - logger.error(f"Error processing {cert_file}: {e}") - return None - - def get_certificates_info(self): - cert_path = Path(self.certificates_path) - if not cert_path.exists(): return [] - - cert_files = [] - for ext in self.cert_extensions: - cert_files.extend(cert_path.rglob(f'*.{ext.strip()}')) - - current_valid_files = set() - cert_data = [] - - for cert_file_path in cert_files: - cert_file = str(cert_file_path) - current_valid_files.add(cert_file) - - try: - mtime = os.path.getmtime(cert_file) - - # Check cache - cached = self._cert_cache.get(cert_file) - if cached and cached['mtime'] == mtime: - cert_data.append(cached['data']) - else: - # Parse and update cache - parsed_data = self.extract_cert_info(cert_file) - if parsed_data: - self._cert_cache[cert_file] = { - 'mtime': mtime, - 'data': parsed_data - } - cert_data.append(parsed_data) - except OSError: - continue - - # Prune cache for deleted files - for cached_file in list(self._cert_cache.keys()): - if cached_file not in current_valid_files: - del self._cert_cache[cached_file] - - return cert_data + # ----------------------------------------------------------- # ----------------------------------------------------------- def get_current_stats(self): @@ -1109,14 +973,7 @@ def get_client_stats(common_name): logger.error(f"API Error: {e}") return jsonify({'success': False, 'error': str(e)}), 500 -@app.route('/api/v1/certificates', methods=['GET']) -@token_required -def get_certificates(): - try: - data = api.get_certificates_info() - return jsonify({'success': True, 'data': data}) - except Exception as e: - return jsonify({'success': False, 'error': str(e)}), 500 + @app.route('/api/v1/clients', methods=['GET']) @token_required @@ -1180,9 +1037,9 @@ def get_sessions(): if __name__ == "__main__": - host = api.config.get('api', 'host', fallback='0.0.0.0') - port = 5001 # Используем 5001, чтобы не конфликтовать, если что-то уже есть на 5000 - debug = api.config.getboolean('api', 'debug', fallback=False) + host = api.get_config_value('api', 'host', fallback='0.0.0.0') + port = int(api.get_config_value('api', 'port', fallback=5001)) + debug = api.get_config_value('api', 'debug', fallback='false').lower() == 'true' logger.info(f"Starting API on {host}:{port}") app.run(host=host, port=port, debug=debug) \ No newline at end of file diff --git a/APP_CORE/openvpn_gatherer_v3.py b/APP_CORE/openvpn_gatherer_v3.py index e8805e1..0e8af92 100644 --- a/APP_CORE/openvpn_gatherer_v3.py +++ b/APP_CORE/openvpn_gatherer_v3.py @@ -142,14 +142,8 @@ class OpenVPNDataGatherer: 'agg_6h_retention_days': '180', # 6 месяцев 'agg_1d_retention_days': '365' # 12 месяцев }, - 'visualization': { - 'refresh_interval': '5', - 'max_display_rows': '50' - }, - 'certificates': { - 'certificates_path': '/opt/ovpn/pki/issued', - 'certificate_extensions': 'crt' - } + 'visualization': {}, + 'certificates': {} } try: @@ -214,6 +208,12 @@ class OpenVPNDataGatherer: def get_config_value(self, section, key, default=None): try: + # Priority: ENV > Config File > Fallback + # Format: OVPMON_SECTION_KEY (all uppercase, underscores for spaces/dashes) + env_key = f"OVPMON_{section.upper()}_{key.upper()}".replace('-', '_').replace(' ', '_') + env_val = os.getenv(env_key) + if env_val is not None: + return env_val return self.config.get(section, key, fallback=default) except: return default diff --git a/README.md b/README.md index eb49362..7bdc230 100644 --- a/README.md +++ b/README.md @@ -2,61 +2,78 @@ A modern, full-stack management solution for OpenVPN servers. It combines real-time traffic monitoring, historical analytics, and comprehensive user profile/PKI management into a unified web interface. -## �️ Project Architecture +## 🏗️ Project Architecture -The project is modularized into three core components: +The project is modularized into four core microservices: -| Component | Directory | Description | +| Component | Directory | Service Name | Description | +| :--- | :--- | :--- | :--- | +| **User Interface** | `APP_UI/` | `ovp-ui` | Vue 3 + Vite SPA served via Nginx. | +| **Monitoring API** | `APP_CORE/` | `ovp-api` | Flask API for real-time stats and sessions. | +| **Data Gatherer** | `APP_CORE/` | `ovp-gatherer` | Background service for traffic log aggregation & TSDB. | +| **Profiler** | `APP_PROFILER/` | `ovp-profiler` | FastAPI module for PKI, Certificates, and VPN control. | + +## 📦 Quick Start (Docker) + +The recommended way to deploy is using Docker Compose: + +1. **Clone the repository** +2. **Start all services**: + ```bash + docker-compose up -d --build + ``` +3. **Access the Dashboard**: Open `http://localhost` in your browser. + +## ⚙️ Configuration + +The system is highly configurable via environment variables in `docker-compose.yml`. All variables follow the `OVPMON_{SECTION}_{KEY}` format. + +### Key Environment Variables + +| Variable | Description | Default Value | | :--- | :--- | :--- | -| **Core Monitoring** | `APP_CORE/` | Flask-based API (v3) for log parsing, real-time stats, and historical TSDB. | -| **Profiler** | `APP_PROFILER/` | FastAPI-based module for managing PKI, Certificates, and Server Configs. | -| **User Interface** | `APP_UI/` | Vue 3 + Vite Single Page Application (SPA) serving as the unified dashboard. | +| `OVPMON_API_SECRET_KEY` | JWT Secret Key | `ovpmon-secret-change-me` | +| `OVPMON_API_PORT` | Monitoring API Port | `5001` | +| `OVPMON_OPENVPN_MONITOR_DB_PATH` | Path to SQLite DB | `/app/db/openvpn_monitor.db` | +| `OVPMON_OPENVPN_MONITOR_LOG_PATH`| Path to OpenVPN status log | `/var/log/openvpn/openvpn-status.log` | +| `OVPMON_LOGGING_LEVEL` | Logging level (INFO/DEBUG) | `INFO` | ## 📚 Documentation -Detailed documentation has been moved to the `DOCS/` directory. +Detailed documentation is available in the `DOCS/` directory: -- **[Installation & Deployment](DOCS/General/Deployment.md)**: Setup guide for Linux (Alpine/Debian). -- **[Service Management](DOCS/General/Service_Management.md)**: Configuring Systemd/OpenRC services. -- **[Security & Auth](DOCS/General/Security_Architecture.md)**: 2FA, JWT, and Security details. +- **[Deployment Guide](DOCS/General/Deployment.md)**: Manual setup for Linux. +- **[Security Architecture](DOCS/General/Security_Architecture.md)**: 2FA, JWT, and Security. +- **[API Reference](DOCS/Core_Monitoring/API_Reference.md)**: Core Monitoring endpoints. -### API References -- **[Core Monitoring API](DOCS/Core_Monitoring/API_Reference.md)**: Endpoints for stats, sessions, and history. -- **[Profiler Management API](DOCS/Profiler_Management/API_Reference.md)**: Endpoints for profiles, system config, and control. +## 🛠️ Development (Manual) -## 🚀 Quick Start (Dev Mode) +If you wish to run services manually for development: -### 1. Core API (Flask) +### 1. Core API & Gatherer ```bash cd APP_CORE -python3 -m venv venv -source venv/bin/activate pip install -r requirements.txt -python3 openvpn_api_v3.py -# Runs on :5001 (Monitoring) +python3 openvpn_api_v3.py # APP_CORE API (:5001) +python3 openvpn_gatherer_v3.py # APP_CORE Gatherer ``` -### 2. Profiler API (FastAPI) +### 2. Profiler API ```bash cd APP_PROFILER -python3 -m venv venv -source venv/bin/activate pip install -r requirements.txt -python3 main.py -# Runs on :8000 (Management) +python3 main.py # APP_PROFILER API (:8000) ``` -### 3. Frontend (Vue 3) +### 3. Frontend ```bash cd APP_UI -npm install -npm run dev -# Runs on localhost:5173 +npm install && npm run dev # UI (:5173) ``` --- ## ⚠️ Important Notes -1. **Environment**: Production deployment relies on Nginx to proxy requests to the backend services. See the [Deployment Guide](DOCS/General/Deployment.md). -2. **Permissions**: The backend requires `sudo` or root privileges to manage OpenVPN processes and write to `/etc/openvpn`. +1. **Permissions**: The Profiler container requires `NET_ADMIN` capabilities and access to `/dev/net/tun`. +2. **Cleanup**: Certificate management and legacy visualization settings have been moved or removed from the Core module. diff --git a/docker-compose.yml b/docker-compose.yml index 5959aca..4bd1fd7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,9 +19,13 @@ services: container_name: ovp-gatherer volumes: - ovp_logs:/var/log/openvpn - - db_data:/app/db # Assuming APP_CORE looks for DB in /app/db + - db_data:/app/db depends_on: - app-profiler + environment: + - OVPMON_OPENVPN_MONITOR_DB_PATH=/app/db/openvpn_monitor.db + - OVPMON_OPENVPN_MONITOR_LOG_PATH=/var/log/openvpn/openvpn-status.log + - OVPMON_LOGGING_LEVEL=INFO networks: - ovp-net @@ -37,7 +41,12 @@ services: networks: - ovp-net environment: - - JWT_SECRET=${JWT_SECRET:-supersecret} + - OVPMON_API_SECRET_KEY=${JWT_SECRET:-supersecret} + - OVPMON_API_PORT=5001 + - OVPMON_OPENVPN_MONITOR_DB_PATH=/app/db/openvpn_monitor.db + - OVPMON_LOGGING_LEVEL=INFO + depends_on: + - app-gatherer app-profiler: build: ./APP_PROFILER