Add latest changes
Dieser Commit ist enthalten in:
Binäre Datei nicht angezeigt.
Binäre Datei nicht angezeigt.
@@ -3,6 +3,8 @@ import time
|
||||
import gzip
|
||||
import logging
|
||||
import subprocess
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
@@ -10,7 +12,7 @@ from cryptography.fernet import Fernet
|
||||
from db import get_db_connection, get_db_cursor
|
||||
from config import BACKUP_DIR, DATABASE_CONFIG, EMAIL_ENABLED, BACKUP_ENCRYPTION_KEY
|
||||
from utils.audit import log_audit
|
||||
from utils.github_backup import GitHubBackupManager, create_server_backup as create_server_backup_impl
|
||||
from utils.github_backup import GitHubBackupManager, create_server_backup_impl
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -125,6 +127,10 @@ def create_backup(backup_type="manual", created_by=None):
|
||||
send_backup_notification(True, filename, filesize, duration)
|
||||
|
||||
logger.info(f"Backup successfully created: {filename}")
|
||||
|
||||
# Apply retention policy - keep only last 5 local backups
|
||||
cleanup_old_backups("database", 5)
|
||||
|
||||
return True, filename
|
||||
|
||||
except Exception as e:
|
||||
@@ -224,6 +230,69 @@ def send_backup_notification(success, filename, filesize=None, duration=None, er
|
||||
logger.info(f"Email notification prepared: Backup {'successful' if success else 'failed'}")
|
||||
|
||||
|
||||
def cleanup_old_backups(backup_type="database", keep_count=5):
|
||||
"""Clean up old local backups, keeping only the most recent ones"""
|
||||
try:
|
||||
# Get list of local backups from database
|
||||
with get_db_connection() as conn:
|
||||
with get_db_cursor(conn) as cur:
|
||||
cur.execute("""
|
||||
SELECT id, filename, filepath
|
||||
FROM backup_history
|
||||
WHERE backup_type = %s
|
||||
AND status = 'success'
|
||||
AND local_deleted = FALSE
|
||||
AND filepath IS NOT NULL
|
||||
ORDER BY created_at DESC
|
||||
""", (backup_type,))
|
||||
backups = cur.fetchall()
|
||||
|
||||
if len(backups) <= keep_count:
|
||||
logger.info(f"No cleanup needed. Found {len(backups)} {backup_type} backups, keeping {keep_count}")
|
||||
return
|
||||
|
||||
# Delete old backups
|
||||
backups_to_delete = backups[keep_count:]
|
||||
deleted_count = 0
|
||||
|
||||
for backup_id, filename, filepath in backups_to_delete:
|
||||
try:
|
||||
# Check if file exists
|
||||
if filepath and os.path.exists(filepath):
|
||||
os.unlink(filepath)
|
||||
logger.info(f"Deleted old backup: {filename}")
|
||||
|
||||
# Update database
|
||||
with get_db_connection() as conn:
|
||||
with get_db_cursor(conn) as cur:
|
||||
cur.execute("""
|
||||
UPDATE backup_history
|
||||
SET local_deleted = TRUE
|
||||
WHERE id = %s
|
||||
""", (backup_id,))
|
||||
conn.commit()
|
||||
|
||||
deleted_count += 1
|
||||
else:
|
||||
# File doesn't exist, just update database
|
||||
with get_db_connection() as conn:
|
||||
with get_db_cursor(conn) as cur:
|
||||
cur.execute("""
|
||||
UPDATE backup_history
|
||||
SET local_deleted = TRUE
|
||||
WHERE id = %s
|
||||
""", (backup_id,))
|
||||
conn.commit()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete backup {filename}: {e}")
|
||||
|
||||
logger.info(f"Backup cleanup completed. Deleted {deleted_count} old {backup_type} backups")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Backup cleanup failed: {e}")
|
||||
|
||||
|
||||
def create_backup_with_github(backup_type="manual", created_by=None, push_to_github=True, delete_local=True):
|
||||
"""Create backup and optionally push to GitHub"""
|
||||
# Create the backup
|
||||
@@ -242,7 +311,8 @@ def create_backup_with_github(backup_type="manual", created_by=None, push_to_git
|
||||
db_backup_dir.mkdir(exist_ok=True)
|
||||
|
||||
target_path = db_backup_dir / filename
|
||||
filepath.rename(target_path)
|
||||
# Use shutil.move instead of rename to handle cross-device links
|
||||
shutil.move(str(filepath), str(target_path))
|
||||
|
||||
# Push to GitHub
|
||||
github = GitHubBackupManager()
|
||||
@@ -269,8 +339,8 @@ def create_backup_with_github(backup_type="manual", created_by=None, push_to_git
|
||||
conn.commit()
|
||||
else:
|
||||
logger.error(f"Failed to push to GitHub: {git_result}")
|
||||
# Move file back
|
||||
target_path.rename(filepath)
|
||||
# Move file back using shutil
|
||||
shutil.move(str(target_path), str(filepath))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"GitHub upload error: {str(e)}")
|
||||
@@ -279,6 +349,63 @@ def create_backup_with_github(backup_type="manual", created_by=None, push_to_git
|
||||
return True, filename
|
||||
|
||||
|
||||
def create_container_server_backup_info(created_by="system"):
|
||||
"""Create a server info backup in container environment"""
|
||||
try:
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"server_backup_info_{timestamp}.json"
|
||||
filepath = Path("/app/backups") / filename
|
||||
|
||||
# Collect server info available in container
|
||||
server_info = {
|
||||
"backup_type": "server_info",
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"created_by": created_by,
|
||||
"container_environment": True,
|
||||
"message": "Full server backups nur über Host-System möglich. Dies ist eine Info-Datei.",
|
||||
"docker_compose": None,
|
||||
"env_vars": {},
|
||||
"existing_backups": []
|
||||
}
|
||||
|
||||
# Try to read docker-compose if mounted
|
||||
if os.path.exists("/app/docker-compose.yaml"):
|
||||
try:
|
||||
with open("/app/docker-compose.yaml", 'r') as f:
|
||||
server_info["docker_compose"] = f.read()
|
||||
except:
|
||||
pass
|
||||
|
||||
# Try to read env vars (without secrets)
|
||||
if os.path.exists("/app/.env"):
|
||||
try:
|
||||
with open("/app/.env", 'r') as f:
|
||||
for line in f:
|
||||
if '=' in line and not any(secret in line.upper() for secret in ['PASSWORD', 'SECRET', 'KEY']):
|
||||
key, value = line.strip().split('=', 1)
|
||||
server_info["env_vars"][key] = "***" if len(value) > 20 else value
|
||||
except:
|
||||
pass
|
||||
|
||||
# List existing server backups
|
||||
if os.path.exists("/app/server-backups"):
|
||||
try:
|
||||
server_info["existing_backups"] = sorted(os.listdir("/app/server-backups"))[-10:]
|
||||
except:
|
||||
pass
|
||||
|
||||
# Write info file
|
||||
with open(filepath, 'w') as f:
|
||||
json.dump(server_info, f, indent=2)
|
||||
|
||||
logger.info(f"Container server backup info created: {filename}")
|
||||
return True, str(filepath)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Container server backup info failed: {e}")
|
||||
return False, str(e)
|
||||
|
||||
|
||||
def create_server_backup(created_by=None, push_to_github=True, delete_local=True):
|
||||
"""Create full server backup"""
|
||||
start_time = time.time()
|
||||
@@ -296,7 +423,7 @@ def create_server_backup(created_by=None, push_to_github=True, delete_local=True
|
||||
conn.commit()
|
||||
|
||||
try:
|
||||
# Create server backup
|
||||
# Create server backup - always use full backup now
|
||||
success, result = create_server_backup_impl(created_by)
|
||||
|
||||
if not success:
|
||||
@@ -353,6 +480,9 @@ def create_server_backup(created_by=None, push_to_github=True, delete_local=True
|
||||
log_audit('BACKUP', 'server', backup_id,
|
||||
additional_info=f"Server backup created: {filename} ({filesize} bytes)")
|
||||
|
||||
# Apply retention policy - keep only last 5 local server backups
|
||||
cleanup_old_backups("server", 5)
|
||||
|
||||
return True, filename
|
||||
|
||||
except Exception as e:
|
||||
|
||||
97
v2_adminpanel/utils/device_monitoring.py
Normale Datei
97
v2_adminpanel/utils/device_monitoring.py
Normale Datei
@@ -0,0 +1,97 @@
|
||||
"""Device limit monitoring utilities"""
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from db import get_connection
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def check_device_limits():
|
||||
"""Check all licenses for device limit violations"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Find licenses approaching or exceeding device limits
|
||||
cur.execute("""
|
||||
SELECT
|
||||
l.id,
|
||||
l.license_key,
|
||||
l.device_limit,
|
||||
COUNT(DISTINCT dr.id) as device_count,
|
||||
c.name as customer_name,
|
||||
c.email as customer_email
|
||||
FROM licenses l
|
||||
LEFT JOIN device_registrations dr ON dr.license_id = l.id AND dr.is_active = true
|
||||
LEFT JOIN customers c ON c.id = l.customer_id
|
||||
WHERE l.is_active = true
|
||||
GROUP BY l.id, l.license_key, l.device_limit, c.name, c.email
|
||||
HAVING COUNT(DISTINCT dr.id) >= l.device_limit * 0.8 -- 80% threshold
|
||||
ORDER BY (COUNT(DISTINCT dr.id)::float / l.device_limit) DESC
|
||||
""")
|
||||
|
||||
warnings = []
|
||||
for row in cur.fetchall():
|
||||
license_id, license_key, device_limit, device_count, customer_name, customer_email = row
|
||||
usage_percent = (device_count / device_limit) * 100 if device_limit > 0 else 0
|
||||
|
||||
warning = {
|
||||
'license_id': license_id,
|
||||
'license_key': license_key,
|
||||
'customer_name': customer_name or 'Unknown',
|
||||
'customer_email': customer_email or 'No email',
|
||||
'device_limit': device_limit,
|
||||
'device_count': device_count,
|
||||
'usage_percent': round(usage_percent, 1),
|
||||
'status': 'exceeded' if device_count > device_limit else 'warning'
|
||||
}
|
||||
warnings.append(warning)
|
||||
|
||||
if device_count > device_limit:
|
||||
logger.warning(f"License {license_key} exceeded device limit: {device_count}/{device_limit}")
|
||||
|
||||
return warnings
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking device limits: {e}")
|
||||
return []
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
def get_device_usage_stats():
|
||||
"""Get overall device usage statistics"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Get overall stats
|
||||
cur.execute("""
|
||||
SELECT
|
||||
COUNT(DISTINCT l.id) as total_licenses,
|
||||
COUNT(DISTINCT dr.id) as total_devices,
|
||||
SUM(l.device_limit) as total_device_limit,
|
||||
COUNT(DISTINCT CASE WHEN dr.last_seen_at > NOW() - INTERVAL '24 hours' THEN dr.id END) as active_24h,
|
||||
COUNT(DISTINCT CASE WHEN dr.last_seen_at > NOW() - INTERVAL '7 days' THEN dr.id END) as active_7d
|
||||
FROM licenses l
|
||||
LEFT JOIN device_registrations dr ON dr.license_id = l.id AND dr.is_active = true
|
||||
WHERE l.is_active = true
|
||||
""")
|
||||
|
||||
stats = cur.fetchone()
|
||||
|
||||
return {
|
||||
'total_licenses': stats[0] or 0,
|
||||
'total_devices': stats[1] or 0,
|
||||
'total_device_limit': stats[2] or 0,
|
||||
'usage_percent': round((stats[1] / stats[2] * 100) if stats[2] > 0 else 0, 1),
|
||||
'active_24h': stats[3] or 0,
|
||||
'active_7d': stats[4] or 0,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting device usage stats: {e}")
|
||||
return {}
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
@@ -170,6 +170,7 @@ def create_batch_export(licenses):
|
||||
'Email': license.get('customer_email', ''),
|
||||
'Lizenztyp': license.get('license_type', 'full').upper(),
|
||||
'Geräte-Limit': license.get('device_limit', 3),
|
||||
'Max. Sessions': license.get('max_concurrent_sessions', 1),
|
||||
'Gültig von': format_datetime_for_export(license.get('valid_from')),
|
||||
'Gültig bis': format_datetime_for_export(license.get('valid_until')),
|
||||
'Status': 'Aktiv' if license.get('is_active', True) else 'Inaktiv',
|
||||
|
||||
@@ -9,6 +9,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class GitHubBackupManager:
|
||||
def __init__(self):
|
||||
# Always use full path now that container has access
|
||||
self.repo_path = Path("/opt/v2-Docker")
|
||||
self.backup_remote = "backup"
|
||||
self.git_lfs_path = "/home/root/.local/bin"
|
||||
@@ -141,8 +142,8 @@ class GitHubBackupManager:
|
||||
logger.error(f"Download from GitHub error: {str(e)}")
|
||||
return False, str(e)
|
||||
|
||||
def create_server_backup(created_by="system"):
|
||||
"""Create a full server backup"""
|
||||
def create_server_backup_impl(created_by="system"):
|
||||
"""Create a full server backup - implementation"""
|
||||
try:
|
||||
# Run the backup script
|
||||
backup_script = Path("/opt/v2-Docker/create_full_backup.sh")
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren