Add latest changes

Dieser Commit ist enthalten in:
2025-07-03 20:38:33 +00:00
Ursprung 63f3d92724
Commit 6f6cde65db
129 geänderte Dateien mit 3998 neuen und 1199 gelöschten Zeilen

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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