0c27d7ac86
- 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.
367 lines
9.8 KiB
Markdown
367 lines
9.8 KiB
Markdown
# 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):
|
||
```python
|
||
from flask import Flask, render_template, request, ...
|
||
from werkzeug.utils import secure_filename
|
||
# ... weitere imports
|
||
```
|
||
|
||
**NACHHER** (Zusätzliche Imports):
|
||
```python
|
||
# 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):
|
||
|
||
```python
|
||
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__'`):
|
||
|
||
```python
|
||
@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)
|
||
|
||
```python
|
||
# 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)
|
||
|
||
```python
|
||
# 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:
|
||
|
||
```python
|
||
@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**:
|
||
```python
|
||
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**:
|
||
```python
|
||
@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:
|
||
|
||
```python
|
||
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):
|
||
|
||
```python
|
||
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)
|
||
|
||
```python
|
||
@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)
|
||
|
||
```bash
|
||
# 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
|
||
```bash
|
||
docker-compose -f docker-compose-multitenant.yml up -d --scale app=1
|
||
```
|
||
|
||
2. Redis Session Backend aktivieren
|
||
```bash
|
||
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
|
||
```bash
|
||
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.
|
||
|
||
```json
|
||
{
|
||
"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
|
||
|