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 FROM python:3.12-alpine
WORKDIR /app WORKDIR /app
@@ -6,11 +12,12 @@ WORKDIR /app
COPY requirements.txt . COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# Copy source code # Copy application
COPY . . COPY . .
# Expose the port # Ensure DB directory exists
RUN mkdir -p /app/db
EXPOSE 5001 EXPOSE 5001
# Run the API
CMD ["python", "openvpn_api_v3.py"] 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 FROM python:3.12-alpine
WORKDIR /app WORKDIR /app
@@ -6,8 +11,10 @@ WORKDIR /app
COPY requirements.txt . COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# Copy source code # Copy application
COPY . . COPY . .
# Run the gatherer # Ensure DB directory exists
RUN mkdir -p /app/db
CMD ["python", "openvpn_gatherer_v3.py"] CMD ["python", "openvpn_gatherer_v3.py"]

View File

@@ -1,27 +1,17 @@
[api] [api]
host = 0.0.0.0 host = 0.0.0.0
port = 5000 port = 5001
debug = false debug = false
secret_key = ovpmon-secret-change-me secret_key = ovpmon-secret-change-me
[openvpn_monitor] [openvpn_monitor]
log_path = /etc/openvpn/openvpn-status.log log_path = /var/log/openvpn/openvpn-status.log
db_path = /opt/ovpmon/openvpn_monitor.db db_path = openvpn_monitor.db
check_interval = 10 check_interval = 10
data_retention_days = 90
cleanup_interval_hours = 24
[logging] [logging]
level = INFO level = INFO
log_file = /opt/ovpmon/openvpn_monitor.log log_file = openvpn_gatherer.log
[visualization]
refresh_interval = 5
max_display_rows = 50
[certificates]
certificates_path = /opt/ovpn/pki/issued
certificate_extensions = crt
[retention] [retention]
raw_retention_days = 7 raw_retention_days = 7
@@ -30,7 +20,3 @@ agg_15m_retention_days = 28
agg_1h_retention_days = 90 agg_1h_retention_days = 90
agg_6h_retention_days = 180 agg_6h_retention_days = 180
agg_1d_retention_days = 365 agg_1d_retention_days = 365
[pki]
pki_path = /opt/ovpn/pki
easyrsa_path = /opt/ovpn/easy-rsa

View File

@@ -13,6 +13,12 @@ class DatabaseManager:
def load_config(self): def load_config(self):
if os.path.exists(self.config_file): if os.path.exists(self.config_file):
self.config.read(self.config_file) self.config.read(self.config_file)
# 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') self.db_path = self.config.get('openvpn_monitor', 'db_path', fallback='openvpn_monitor.db')
def get_connection(self): def get_connection(self):

View File

@@ -4,10 +4,7 @@ from datetime import datetime, timedelta, timezone
from flask import Flask, jsonify, request, send_file from flask import Flask, jsonify, request, send_file
from flask_cors import CORS from flask_cors import CORS
import logging import logging
import subprocess
import os import os
from pathlib import Path
import re
import jwt import jwt
import pyotp import pyotp
import bcrypt import bcrypt
@@ -31,6 +28,17 @@ app = Flask(__name__)
CORS(app, resources={r"/api/*": {"origins": "*"}}, supports_credentials=True) CORS(app, resources={r"/api/*": {"origins": "*"}}, supports_credentials=True)
class OpenVPNAPI: 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'): def __init__(self, config_file='config.ini'):
self.db_manager = DatabaseManager(config_file) self.db_manager = DatabaseManager(config_file)
self.db_manager.init_database() self.db_manager.init_database()
@@ -38,21 +46,10 @@ class OpenVPNAPI:
self.config.read(config_file) self.config.read(config_file)
# Paths # Paths
self.certificates_path = self.config.get('certificates', 'certificates_path', fallback='/etc/openvpn/certs') self.public_ip = self.get_config_value('openvpn_monitor', 'public_ip', fallback='')
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 = {}
# Security # Security
# Priority 1: Environment Variable self.secret_key = self.get_config_value('api', 'secret_key', fallback='ovpmon-secret-change-me')
# Priority 2: Config file
self.secret_key = os.getenv('OVPMON_SECRET_KEY') or self.config.get('api', 'secret_key', fallback='ovpmon-secret-change-me')
app.config['SECRET_KEY'] = self.secret_key app.config['SECRET_KEY'] = self.secret_key
# Ensure at least one user exists # Ensure at least one user exists
@@ -130,140 +127,7 @@ class OpenVPNAPI:
conn.close() 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): def get_current_stats(self):
@@ -1109,14 +973,7 @@ def get_client_stats(common_name):
logger.error(f"API Error: {e}") logger.error(f"API Error: {e}")
return jsonify({'success': False, 'error': str(e)}), 500 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']) @app.route('/api/v1/clients', methods=['GET'])
@token_required @token_required
@@ -1180,9 +1037,9 @@ def get_sessions():
if __name__ == "__main__": if __name__ == "__main__":
host = api.config.get('api', 'host', fallback='0.0.0.0') host = api.get_config_value('api', 'host', fallback='0.0.0.0')
port = 5001 # Используем 5001, чтобы не конфликтовать, если что-то уже есть на 5000 port = int(api.get_config_value('api', 'port', fallback=5001))
debug = api.config.getboolean('api', 'debug', fallback=False) debug = api.get_config_value('api', 'debug', fallback='false').lower() == 'true'
logger.info(f"Starting API on {host}:{port}") logger.info(f"Starting API on {host}:{port}")
app.run(host=host, port=port, debug=debug) app.run(host=host, port=port, debug=debug)

