docker environment control improvement

This commit is contained in:
Антон
2026-02-06 09:02:59 +03:00
parent 0d0761cb31
commit bb1a3c9400
8 changed files with 114 additions and 225 deletions

View File

@@ -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"]

View File

@@ -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"]

View File

@@ -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

View File

@@ -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"""

View File

@@ -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)

View File

@@ -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

View File

@@ -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.
## <EFBFBD> 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.

View File

@@ -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