commit f00c1f48e921ffd7c62d91cdc70d32d3544e8970 Author: Samuel Müller Date: Wed Mar 26 08:40:32 2025 +0100 Erstelle die Grundstruktur für die Ceph Max Storage Rechner-Anwendung mit Flask und HTMX. Füge Konfigurationsdateien, Routen, Modelle, Templates und statische Dateien hinzu. Implementiere grundlegende Funktionen zur Berechnung der Speichernutzung und zur PDF-Generierung. Integriere CSRF-Schutz und Logging. Stelle sicher, dass die Anwendung modular und wartbar ist. diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..1c83f19 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,39 @@ +// HTMX with Flask .cursorrules + +// HTMX and Flask best practices + +const htmxFlaskBestPractices = [ + "Use Flask's render_template for server-side rendering", + "Implement Flask-WTF for form handling", + "Utilize Flask's url_for for generating URLs", + "Use Flask's jsonify for JSON responses", + "Implement Flask-SQLAlchemy for database operations", + "Utilize Flask's Blueprint for modular applications", +]; + +// Folder structure + +const folderStructure = ` +app/ + templates/ + static/ + css/ + js/ + models/ + routes/ + __init__.py +config.py +run.py +`; + +// Additional instructions + +const additionalInstructions = ` +1. Use Jinja2 templating with HTMX attributes +2. Implement proper CSRF protection with Flask-WTF +3. Utilize Flask's request object for handling HTMX requests +4. Use Flask-Migrate for database migrations +5. Implement proper error handling and logging +6. Follow Flask's application factory pattern +7. Use environment variables for configuration +`; \ No newline at end of file diff --git a/.env b/.env new file mode 100644 index 0000000..7e867c5 --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +SECRET_KEY=your-secure-secret-key-here +DATABASE_URL=sqlite:///app.db +FLASK_ENV=development +FLASK_DEBUG=1 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2c5405b --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# Ceph Max Storage Rechner + +Eine moderne Web-Anwendung zur Berechnung der maximal zulässigen Speichernutzung in einem Ceph-Cluster. + +## Funktionen + +- Berechnung der maximalen Speichernutzung basierend auf Ceph-Dokumentation +- Unterstützung für Replikation und Erasure Coding +- Dynamisches Hinzufügen und Entfernen von Nodes und OSDs +- Benutzerfreundliche Oberfläche mit modernem Design + +## Installation + +1. Repository klonen: + ``` + git clone + cd ceph-calculator + ``` + +2. Python-Umgebung einrichten: + ``` + python -m venv venv + venv\Scripts\activate # Windows + source venv/bin/activate # Linux/Mac + ``` + +3. Abhängigkeiten installieren: + ``` + pip install -r requirements.txt + ``` + +4. Anwendung starten: + ``` + python run.py + ``` + +5. Öffnen Sie in einem Browser: `http://localhost:5000` + +## Verwendung + +1. Wählen Sie den Replikationstyp: Replikation oder Erasure Coding +2. Geben Sie die entsprechenden Parameter ein (Anzahl der Replikate oder k/m-Werte für EC) +3. Fügen Sie Nodes hinzu und konfigurieren Sie deren OSDs mit entsprechenden Speichergrößen +4. Klicken Sie auf "Kapazität berechnen", um das Ergebnis zu sehen + +## Technologie + +- Backend: Flask +- Frontend: HTMX, Alpine.js, Tailwind CSS +- Responsive Design für alle Geräte + +## Hinweis + +Die Berechnungen basieren auf den allgemeinen Empfehlungen aus der Ceph-Dokumentation und +dienen als Richtwert. Für genaue Kapazitätsplanung sollten Sie die Ceph-Dokumentation konsultieren +und Ihre spezifischen Clusteranforderungen berücksichtigen. \ No newline at end of file diff --git a/__pycache__/config.cpython-313.pyc b/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000..b5d9845 Binary files /dev/null and b/__pycache__/config.cpython-313.pyc differ diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..aab4056 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,54 @@ +import logging +from logging.handlers import RotatingFileHandler +import os +from flask import Flask, render_template +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate +from flask_wtf.csrf import CSRFProtect +from config import Config + +db = SQLAlchemy() +migrate = Migrate() +csrf = CSRFProtect() + +def create_app(config_class=Config): + app = Flask(__name__) + app.config.from_object(config_class) + + # Initialisierung der Erweiterungen + db.init_app(app) + migrate.init_app(app, db) + csrf.init_app(app) + + # Logging-Konfiguration + if not app.debug and not app.testing: + if not os.path.exists('logs'): + os.mkdir('logs') + file_handler = RotatingFileHandler('logs/app.log', maxBytes=10240, backupCount=10) + file_handler.setFormatter(logging.Formatter( + '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]' + )) + file_handler.setLevel(logging.INFO) + app.logger.addHandler(file_handler) + app.logger.setLevel(logging.INFO) + app.logger.info('Anwendung startet') + + # Blueprints registrieren + from app.routes.main import bp as main_bp + app.register_blueprint(main_bp) + + # Fehlerbehandlung + @app.errorhandler(404) + def not_found_error(error): + return render_template('errors/404.html'), 404 + + @app.errorhandler(500) + def internal_error(error): + db.session.rollback() + return render_template('errors/500.html'), 500 + + @app.context_processor + def utility_processor(): + return dict(enumerate=enumerate) + + return app \ No newline at end of file diff --git a/app/__pycache__/__init__.cpython-313.pyc b/app/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..80e5e51 Binary files /dev/null and b/app/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/routes/__init__.py b/app/routes/__init__.py new file mode 100644 index 0000000..04a34a8 --- /dev/null +++ b/app/routes/__init__.py @@ -0,0 +1 @@ +# Diese Datei macht das Verzeichnis zu einem Python-Paket \ No newline at end of file diff --git a/app/routes/__pycache__/__init__.cpython-313.pyc b/app/routes/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..70048d1 Binary files /dev/null and b/app/routes/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/routes/__pycache__/main.cpython-313.pyc b/app/routes/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000..4e959fa Binary files /dev/null and b/app/routes/__pycache__/main.cpython-313.pyc differ diff --git a/app/routes/main.py b/app/routes/main.py new file mode 100644 index 0000000..df465b8 --- /dev/null +++ b/app/routes/main.py @@ -0,0 +1,99 @@ +from flask import Blueprint, render_template, request, jsonify, send_file, current_app +from app.utils.ceph_calculator import calculate_ceph_capacity +from app.utils.pdf_generator import generate_pdf_report +from io import BytesIO +import logging +import traceback + +logger = logging.getLogger(__name__) +bp = Blueprint('main', __name__) + +@bp.route('/', methods=['GET']) +def index(): + return render_template('index.html') + +@bp.route('/calculate', methods=['POST']) +def calculate(): + try: + data = request.json + if not data: + return jsonify({"error": "Keine Daten empfangen"}), 400 + + replication_type = data.get('replication_type') # 'replication' oder 'erasure_coding' + if not replication_type: + return jsonify({"error": "Replikationstyp fehlt"}), 400 + + replicas = int(data.get('replicas', 3)) # Standardwert: 3 Replikate + + # Für Erasure Coding + k = int(data.get('k', 0)) # Datenchunks + m = int(data.get('m', 0)) # Codierungschunks + + # Minimale Replikate für I/O-Operationen + min_size = int(data.get('min_size', 2)) # Standardwert: 2 + + # Nodes und OSDs + nodes = data.get('nodes', []) + if not nodes: + return jsonify({"error": "Keine Nodes definiert"}), 400 + + # Speichereinheit + storage_unit = data.get('storage_unit', 'GB') + + # Berechnung durchführen + result = calculate_ceph_capacity( + replication_type=replication_type, + replicas=replicas, + k=k, + m=m, + nodes=nodes, + min_size=min_size, + storage_unit=storage_unit + ) + + return jsonify(result) + except ValueError as e: + logger.error(f"Validierungsfehler bei der Berechnung: {str(e)}") + return jsonify({"error": str(e)}), 400 + except Exception as e: + logger.error(f"Fehler bei der Berechnung: {str(e)}\n{traceback.format_exc()}") + return jsonify({"error": "Ein unerwarteter Fehler ist aufgetreten"}), 500 + +@bp.route('/generate-pdf', methods=['POST']) +def generate_pdf(): + try: + data = request.get_json() + if not data: + return jsonify({"error": "Keine Daten empfangen"}), 400 + + # Validiere die Daten + if not data.get('replication_type'): + return jsonify({"error": "Replikationstyp fehlt"}), 400 + + if not data.get('nodes') or not isinstance(data['nodes'], list): + return jsonify({"error": "Ungültige Node-Daten"}), 400 + + if not data.get('result') or not isinstance(data['result'], dict): + return jsonify({"error": "Ungültige Ergebnisdaten"}), 400 + + if not data['result'].get('raw_total'): + return jsonify({"error": "Keine Berechnungsergebnisse vorhanden"}), 400 + + # Generiere PDF + pdf_bytes = generate_pdf_report(data) + + if not pdf_bytes: + return jsonify({"error": "PDF-Generierung fehlgeschlagen"}), 500 + + return send_file( + BytesIO(pdf_bytes), + mimetype='application/pdf', + as_attachment=True, + download_name='ceph-report.pdf' + ) + except ValueError as e: + logger.error(f"Validierungsfehler bei der PDF-Generierung: {str(e)}") + return jsonify({"error": str(e)}), 400 + except Exception as e: + logger.error(f"Fehler bei der PDF-Generierung: {str(e)}\n{traceback.format_exc()}") + return jsonify({"error": "Ein unerwarteter Fehler ist aufgetreten"}), 500 \ No newline at end of file diff --git a/app/static/css/styles.css b/app/static/css/styles.css new file mode 100644 index 0000000..e3f1b0e --- /dev/null +++ b/app/static/css/styles.css @@ -0,0 +1,104 @@ +/* Zusätzliche Stile für den Ceph-Rechner */ + +/* Sanfter Farbübergang im Hintergrund */ +body { + background: linear-gradient(135deg, #f0f4f8 0%, #d9e2ec 100%); +} + +/* Grundlegende Formular-Styles */ +.form-control, input, select, textarea { + background-color: #ffffff !important; + border: 1px solid #e2e8f0 !important; + color: #2d3748 !important; +} + +.form-control:focus, input:focus, select:focus, textarea:focus { + background-color: #ffffff !important; + border-color: #4299e1 !important; + box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.25) !important; +} + +.form-control::placeholder, input::placeholder, select::placeholder, textarea::placeholder { + color: #a0aec0 !important; +} + +select option { + background-color: #ffffff !important; + color: #2d3748 !important; +} + +/* Bessere Lesbarkeit für Ergebnisse */ +.result-value { + font-variant-numeric: tabular-nums; +} + +/* Hover-Effekte für Karten */ +.card-hover { + transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out; +} + +.card-hover:hover { + transform: translateY(-2px); + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); +} + +/* Light Mode Anpassungen */ +@media (prefers-color-scheme: light) { + .form-control, input, select, textarea { + background-color: #ffffff !important; + border: 1px solid #e2e8f0 !important; + color: #2d3748 !important; + } + + .form-control:focus, input:focus, select:focus, textarea:focus { + background-color: #ffffff !important; + border-color: #4299e1 !important; + box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.25) !important; + } + + .form-control::placeholder, input::placeholder, select::placeholder, textarea::placeholder { + color: #a0aec0 !important; + } + + select option { + background-color: #ffffff !important; + color: #2d3748 !important; + } +} + +/* Darkmode Anpassungen */ +@media (prefers-color-scheme: dark) { + body { + background: linear-gradient(135deg, #1a1a1a 0%, #2d3748 100%); + } + + /* Eingabefelder im Darkmode */ + .form-control, input, select, textarea { + background-color: #2d3748 !important; + border: 1px solid #4a5568 !important; + color: #e2e8f0 !important; + } + + .form-control:focus, input:focus, select:focus, textarea:focus { + background-color: #2d3748 !important; + border-color: #63b3ed !important; + box-shadow: 0 0 0 3px rgba(99, 179, 237, 0.25) !important; + } + + .form-control::placeholder, input::placeholder, select::placeholder, textarea::placeholder { + color: #718096 !important; + } + + select option { + background-color: #2d3748 !important; + color: #e2e8f0 !important; + } +} + +/* Responsive Anpassungen */ +@media (max-width: 640px) { + .container { + padding-left: 1rem; + padding-right: 1rem; + } +} \ No newline at end of file diff --git a/app/static/js/script.js b/app/static/js/script.js new file mode 100644 index 0000000..7c5d817 --- /dev/null +++ b/app/static/js/script.js @@ -0,0 +1,230 @@ +function cephCalculator() { + return { + replicationType: 'replication', + replicas: 3, + minSize: 2, + k: 4, + m: 2, + storageUnit: 'TB', + nodes: [ + { + osd_count: 4, + osd_size_gb: 1000 + } + ], + result: { + max_usage_percent: 0, + max_usage_gb: 0, + max_usage_tb: 0, + raw_total: 0 + }, + isCalculating: false, + + init() { + const savedConfig = localStorage.getItem('lastConfig'); + if (savedConfig) { + const config = JSON.parse(savedConfig); + Object.assign(this, config); + } + }, + + addNode() { + this.nodes.push({ + osd_count: 4, + osd_size_gb: this.storageUnit === 'TB' ? 1000 : 1 + }); + }, + + removeNode(nodeIndex) { + this.nodes.splice(nodeIndex, 1); + if (this.nodes.length === 0) { + this.addNode(); + } + }, + + getOsdSizeInGB(size) { + return this.storageUnit === 'TB' ? size * 1024 : size; + }, + + calculateCapacity() { + const data = { + replication_type: this.replicationType, + replicas: parseInt(this.replicas), + k: parseInt(this.k), + m: parseInt(this.m), + nodes: this.nodes.map(node => ({ + osd_count: parseInt(node.osd_count), + osd_size_gb: this.getOsdSizeInGB(parseFloat(node.osd_size_gb)) + })), + min_size: parseInt(this.minSize), + storage_unit: this.storageUnit + }; + + this.isCalculating = true; + + fetch('/calculate', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') + }, + body: JSON.stringify(data) + }) + .then(response => response.json()) + .then(data => { + this.result = data; + }) + .catch(error => { + console.error('Fehler bei der Berechnung:', error); + alert('Es ist ein Fehler bei der Berechnung aufgetreten. Bitte versuchen Sie es erneut.'); + }) + .finally(() => { + this.isCalculating = false; + }); + }, + + exportConfig() { + const config = { + replicationType: this.replicationType, + replicas: this.replicas, + minSize: this.minSize, + k: this.k, + m: this.m, + storageUnit: this.storageUnit, + nodes: this.nodes, + result: this.result + }; + + const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'ceph-config.json'; + a.click(); + URL.revokeObjectURL(url); + }, + + importConfig(event) { + const file = event.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + try { + const config = JSON.parse(e.target.result); + Object.assign(this, config); + // Berechne automatisch nach dem Import + this.calculateCapacity(); + } catch (error) { + alert('Fehler beim Importieren der Konfiguration. Bitte überprüfen Sie das Dateiformat.'); + } + }; + reader.readAsText(file); + }, + + generatePDF() { + if (!this.result.raw_total) { + alert('Bitte führen Sie zuerst eine Berechnung durch.'); + return; + } + + // Validiere die Daten vor dem Senden + if (!this.nodes || this.nodes.length === 0) { + alert('Bitte fügen Sie mindestens einen Node hinzu.'); + return; + } + + // Validiere die Node-Daten + for (let i = 0; i < this.nodes.length; i++) { + const node = this.nodes[i]; + if (!node.osd_count || node.osd_count < 1) { + alert(`Bitte geben Sie eine gültige Anzahl von OSDs für Node ${i + 1} ein.`); + return; + } + if (!node.osd_size_gb || node.osd_size_gb <= 0) { + alert(`Bitte geben Sie eine gültige OSD-Größe für Node ${i + 1} ein.`); + return; + } + } + + const data = { + replication_type: this.replicationType, + replicas: parseInt(this.replicas), + min_size: parseInt(this.minSize), + k: parseInt(this.k), + m: parseInt(this.m), + storage_unit: this.storageUnit, + nodes: this.nodes.map(node => ({ + osd_count: parseInt(node.osd_count), + osd_size_gb: parseFloat(this.getOsdSizeInGB(parseFloat(node.osd_size_gb))) + })), + result: { + raw_total: parseFloat(this.result.raw_total), + max_usage_percent: parseFloat(this.result.max_usage_percent), + max_usage_gb: parseFloat(this.result.max_usage_gb), + max_usage_tb: parseFloat(this.result.max_usage_tb), + node_failure_tolerance: Boolean(this.result.node_failure_tolerance), + node_failure_info: String(this.result.node_failure_info || ''), + multi_failure_tolerance: Boolean(this.result.multi_failure_tolerance), + max_failure_nodes: parseInt(this.result.max_failure_nodes || 0), + osd_failure_tolerance: Boolean(this.result.osd_failure_tolerance), + osd_failure_info: String(this.result.osd_failure_info || '') + } + }; + + // Zeige Ladeindikator + const button = document.querySelector('button[onclick="generatePDF()"]'); + const originalText = button.innerHTML; + button.innerHTML = 'Generiere PDF...'; + button.disabled = true; + + // Hole CSRF-Token + const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); + if (!csrfToken) { + alert('CSRF-Token nicht gefunden. Bitte laden Sie die Seite neu.'); + button.innerHTML = originalText; + button.disabled = false; + return; + } + + fetch('/generate-pdf', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken + }, + body: JSON.stringify(data) + }) + .then(response => { + if (!response.ok) { + return response.json().then(err => { + throw new Error(err.error || `HTTP error! status: ${response.status}`); + }); + } + return response.blob(); + }) + .then(blob => { + if (!blob || blob.size === 0) { + throw new Error('Die generierte PDF-Datei ist leer'); + } + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'ceph-report.pdf'; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + }) + .catch(error => { + console.error('Fehler bei der PDF-Generierung:', error); + alert(`Fehler bei der PDF-Generierung: ${error.message}`); + }) + .finally(() => { + // Stelle den Button wieder her + button.innerHTML = originalText; + button.disabled = false; + }); + } + }; +} \ No newline at end of file diff --git a/app/static/js/script.js.bak b/app/static/js/script.js.bak new file mode 100644 index 0000000..aca56c4 --- /dev/null +++ b/app/static/js/script.js.bak @@ -0,0 +1,167 @@ +function cephCalculator() { + return { + replicationType: 'replication', + replicas: 3, + minSize: 2, + k: 4, + m: 2, + storageUnit: 'TB', + nodes: [ + { + osd_count: 4, + osd_size_gb: 1000 + } + ], + result: { + max_usage_percent: 0, + max_usage_gb: 0, + max_usage_tb: 0, + raw_total: 0 + }, + isCalculating: false, + + init() { + const savedConfig = localStorage.getItem('lastConfig'); + if (savedConfig) { + const config = JSON.parse(savedConfig); + Object.assign(this, config); + } + }, + + addNode() { + this.nodes.push({ + osd_count: 4, + osd_size_gb: this.storageUnit === 'TB' ? 1000 : 1 + }); + }, + + removeNode(nodeIndex) { + this.nodes.splice(nodeIndex, 1); + if (this.nodes.length === 0) { + this.addNode(); + } + }, + + getOsdSizeInGB(size) { + return this.storageUnit === 'TB' ? size * 1024 : size; + }, + + calculateCapacity() { + const data = { + replication_type: this.replicationType, + replicas: parseInt(this.replicas), + k: parseInt(this.k), + m: parseInt(this.m), + nodes: this.nodes.map(node => ({ + osd_count: parseInt(node.osd_count), + osd_size_gb: this.getOsdSizeInGB(parseFloat(node.osd_size_gb)) + })), + min_size: parseInt(this.minSize), + storage_unit: this.storageUnit + }; + + this.isCalculating = true; + + fetch('/calculate', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') + }, + body: JSON.stringify(data) + }) + .then(response => response.json()) + .then(data => { + this.result = data; + }) + .catch(error => { + console.error('Fehler bei der Berechnung:', error); + alert('Es ist ein Fehler bei der Berechnung aufgetreten. Bitte versuchen Sie es erneut.'); + }) + .finally(() => { + this.isCalculating = false; + }); + }, + + exportConfig() { + const config = { + replicationType: this.replicationType, + replicas: this.replicas, + minSize: this.minSize, + k: this.k, + m: this.m, + storageUnit: this.storageUnit, + nodes: this.nodes, + result: this.result + }; + + const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'ceph-config.json'; + a.click(); + URL.revokeObjectURL(url); + }, + + importConfig(event) { + const file = event.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + try { + const config = JSON.parse(e.target.result); + Object.assign(this, config); + // Berechne automatisch nach dem Import + this.calculateCapacity(); + } catch (error) { + alert('Fehler beim Importieren der Konfiguration. Bitte überprüfen Sie das Dateiformat.'); + } + }; + reader.readAsText(file); + }, + + generatePDF() { + if (!this.result.raw_total) { + alert('Bitte führen Sie zuerst eine Berechnung durch.'); + return; + } + + const data = { + replication_type: this.replicationType, + replicas: this.replicas, + min_size: this.minSize, + k: this.k, + m: this.m, + storage_unit: this.storageUnit, + nodes: this.nodes, + result: this.result + }; + + fetch('/generate-pdf', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') + }, + body: JSON.stringify(data) + }) + .then(response => response.blob()) + .then(blob => { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'ceph-report.pdf'; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + }) + .catch(error => { + console.error('Fehler bei der PDF-Generierung:', error); + alert('Es ist ein Fehler bei der PDF-Generierung aufgetreten. Bitte versuchen Sie es erneut.'); + }); + } + }; +} \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..02c9f37 --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,14 @@ + + + + + + Ceph Storage Calculator + + + + + + // ... existing code ... + + \ No newline at end of file diff --git a/app/templates/errors/404.html b/app/templates/errors/404.html new file mode 100644 index 0000000..165dca2 --- /dev/null +++ b/app/templates/errors/404.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+

404

+

Seite nicht gefunden

+

Die angeforderte Seite konnte leider nicht gefunden werden.

+ Zurück zur Startseite +
+
+
+{% endblock %} \ No newline at end of file diff --git a/app/templates/errors/500.html b/app/templates/errors/500.html new file mode 100644 index 0000000..53f4938 --- /dev/null +++ b/app/templates/errors/500.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+

500

+

Interner Serverfehler

+

Es ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.

+ Zurück zur Startseite +
+
+
+{% endblock %} \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..46225d9 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,488 @@ + + + + + + + Ceph Max Storage Rechner + + + + + + + + + + +
+
+ +
+ +
+ +
+

Ceph Max Storage Kapazitätsrechner

+

Berechnen Sie die maximale Speichernutzung Ihres Ceph-Clusters

+
+ +
+ +
+

Replikationstyp

+
+ + +
+
+ + +
+

Replikationskonfiguration

+
+ +
+ + +
+
+
+ + + Minimale Anzahl von Kopien für I/O-Operationen +
+
+ + +
+
+ + +
+

Erasure Coding Konfiguration

+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+

Nodes und OSDs

+ + + + +
+ + +
+ + + + +
+ + +
+

Ergebnis

+
+
+
+

Gesamt-Rohspeicher:

+

+ +

