new calculation approach with unique sessions, new API endpoint to get list of active sessions, fix for UNDEF user, UI and Back to support certificate management still under development

This commit is contained in:
Антон
2026-01-12 11:44:50 +03:00
parent 839dd4994f
commit 6df0f5e180
10 changed files with 1175 additions and 59 deletions

View File

@@ -109,6 +109,10 @@ class OpenVPNDataGatherer:
# Инициализация модуля агрегации
# Передаем ссылку на метод подключения к БД
self.ts_aggregator = TimeSeriesAggregator(self.db_manager.get_connection)
# In-Memory Cache для отслеживания сессий (CN, RealAddress) -> {last_bytes...}
# Используется для корректного расчета инкрементов при множественных сессиях одного CN
self.session_cache = {}
def load_config(self, config_file):
"""Загрузка конфигурации или создание дефолтной со сложной структурой"""
@@ -284,6 +288,10 @@ class OpenVPNDataGatherer:
# 6: Bytes Sent
if len(parts) >= 8 and parts[1] != 'Common Name':
# SKIPPING 'UNDEF' CLIENTS
if parts[1].strip() == 'UNDEF':
continue
try:
client = {
'common_name': parts[1].strip(),
@@ -304,51 +312,130 @@ class OpenVPNDataGatherer:
return clients
def update_client_status_and_bytes(self, active_clients):
"""Обновление статусов и расчет инкрементов трафика"""
"""
Обновление статусов и расчет инкрементов трафика.
Использует In-Memory Cache (self.session_cache) для корректной обработки
множественных сессий одного пользователя (Ping-Pong effect fix).
"""
conn = self.db_manager.get_connection()
cursor = conn.cursor()
try:
# Загружаем текущее состояние всех клиентов
cursor.execute('SELECT id, common_name, status, last_bytes_received, last_bytes_sent FROM clients')
db_clients = {}
for row in cursor.fetchall():
db_clients[row[1]] = {
'id': row[0],
'status': row[2],
'last_bytes_received': row[3],
'last_bytes_sent': row[4]
}
# 1. Загружаем текущее состояние CNs из БД для обновления статусов
cursor.execute('SELECT id, common_name, status, total_bytes_received, total_bytes_sent FROM clients')
db_clients = {row[1]: {'id': row[0], 'status': row[2]} for row in cursor.fetchall()}
active_names = set()
# Структура для агрегации инкрементов по Common Name перед записью в БД
# cn -> { 'inc_rx': 0, 'inc_tx': 0, 'curr_rx': 0, 'curr_tx': 0, 'real_address': '...'}
cn_updates = {}
# Множество активных ключей сессий (CN, RealAddr) для очистки кэша
active_session_keys = set()
active_cns = set()
# 2. Обрабатываем каждую активную сессию
for client in active_clients:
name = client['common_name']
active_names.add(name)
real_addr = client['real_address']
curr_recv = client['bytes_received']
curr_sent = client['bytes_sent']
if name in db_clients:
# Клиент существует в базе
db_client = db_clients[name]
client['db_id'] = db_client['id'] # ID для агрегатора и истории
# Уникальный ключ сессии
session_key = (name, real_addr)
active_session_keys.add(session_key)
active_cns.add(name)
# --- ЛОГИКА РАСЧЕТА ДЕЛЬТЫ (In-Memory) ---
if session_key in self.session_cache:
prev_state = self.session_cache[session_key]
prev_recv = prev_state['bytes_received']
prev_sent = prev_state['bytes_sent']
# Проверка на рестарт сервера/сессии (сброс счетчиков)
# Если текущее значение меньше сохраненного, значит был сброс -> берем всё текущее значение как дельту
if curr_recv < db_client['last_bytes_received']:
# Расчет RX
if curr_recv < prev_recv:
# Рестарт сессии (счетчик сбросился)
inc_recv = curr_recv
self.logger.info(f"Counter reset detected for {name} (Recv)")
self.logger.info(f"Session reset detected for {session_key} (Recv)")
else:
inc_recv = curr_recv - db_client['last_bytes_received']
if curr_sent < db_client['last_bytes_sent']:
inc_recv = curr_recv - prev_recv
# Расчет TX
if curr_sent < prev_sent:
inc_sent = curr_sent
self.logger.info(f"Counter reset detected for {name} (Sent)")
self.logger.info(f"Session reset detected for {session_key} (Sent)")
else:
inc_sent = curr_sent - db_client['last_bytes_sent']
inc_sent = curr_sent - prev_sent
else:
# Новая сессия (или после рестарта сервиса)
# Если сервиса только запустился, мы не знаем предыдущего состояния.
# Чтобы избежать спайков, считаем инкремент = 0 для первого появления,
# если это похоже на продолжающуюся сессию (большие числа).
# Если числа маленькие (<10MB), считаем как новую.
# Обновляем клиента
# 10 MB threshold
threshold = 10 * 1024 * 1024
if curr_recv < threshold and curr_sent < threshold:
inc_recv = curr_recv
inc_sent = curr_sent
else:
# Скорее всего рестарт сервиса, пропускаем первый тик
inc_recv = 0
inc_sent = 0
self.logger.debug(f"New session tracking started for {session_key}. Initializing baseline.")
# Обновляем кэш
if session_key not in self.session_cache:
# New session
self.session_cache[session_key] = {
'bytes_received': curr_recv,
'bytes_sent': curr_sent,
'last_seen': datetime.now(),
'connected_since': datetime.now() # Track start time
}
else:
# Update existing
self.session_cache[session_key]['bytes_received'] = curr_recv
self.session_cache[session_key]['bytes_sent'] = curr_sent
self.session_cache[session_key]['last_seen'] = datetime.now()
# Добавляем в клиентский объект (для истории/графиков)
# Важно: это инкремент конкретной сессии
client['bytes_received_inc'] = inc_recv
client['bytes_sent_inc'] = inc_sent
# Ensure db_id is available for active_sessions later (populated in step 4 or from cache)
# We defer writing to active_sessions until we have DB IDs
client['session_key'] = session_key
# --- АГРЕГАЦИЯ ДЛЯ БД (по Common Name) ---
if name not in cn_updates:
cn_updates[name] = {
'inc_recv': 0, 'inc_tx': 0,
'max_rx': 0, 'max_tx': 0, # Для last_bytes в БД сохраним текущие счетчики самой большой сессии (примерно)
'real_address': real_addr # Берем последний адрес
}
cn_updates[name]['inc_recv'] += inc_recv
cn_updates[name]['inc_tx'] += inc_sent
# Сохраняем "текущее" значение как максимальное из сессий, чтобы в БД last_bytes было хоть что-то осмысленное
# (хотя при in-memory подходе поле last_bytes в БД теряет смысл для логики, но нужно для UI)
cn_updates[name]['max_rx'] = max(cn_updates[name]['max_rx'], curr_recv)
cn_updates[name]['max_tx'] = max(cn_updates[name]['max_tx'], curr_sent)
cn_updates[name]['real_address'] = real_addr
# 3. Очистка кэша от мертвых сессий
# Создаем список ключей для удаления, чтобы не менять словарь во время итерации
dead_sessions = [k for k in self.session_cache if k not in active_session_keys]
for k in dead_sessions:
del self.session_cache[k]
self.logger.debug(f"Removed inactive session from cache: {k}")
# 4. Обновление БД (Upsert Clients)
for name, data in cn_updates.items():
if name in db_clients:
# UPDATE
db_id = db_clients[name]['id']
cursor.execute('''
UPDATE clients
SET status = 'Active',
@@ -361,47 +448,72 @@ class OpenVPNDataGatherer:
last_activity = CURRENT_TIMESTAMP
WHERE id = ?
''', (
client['real_address'],
inc_recv,
inc_sent,
curr_recv,
curr_sent,
db_client['id']
data['real_address'],
data['inc_recv'],
data['inc_tx'],
data['max_rx'],
data['max_tx'],
db_id
))
client['bytes_received_inc'] = inc_recv
client['bytes_sent_inc'] = inc_sent
# Прокидываем DB ID обратно в объекты клиентов (для TSDB)
# Так как active_clients - это список сессий, ищем все сессии этого юзера
for client in active_clients:
if client['common_name'] == name:
client['db_id'] = db_id
else:
# Новый клиент
# INSERT (New Client)
cursor.execute('''
INSERT INTO clients
(common_name, real_address, status, total_bytes_received, total_bytes_sent, last_bytes_received, last_bytes_sent)
VALUES (?, ?, 'Active', 0, 0, ?, ?)
''', (
name,
client['real_address'],
curr_recv,
curr_sent
data['real_address'],
data['max_rx'],
data['max_tx']
))
new_id = cursor.lastrowid
client['db_id'] = new_id
# Для первой записи считаем инкремент 0 (или можно считать весь трафик)
client['bytes_received_inc'] = 0
client['bytes_sent_inc'] = 0
self.logger.info(f"New client added: {name}")
# Прокидываем ID
for client in active_clients:
if client['common_name'] == name:
client['db_id'] = new_id
# Помечаем отключенных
for name, db_client in db_clients.items():
if name not in active_names and db_client['status'] == 'Active':
# 5. Помечаем отключенных
for name, db_info in db_clients.items():
if name not in active_cns and db_info['status'] == 'Active':
cursor.execute('''
UPDATE clients
SET status = 'Disconnected', updated_at = CURRENT_TIMESTAMP
WHERE id = ?
''', (db_client['id'],))
''', (db_info['id'],))
self.logger.info(f"Client disconnected: {name}")
# 6. SYNC ACTIVE SESSIONS TO DB (Snapshot)
# Clear old state
cursor.execute('DELETE FROM active_sessions')
# Insert current state
for client in active_clients:
# client['db_id'] should be populated by now (from step 4)
if 'db_id' in client and 'session_key' in client:
sess_data = self.session_cache.get(client['session_key'])
if sess_data:
cursor.execute('''
INSERT INTO active_sessions
(client_id, common_name, real_address, bytes_received, bytes_sent, connected_since, last_seen)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (
client['db_id'],
client['common_name'],
client['real_address'],
client['bytes_received'],
client['bytes_sent'],
sess_data.get('connected_since', datetime.now()),
sess_data.get('last_seen', datetime.now())
))
conn.commit()
except Exception as e: