Files
Inventarsystem/Legal/MULTITENANT_INTEGRATION.md
T
Aiirondev_dev 0c27d7ac86 feat: Add Excel, PDF export, and user generation modules
- Implemented `excel_export.py` for generating library item exports in Excel format.
- Created `pdf_export.py` for generating audit reports compliant with DIN 5008 standards, including detailed event tables and signature blocks.
- Developed `generate_user.py` for interactive user creation with validation for usernames and passwords.
- Introduced `module_registry.py` for managing module states and path matching.
- Added a basic `__init__.py` in the `terminplaner` module for initialization.
2026-05-20 15:30:23 +02:00

9.8 KiB
Raw Blame History

Multi-Tenant Integration in Flask App

Hier sind die konkreten Änderungen für Web/app.py, um Multi-Tenant Funktionalität zu aktivieren.

Änderung 1: Imports hinzufügen

VORHER (Zeile 1-60 in app.py):

from flask import Flask, render_template, request, ...
from werkzeug.utils import secure_filename
# ... weitere imports

NACHHER (Zusätzliche Imports):

# Multi-Tenant Imports
from tenant import get_tenant_context, require_tenant, get_tenant_db
from session_manager import create_redis_session_interface
from query_cache import get_cache_manager, cached_query, invalidate_cache

Änderung 2: Redis Session Backend konfigurieren

NACH app = Flask(...) (ca. Zeile 65):

app = Flask(__name__, static_folder='static')
app.logger.setLevel(logging.WARNING)
app.secret_key = cfg.SECRET_KEY

# ========== MULTI-TENANT KONFIGURATION ==========

# Aktiviere Redis Session Backend statt Filesystem
if os.getenv('INVENTAR_SESSION_BACKEND') == 'redis':
    try:
        app.session_interface = create_redis_session_interface(app)
        app.logger.info("Redis session backend enabled")
    except Exception as e:
        app.logger.warning(f"Redis session backend failed, using default: {e}")

# ================================================

Änderung 3: Health Check Endpoint hinzufügen

Neuer Route (nach allen anderen Routes, vor if __name__ == '__main__'):

@app.route('/health')
def health_check():
    """
    Health check endpoint für Nginx Load Balancer.
    Wird regelmäßig von Nginx aufgerufen (30s interval).
    
    Rückgabe: 200 OK wenn app bereit, sonst 503
    """
    try:
        # Check Database Connection
        from settings import MongoClient
        mongo = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
        mongo.admin.command('ping')
        
        # Check Redis Connection (falls Redis Session aktiv)
        if os.getenv('INVENTAR_SESSION_BACKEND') == 'redis':
            cache_mgr = get_cache_manager()
            if cache_mgr and cache_mgr.redis:
                cache_mgr.redis.ping()
        
        return {'status': 'healthy'}, 200
    
    except Exception as e:
        app.logger.error(f"Health check failed: {e}")
        return {'status': 'unhealthy', 'error': str(e)}, 503

Änderung 4: Tenant Context in bestehende Database-Calls

WICHTIG: Alle MongoClient Zugriffe müssen durch Tenant-Context gehen.

Beispiel 1: Bestehender Code (VORHER)

# VORHER: Direkter DB-Zugriff
@app.route('/items')
def get_items():
    client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
    db = client[cfg.MONGODB_DB]  # ← PROBLEM: Alle Tenants teilen sich die gleiche DB
    items = db['items'].find().to_list(100)
    return jsonify(items)

Beispiel 1: Mit Tenant-Routing (NACHHER)

# NACHHER: Tenant-Isolierte DB
@app.route('/items')
@require_tenant  # ← Decorator setzt Tenant Context
def get_items():
    client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
    ctx = get_tenant_context()
    
    # Richtige Datenbank für diesen Tenant
    db = client[ctx.db_name]  # z.B. "inventar_schule1"
    
    items = db['items'].find().to_list(100)
    return jsonify(items)

Oder kürzere Variante:

@app.route('/items')
@require_tenant
def get_items():
    db = get_tenant_db(MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT))
    items = db['items'].find().to_list(100)
    return jsonify(items)

Änderung 5: Query Caching für häufige Abfragen

VORHER:

def load_user_profile(user_id):
    db = client[cfg.MONGODB_DB]
    # Direkter DB-Zugriff bei jedem Request
    return db['users'].find_one({'_id': ObjectId(user_id)})

NACHHER:

@cached_query(category='user', ttl=7*24*3600)  # 7 Tage Cache
def load_user_profile(user_id):
    db = get_tenant_db(MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT))
    return db['users'].find_one({'_id': ObjectId(user_id)})

Nach Update muss Cache invalidiert werden:

def update_user_profile(user_id, updates):
    db = get_tenant_db(MongoClient(...))
    db['users'].update_one({'_id': ObjectId(user_id)}, {'$set': updates})
    
    # Cache invalidieren nach Write
    ctx = get_tenant_context()
    invalidate_cache(ctx.tenant_id, 'user')
    
    return True

Änderung 6: Debugging - Tenant-Info in Logs

Logging Helper (am besten in einem bestehenden Logging-Block):

def log_with_tenant(level, message):
    """Helper um Tenant-ID in Logs zu erfassen."""
    ctx = get_tenant_context()
    tenant_id = ctx.tenant_id if ctx else 'unknown'
    prefixed_msg = f"[{tenant_id}] {message}"
    
    if level == 'error':
        app.logger.error(prefixed_msg)
    elif level == 'warning':
        app.logger.warning(prefixed_msg)
    elif level == 'info':
        app.logger.info(prefixed_msg)
    else:
        app.logger.debug(prefixed_msg)

# Nutzung:
log_with_tenant('info', 'User login successful')
# Output: "[schule1] User login successful"