+
+
+

Maximale empfohlene Speichernutzung:

+

+

+ +

+
+
+ + +
+

Ausfalltoleranz-Analyse

+ + +
+
+ Node-Ausfalltoleranz: + + +
+

+
+
+ Größter Node: + +
+
+ Rohspeicher nach Nodeausfall: + +
+
+ Nutzbarer Speicher nach Ausfall: + +
+
+ Nutzbarer Speicher nach max. Ausfällen: + +
+
+ + +
+

+ Min-Size: + - + Diese Einstellung bestimmt die minimale Anzahl von Replikaten für I/O-Operationen. + Der Cluster kann bis zu Nodes verlieren, + bevor die Daten unzugänglich werden. +

+
+
+ + +
+
+ OSD-Ausfalltoleranz: + +
+

+
+ + +
+

Empfehlungen gemäß Ceph-Dokumentation:

+
    +
  • Stelle sicher, dass nach Nodeausfällen genügend Kapazität vorhanden ist, um die 'full ratio' nicht zu erreichen.
  • +
  • Bei Replikation sollte die Anzahl der Nodes größer sein als der Replikationsfaktor.
  • +
  • Bei Erasure Coding sollte die Anzahl der Nodes größer sein als k+m (Datenchunks + Codierungschunks).
  • +
  • Die Performance bei der Wiederherstellung hängt von der Netzwerk- und Speichergeschwindigkeit ab.
  • +
  • Beachte, dass nach einem Nodeausfall das Wiederausbalancieren des Clusters länger dauert, je größer der ausgefallene Node ist.
  • +