View File

@@ -142,14 +142,8 @@ class OpenVPNDataGatherer:
'agg_6h_retention_days': '180', # 6 месяцев 'agg_6h_retention_days': '180', # 6 месяцев
'agg_1d_retention_days': '365' # 12 месяцев 'agg_1d_retention_days': '365' # 12 месяцев
}, },
'visualization': { 'visualization': {},
'refresh_interval': '5', 'certificates': {}
'max_display_rows': '50'
},
'certificates': {
'certificates_path': '/opt/ovpn/pki/issued',
'certificate_extensions': 'crt'
}
} }
try: try:
@@ -214,6 +208,12 @@ class OpenVPNDataGatherer:
def get_config_value(self, section, key, default=None): def get_config_value(self, section, key, default=None):
try: 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) return self.config.get(section, key, fallback=default)
except: except:
return default 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. 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. | | `OVPMON_API_SECRET_KEY` | JWT Secret Key | `ovpmon-secret-change-me` |
| **Profiler** | `APP_PROFILER/` | FastAPI-based module for managing PKI, Certificates, and Server Configs. | | `OVPMON_API_PORT` | Monitoring API Port | `5001` |
| **User Interface** | `APP_UI/` | Vue 3 + Vite Single Page Application (SPA) serving as the unified dashboard. | | `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 ## 📚 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). - **[Deployment Guide](DOCS/General/Deployment.md)**: Manual setup for Linux.
- **[Service Management](DOCS/General/Service_Management.md)**: Configuring Systemd/OpenRC services. - **[Security Architecture](DOCS/General/Security_Architecture.md)**: 2FA, JWT, and Security.
- **[Security & Auth](DOCS/General/Security_Architecture.md)**: 2FA, JWT, and Security details. - **[API Reference](DOCS/Core_Monitoring/API_Reference.md)**: Core Monitoring endpoints.
### API References ## 🛠️ Development (Manual)
- **[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.
## 🚀 Quick Start (Dev Mode) If you wish to run services manually for development:
### 1. Core API (Flask) ### 1. Core API & Gatherer
```bash ```bash
cd APP_CORE cd APP_CORE
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt pip install -r requirements.txt
python3 openvpn_api_v3.py python3 openvpn_api_v3.py # APP_CORE API (:5001)
# Runs on :5001 (Monitoring) python3 openvpn_gatherer_v3.py # APP_CORE Gatherer
``` ```
### 2. Profiler API (FastAPI) ### 2. Profiler API
```bash ```bash
cd APP_PROFILER cd APP_PROFILER
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt pip install -r requirements.txt
python3 main.py python3 main.py # APP_PROFILER API (:8000)
# Runs on :8000 (Management)
``` ```
### 3. Frontend (Vue 3) ### 3. Frontend
```bash ```bash
cd APP_UI cd APP_UI
npm install npm install && npm run dev # UI (:5173)
npm run dev
# Runs on localhost:5173
``` ```
--- ---
## ⚠️ Important Notes ## ⚠️ Important Notes
1. **Environment**: Production deployment relies on Nginx to proxy requests to the backend services. See the [Deployment Guide](DOCS/General/Deployment.md). 1. **Permissions**: The Profiler container requires `NET_ADMIN` capabilities and access to `/dev/net/tun`.
2. **Permissions**: The backend requires `sudo` or root privileges to manage OpenVPN processes and write to `/etc/openvpn`. 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 container_name: ovp-gatherer
volumes: volumes:
- ovp_logs:/var/log/openvpn - ovp_logs:/var/log/openvpn
- db_data:/app/db # Assuming APP_CORE looks for DB in /app/db - db_data:/app/db
depends_on: depends_on:
- app-profiler - 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: networks:
- ovp-net - ovp-net
@@ -37,7 +41,12 @@ services:
networks: networks:
- ovp-net - ovp-net
environment: 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: app-profiler:
build: ./APP_PROFILER build: ./APP_PROFILER