Änderung 7: Test-Routes (optional, für Debugging)

@app.route('/debug/tenant')
def debug_tenant_info():
    """Debug-Endpoint: Zeigt aktuellen Tenant-Context."""
    ctx = get_tenant_context()
    cache_mgr = get_cache_manager()
    
    return {
        'tenant_id': ctx.tenant_id if ctx else None,
        'db_name': ctx.db_name if ctx else None,
        'subdomain': ctx.subdomain if ctx else None,
        'cache_enabled': cache_mgr is not None,
        'cache_stats': cache_mgr.get_stats(ctx.tenant_id) if ctx and cache_mgr else None,
        'request_host': request.host,
        'request_headers': dict(request.headers)
    }

@app.route('/debug/cache/<action>', methods=['POST'])
def debug_cache_control(action):
    """Debug-Endpoint: Cache Kontrolle."""
    ctx = get_tenant_context()
    cache_mgr = get_cache_manager()
    
    if action == 'clear':
        if cache_mgr:
            cache_mgr.invalidate_tenant(ctx.tenant_id)
            return {'status': 'Cache cleared for tenant', 'tenant': ctx.tenant_id}
    
    return {'status': 'unknown action'}, 400

Änderung 8: Umgebungsvariablen (.env)

# Multi-Tenant Konfiguration
INVENTAR_MULTITENANT_ENABLED=true
INVENTAR_SESSION_BACKEND=redis
INVENTAR_REDIS_HOST=redis
INVENTAR_REDIS_PORT=6379
INVENTAR_REDIS_DB=0

# Query Caching
INVENTAR_QUERY_CACHE_ENABLED=true
INVENTAR_CACHE_DB=1

# Logging (auf WARNING reduzieren)
INVENTAR_LOG_LEVEL=WARNING

# Performance
INVENTAR_WORKERS=4
INVENTAR_WORKER_CLASS=gevent
INVENTAR_WORKER_CONNECTIONS=100

Migration-Strategie

Phase 1: Compatibility Mode (Keine Breaking Changes)

  • ✓ Beide Mode laufen gleichzeitig (Single + Multi)
  • ✓ Alte Routes funktionieren ohne Änderung
  • ✓ Neue Routes können @require_tenant nutzen
  • ✓ Session-Fallback wenn Redis nicht verfügbar

Phase 2: Graduelle Migration

  1. Starten mit SINGLE Instance + Single Database

    docker-compose -f docker-compose-multitenant.yml up -d --scale app=1
    
  2. Redis Session Backend aktivieren

    INVENTAR_SESSION_BACKEND=redis
    
  3. Einzelne Routes mit @require_tenant decorator markieren

  4. Query Caching für häufige Abfragen hinzufügen

  5. Testing mit Multi-Subdomain (test1.local, test2.local)

  6. Full Multi-Tenant in Production

    docker-compose -f docker-compose-multitenant.yml up -d --scale app=5
    

Performance-Vergleich

Single Instance (Vorher)

1 App Instance
- Memory: 200MB
- Startup: 8s
- Max Users: ~50 (gleichzeitig)
- DB Load: 100%
- Sessions: Filesystem I/O

Multi-Instance (Nachher)

3 App Instances
- Memory: 3 × 100MB = 300MB (gesamt)
- Startup: 3s pro Instance
- Max Users: ~150 (gleichzeitig, 50 pro instance)
- DB Load: 30% (durch Caching)
- Sessions: Redis (keine I/O)

Checkliste für Deployment

  • Docker-compose-multitenant.yml durchgelesen
  • Tenant-Module (tenant.py, session_manager.py, query_cache.py) im Web/ Ordner
  • app.py mit Multi-Tenant Imports aktualisiert
  • Redis Session Backend aktiviert (INVENTAR_SESSION_BACKEND=redis)
  • Health Check Endpoint implementiert
  • Nginx multitenant.conf konfiguriert
  • SSL Wildcard Zertifikat erstellt
  • DNS Wildcard Record konfiguriert (*.example.com)
  • First Tenant als test registriert
  • Health Checks funktionieren: curl https://test.example.com/health
  • Cache Stats verfügbar: curl https://test.example.com/debug/tenant
  • Load Test mit 2-3 Tenants durchgeführt
  • Monitoring Setup (Docker Stats, Nginx Logs)

Schul-Konfiguration pro Tenant

Die Datei config.json unterstützt jetzt einen tenants-Block. Damit kann jede Schule eigene Modul-Schalter bekommen, ohne dass das ganze System global umgestellt werden muss.

{
    "tenants": {
        "schule1": {
            "modules": {
                "library": { "enabled": true },
                "student_cards": { "enabled": false }
            }
        },
        "schule2": {
            "modules": {
                "library": { "enabled": false }
            }
        }
    }
}

Wenn ein Request über Subdomain oder X-Tenant-ID aufgelöst wird, liest die App diese Werte automatisch aus und blendet die Bibliothek bzw. andere Module nur für diesen Tenant ein oder aus.


Support & Debugging

Fragen?

  1. Logs prüfen: docker-compose -f docker-compose-multitenant.yml logs -f app
  2. Tenant-Info prüfen: curl https://your-tenant.com/debug/tenant
  3. Cache Stats: curl https://your-tenant.com/debug/cache-stats
  4. Redis Stats: docker exec inventarsystem-redis redis-cli info stats

Häufige Fehler:

  • X-Tenant-ID Header missing → Nginx nutzt alte Konfiguration
  • Redis connection refused → Redis Container nicht gestartet
  • Database not found → Tenant nicht registriert (auto-create bei erstem Request)
  • Out of memory → Memory Limit zu niedrig oder zu viele Instanzen