+
+
+
+
+
+ +
+

Basierend auf Ceph-Dokumentation | Ergebnisse dienen nur als Richtwert

+
+
+
+ + + + \ No newline at end of file diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 0000000..04a34a8 --- /dev/null +++ b/app/utils/__init__.py @@ -0,0 +1 @@ +# Diese Datei macht das Verzeichnis zu einem Python-Paket \ No newline at end of file diff --git a/app/utils/__pycache__/__init__.cpython-313.pyc b/app/utils/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..e823302 Binary files /dev/null and b/app/utils/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/utils/__pycache__/ceph_calculator.cpython-313.pyc b/app/utils/__pycache__/ceph_calculator.cpython-313.pyc new file mode 100644 index 0000000..eb68309 Binary files /dev/null and b/app/utils/__pycache__/ceph_calculator.cpython-313.pyc differ diff --git a/app/utils/__pycache__/pdf_generator.cpython-313.pyc b/app/utils/__pycache__/pdf_generator.cpython-313.pyc new file mode 100644 index 0000000..f82d199 Binary files /dev/null and b/app/utils/__pycache__/pdf_generator.cpython-313.pyc differ diff --git a/app/utils/ceph_calculator.py b/app/utils/ceph_calculator.py new file mode 100644 index 0000000..07c53c6 --- /dev/null +++ b/app/utils/ceph_calculator.py @@ -0,0 +1,204 @@ +def calculate_ceph_capacity(replication_type, replicas=3, k=0, m=0, nodes=None, min_size=2, storage_unit='GB'): + """ + Berechnet die maximal zulässige Speichernutzung für ein Ceph-Cluster unter Berücksichtigung von Nodeausfällen. + + Parameter: + - replication_type: 'replication' oder 'erasure_coding' + - replicas: Anzahl der Replikate (Standard: 3) + - k: Anzahl der Datenchunks für EC + - m: Anzahl der Codierungschunks für EC + - nodes: Liste der Knoten mit Anzahl der OSDs und deren Größe + [{'osd_count': 4, 'osd_size_gb': 1000}, ...] + - min_size: Minimale Anzahl von Replikaten für I/O-Operationen (Standard: 2) + - storage_unit: 'GB' oder 'TB' (Standard: 'GB') + + Returns: + - Dictionary mit max_usage_percent, max_usage_gb, max_usage_tb, raw_total und zusätzlichen Informationen zur Ausfallsicherheit + """ + if nodes is None or len(nodes) == 0: + return { + 'max_usage_percent': 0, + 'max_usage_gb': 0, + 'max_usage_tb': 0, + 'raw_total': 0, + 'node_failure_tolerance': False, + 'node_failure_info': 'Keine Nodes im Cluster', + 'storage_unit': storage_unit + } + + # Die Umrechnung von TB zu GB ist bereits im JavaScript-Code erfolgt + # Hier werden die Werte direkt verwendet + + # Berechne den gesamten Rohspeicher und Informationen zu jedem Node + raw_total_gb = 0 + node_capacities = [] + + for node_config in nodes: + osd_count = int(node_config.get('osd_count', 0)) + osd_size_gb = float(node_config.get('osd_size_gb', 0)) + node_capacity = osd_count * osd_size_gb + node_capacities.append(node_capacity) + raw_total_gb += node_capacity + + # Größter Node (worst-case Szenario bei Ausfall) + largest_node_capacity = max(node_capacities) if node_capacities else 0 + + # Berechne die nutzbare Kapazität ohne Ausfall + if replication_type == 'replication': + # Bei Replikation ist der nutzbare Speicher = Rohspeicher / Anzahl der Replikate + usable_capacity_gb = raw_total_gb / replicas + else: # Erasure Coding + # Bei EC ist der nutzbare Speicher = Rohspeicher * (k / (k + m)) + usable_capacity_gb = raw_total_gb * (k / (k + m)) + + # Die empfohlene maximale Auslastung für den Normalfall (ohne Ausfall) + max_recommended_usage_percent = 80 + + # Berechne die OSD-Auslastung nach der Formel x = (s × p) / (s + 1) + # wobei s = Anzahl der OSDs pro Server und p = Prozentsatz der Gesamtauslastung + osds_per_server = nodes[0].get('osd_count', 0) if nodes else 0 + osd_usage_percent = (osds_per_server * max_recommended_usage_percent) / (osds_per_server + 1) + + # Finde die größte OSD-Größe für die Berechnung der Kapazität nach OSD-Ausfall + largest_osd_size = max((node.get('osd_size_gb', 0) for node in nodes), default=0) + + # Berechne die nutzbare Kapazität nach OSD-Ausfall + raw_after_osd_failure = raw_total_gb - largest_osd_size + if replication_type == 'replication': + usable_after_osd_failure = raw_after_osd_failure / replicas + else: + usable_after_osd_failure = raw_after_osd_failure * (k / (k + m)) + + # Berechne die maximale sichere Nutzung unter Berücksichtigung von OSD-Ausfall + max_usage_gb = min( + usable_capacity_gb * (osd_usage_percent / 100), # OSD-Auslastung basierend auf der Formel + usable_after_osd_failure * 0.8 # 80% der Kapazität nach OSD-Ausfall + ) + max_usage_tb = max_usage_gb / 1024 + + # Berechnung der Ausfalltoleranz unter Berücksichtigung von min_size + if replication_type == 'replication': + max_failure_nodes = min( + len(nodes) - min_size, # Maximale Ausfälle basierend auf min_size + replicas - min_size # Maximale Ausfälle basierend auf Replikationsfaktor + ) + else: # Erasure Coding + max_failure_nodes = min( + len(nodes) - (k + 1), # Mindestens k+1 Nodes müssen verfügbar bleiben + m # Maximale Anzahl von Codierungschunks die ausfallen können + ) + + # Sortiere die Nodes nach Größe absteigend für Worst-Case-Analyse + node_capacities_sorted = sorted(node_capacities, reverse=True) + + # Kapazität nach Ausfall der größten N Nodes + raw_after_max_failures_gb = raw_total_gb + for i in range(min(max_failure_nodes, len(node_capacities_sorted))): + raw_after_max_failures_gb -= node_capacities_sorted[i] + + # Nutzbare Kapazität nach maximalen tolerierbaren Ausfällen + if replication_type == 'replication': + usable_after_max_failures_gb = raw_after_max_failures_gb / min_size + else: # Erasure Coding + remaining_m = m - max_failure_nodes + usable_after_max_failures_gb = raw_after_max_failures_gb * (k / (k + remaining_m)) + + # Berechne die nutzbare Kapazität nach Ausfall des größten Nodes + raw_after_failure_gb = raw_total_gb - largest_node_capacity + if replication_type == 'replication': + usable_after_failure_gb = raw_after_failure_gb / min_size + else: # Erasure Coding + usable_after_failure_gb = raw_after_failure_gb * (k / (k + m)) + + # Prüfen, ob genügend Speicherplatz nach einem Nodeausfall vorhanden ist + node_failure_tolerance = True + + # Prüfe die Mindestanforderungen für Nodes + if replication_type == 'replication': + if len(nodes) < min_size: + node_failure_tolerance = False + elif usable_after_failure_gb < max_usage_gb: + node_failure_tolerance = False + else: # Erasure Coding + if len(nodes) <= k: + node_failure_tolerance = False + elif usable_after_failure_gb < max_usage_gb: + node_failure_tolerance = False + + # Für multiple Ausfälle prüfen + multi_failure_tolerance = False + if max_failure_nodes > 0: + multi_failure_tolerance = ( + usable_after_max_failures_gb >= max_usage_gb and + len(nodes) > max_failure_nodes + ) + + # Maximale sichere Nutzung unter Berücksichtigung eines möglichen Nodeausfalls + safe_usage_percent = 0 + safe_usage_gb = 0 + safe_usage_tb = 0 + node_failure_info = "" + + if node_failure_tolerance: + safe_usage_percent = max_recommended_usage_percent + safe_usage_gb = max_usage_gb + safe_usage_tb = max_usage_tb + + if multi_failure_tolerance and max_failure_nodes > 1: + node_failure_info = f"Der Cluster kann den Ausfall von bis zu {max_failure_nodes} Nodes tolerieren (min_size={min_size})." + else: + node_failure_info = f"Der Cluster kann einen Ausfall des größten Nodes tolerieren (min_size={min_size})." + else: + safe_usage_percent = round((usable_after_failure_gb / usable_capacity_gb) * 100 * 0.8) + safe_usage_gb = usable_after_failure_gb * 0.8 + safe_usage_tb = safe_usage_gb / 1024 + + if len(nodes) <= (min_size if replication_type == 'replication' else k + m - min(m, 1)): + node_failure_info = f"KRITISCH: Zu wenige Nodes ({len(nodes)}) für die konfigurierte min_size={min_size}. " + node_failure_info += f"Mindestens {min_size + 1 if replication_type == 'replication' else k + m + 1 - min(m, 1)} Nodes benötigt." + else: + # Unit für die Anzeige + unit_display = "TB" if storage_unit == "TB" else "GB" + node_size_display = round(largest_node_capacity / 1024, 2) if storage_unit == "TB" else round(largest_node_capacity, 2) + + node_failure_info = (f"WARNUNG: Der Cluster hat nicht genügend freie Kapazität, um einen Ausfall des größten Nodes " + f"({node_size_display} {unit_display}) zu tolerieren. " + f"Maximale sichere Nutzung: {safe_usage_percent}%") + + # Berechnung für den Ausfall eines einzelnen OSD + osd_failure_tolerance = False + osd_failure_info = "Keine OSDs im Cluster" + + if nodes and any(node.get('osd_count', 0) > 0 for node in nodes): + osd_failure_tolerance = usable_after_osd_failure >= max_usage_gb + + # Unit für die Anzeige + unit_display = "TB" if storage_unit == "TB" else "GB" + osd_size_display = round(largest_osd_size / 1024, 2) if storage_unit == "TB" else round(largest_osd_size, 2) + + osd_failure_info = f"Der Cluster kann den Ausfall des größten OSD tolerieren (min_size={min_size})." if osd_failure_tolerance else \ + f"WARNUNG: Der Cluster hat nicht genügend freie Kapazität, um den Ausfall des größten OSD ({osd_size_display} {unit_display}) zu tolerieren." + + # Rückgabewerte + result = { + 'max_usage_percent': round(osd_usage_percent, 2), + 'max_usage_gb': round(max_usage_gb if node_failure_tolerance else safe_usage_gb, 2), + 'max_usage_tb': round(max_usage_tb if node_failure_tolerance else safe_usage_tb, 2), + 'raw_total': round(raw_total_gb, 2), + 'node_failure_tolerance': node_failure_tolerance, + 'node_failure_info': node_failure_info, + 'multi_failure_tolerance': multi_failure_tolerance, + 'max_failure_nodes': max_failure_nodes, + 'osd_failure_tolerance': osd_failure_tolerance, + 'osd_failure_info': osd_failure_info, + 'largest_node_gb': round(largest_node_capacity, 2), + 'raw_after_failure_gb': round(raw_after_failure_gb, 2), + 'usable_after_failure_gb': round(usable_after_failure_gb, 2), + 'raw_after_max_failures_gb': round(raw_after_max_failures_gb, 2), + 'usable_after_max_failures_gb': round(usable_after_max_failures_gb, 2), + 'min_size': min_size, + 'osds_per_server': osds_per_server, + 'storage_unit': storage_unit + } + + return result \ No newline at end of file diff --git a/app/utils/pdf_generator.py b/app/utils/pdf_generator.py new file mode 100644 index 0000000..0bdcc8e --- /dev/null +++ b/app/utils/pdf_generator.py @@ -0,0 +1,217 @@ +from reportlab.lib import colors +from reportlab.lib.pagesizes import A4 +from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.lib.units import inch +from datetime import datetime +import logging + +logger = logging.getLogger(__name__) + +def validate_data(data): + """Überprüft, ob alle erforderlichen Daten vorhanden sind.""" + required_fields = ['replication_type', 'nodes', 'result'] + for field in required_fields: + if field not in data: + raise ValueError(f"Fehlendes Feld: {field}") + + if not data['nodes']: + raise ValueError("Keine Nodes definiert") + + if not data['result'].get('raw_total'): + raise ValueError("Keine Berechnungsergebnisse vorhanden") + +def safe_int(value, default=0): + """Konvertiert einen Wert sicher in einen Integer.""" + if value is None: + return default + try: + return int(str(value).strip()) + except (ValueError, TypeError): + logger.warning(f"Konnte Wert '{value}' nicht in Integer konvertieren. Verwende Standardwert {default}") + return default + +def safe_float(value, default=0.0): + """Konvertiert einen Wert sicher in einen Float.""" + if value is None: + return default + try: + return float(str(value).strip()) + except (ValueError, TypeError): + logger.warning(f"Konnte Wert '{value}' nicht in Float konvertieren. Verwende Standardwert {default}") + return default + +def generate_pdf_report(data): + """ + Generiert einen PDF-Report mit den Ceph-Konfigurationsdaten und Ergebnissen. + + Args: + data (dict): Dictionary mit Konfigurations- und Ergebnisdaten + + Returns: + bytes: PDF-Datei als Bytes + + Raises: + ValueError: Wenn erforderliche Daten fehlen + """ + try: + # Validiere Eingabedaten + validate_data(data) + + # Erstelle einen temporären Buffer für die PDF + from io import BytesIO + buffer = BytesIO() + + # Erstelle das PDF-Dokument + doc = SimpleDocTemplate( + buffer, + pagesize=A4, + rightMargin=72, + leftMargin=72, + topMargin=72, + bottomMargin=72, + title="Ceph Storage Kapazitätsreport" + ) + + # Styles definieren + styles = getSampleStyleSheet() + title_style = styles['Heading1'] + heading_style = styles['Heading2'] + normal_style = styles['Normal'] + + # Custom Style für Warnungen + warning_style = ParagraphStyle( + 'Warning', + parent=styles['Normal'], + textColor=colors.red, + fontSize=10 + ) + + # Story (Inhalt) erstellen + story = [] + + # Titel + story.append(Paragraph("Ceph Storage Kapazitätsreport", title_style)) + story.append(Spacer(1, 12)) + story.append(Paragraph(f"Generiert am: {datetime.now().strftime('%d.%m.%Y %H:%M')}", normal_style)) + story.append(Spacer(1, 20)) + + # Konfigurationsübersicht + story.append(Paragraph("Konfigurationsübersicht", heading_style)) + story.append(Spacer(1, 12)) + + # Replikationstyp + config_data = [ + ["Replikationstyp", data['replication_type']], + ["Speichereinheit", data.get('storage_unit', 'GB')] + ] + + if data['replication_type'] == 'replication': + config_data.extend([ + ["Anzahl Replikate", str(safe_int(data.get('replicas', 3)))], + ["Min. Replikate (min_size)", str(safe_int(data.get('min_size', 2)))] + ]) + else: + config_data.extend([ + ["Datenchunks (k)", str(safe_int(data.get('k', 4)))], + ["Codierungschunks (m)", str(safe_int(data.get('m', 2)))] + ]) + + # Nodes und OSDs + story.append(Paragraph("Nodes und OSDs", heading_style)) + story.append(Spacer(1, 12)) + + for i, node in enumerate(data['nodes'], 1): + story.append(Paragraph(f"Node {i}", heading_style)) + story.append(Spacer(1, 6)) + + osd_count = safe_int(node.get('osd_count', 1)) + osd_size = safe_float(node.get('osd_size_gb', 0)) + + if osd_count <= 0: + logger.warning(f"Ungültige OSD-Anzahl für Node {i}: {osd_count}. Verwende Standardwert 1.") + osd_count = 1 + + osd_data = [] + for j in range(osd_count): + osd_data.append([f"OSD {j+1}", f"{osd_size} {data.get('storage_unit', 'GB')}"]) + + t = Table(osd_data, colWidths=[2*inch, 2*inch]) + t.setStyle(TableStyle([ + ('BACKGROUND', (0, 0), (-1, -1), colors.white), + ('TEXTCOLOR', (0, 0), (-1, -1), colors.black), + ('ALIGN', (0, 0), (-1, -1), 'LEFT'), + ('FONTNAME', (0, 0), (-1, -1), 'Helvetica'), + ('FONTSIZE', (0, 0), (-1, -1), 10), + ('BOTTOMPADDING', (0, 0), (-1, -1), 12), + ('GRID', (0, 0), (-1, -1), 1, colors.black) + ])) + story.append(t) + story.append(Spacer(1, 12)) + + # Ergebnisse + story.append(Paragraph("Ergebnisse", heading_style)) + story.append(Spacer(1, 12)) + + result_data = [ + ["Gesamt-Rohspeicher", f"{safe_float(data['result']['raw_total'])} GB ({(safe_float(data['result']['raw_total']) / 1024):.2f} TB)"], + ["Max. empfohlene Nutzung", f"{safe_float(data['result'].get('max_usage_percent', 0))}%"], + ["Max. nutzbarer Speicher", f"{safe_float(data['result'].get('max_usage_gb', 0))} GB ({safe_float(data['result'].get('max_usage_tb', 0))} TB)"] + ] + + t = Table(result_data, colWidths=[3*inch, 3*inch]) + t.setStyle(TableStyle([ + ('BACKGROUND', (0, 0), (-1, -1), colors.white), + ('TEXTCOLOR', (0, 0), (-1, -1), colors.black), + ('ALIGN', (0, 0), (-1, -1), 'LEFT'), + ('FONTNAME', (0, 0), (-1, -1), 'Helvetica'), + ('FONTSIZE', (0, 0), (-1, -1), 10), + ('BOTTOMPADDING', (0, 0), (-1, -1), 12), + ('GRID', (0, 0), (-1, -1), 1, colors.black) + ])) + story.append(t) + + # Ausfalltoleranz + story.append(Spacer(1, 20)) + story.append(Paragraph("Ausfalltoleranz-Analyse", heading_style)) + story.append(Spacer(1, 12)) + + # Node-Ausfalltoleranz + node_tolerance = "Vorhanden" if data['result'].get('node_failure_tolerance', False) else "Nicht vorhanden" + story.append(Paragraph(f"Node-Ausfalltoleranz: {node_tolerance}", normal_style)) + story.append(Paragraph(data['result'].get('node_failure_info', 'Keine Informationen verfügbar'), normal_style)) + + if data['result'].get('multi_failure_tolerance', False): + story.append(Paragraph(f"Multi-Node Ausfalltoleranz: {safe_int(data['result'].get('max_failure_nodes', 0))} Nodes", normal_style)) + + # OSD-Ausfalltoleranz + story.append(Spacer(1, 12)) + osd_tolerance = "Vorhanden" if data['result'].get('osd_failure_tolerance', False) else "Eingeschränkt" + story.append(Paragraph(f"OSD-Ausfalltoleranz: {osd_tolerance}", normal_style)) + story.append(Paragraph(data['result'].get('osd_failure_info', 'Keine Informationen verfügbar'), normal_style)) + + # Empfehlungen + story.append(Spacer(1, 20)) + story.append(Paragraph("Empfehlungen", heading_style)) + story.append(Spacer(1, 12)) + + recommendations = [ + "Stelle sicher, dass nach Nodeausfällen genügend Kapazität vorhanden ist, um die 'full ratio' nicht zu erreichen.", + "Bei Replikation sollte die Anzahl der Nodes größer sein als der Replikationsfaktor.", + "Bei Erasure Coding sollte die Anzahl der Nodes größer sein als k+m (Datenchunks + Codierungschunks).", + "Die Performance bei der Wiederherstellung hängt von der Netzwerk- und Speichergeschwindigkeit ab.", + "Beachte, dass nach einem Nodeausfall das Wiederausbalancieren des Clusters länger dauert, je größer der ausgefallene Node ist." + ] + + for rec in recommendations: + story.append(Paragraph(f"• {rec}", normal_style)) + + # PDF generieren + doc.build(story) + + # PDF-Bytes zurückgeben + return buffer.getvalue() + + except Exception as e: + logger.error(f"Fehler bei der PDF-Generierung: {str(e)}") + raise \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..145ed55 --- /dev/null +++ b/config.py @@ -0,0 +1,26 @@ +import os +from dotenv import load_dotenv + +basedir = os.path.abspath(os.path.dirname(__file__)) +load_dotenv(os.path.join(basedir, '.env')) + +class Config: + # Grundlegende Konfiguration + SECRET_KEY = os.environ.get('SECRET_KEY') + if not SECRET_KEY: + raise ValueError("Kein SECRET_KEY in der Umgebung gefunden!") + + # Datenbank-Konfiguration + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ + 'sqlite:///' + os.path.join(basedir, 'app.db') + SQLALCHEMY_TRACK_MODIFICATIONS = False + + # Logging-Konfiguration + LOG_TO_STDOUT = os.environ.get('LOG_TO_STDOUT') + LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO') + + # Sicherheitseinstellungen + SESSION_COOKIE_SECURE = True + REMEMBER_COOKIE_SECURE = True + SESSION_COOKIE_HTTPONLY = True + REMEMBER_COOKIE_HTTPONLY = True \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b9b2b66 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +Flask>=2.2.5 +Flask-WTF>=1.1.1 +python-dotenv==1.0.0 +flask-sqlalchemy==3.0.5 +flask-migrate==4.0.4 +reportlab==4.0.4 \ No newline at end of file diff --git a/run.py b/run.py new file mode 100644 index 0000000..d1a7bec --- /dev/null +++ b/run.py @@ -0,0 +1,6 @@ +from app import create_app, db + +app = create_app() + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file