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.
This commit is contained in:
39
.cursorrules
Normal file
39
.cursorrules
Normal file
@ -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
|
||||
`;
|
||||
4
.env
Normal file
4
.env
Normal file
@ -0,0 +1,4 @@
|
||||
SECRET_KEY=your-secure-secret-key-here
|
||||
DATABASE_URL=sqlite:///app.db
|
||||
FLASK_ENV=development
|
||||
FLASK_DEBUG=1
|
||||
56
README.md
Normal file
56
README.md
Normal file
@ -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 <repository-url>
|
||||
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.
|
||||
BIN
__pycache__/config.cpython-313.pyc
Normal file
BIN
__pycache__/config.cpython-313.pyc
Normal file
Binary file not shown.
54
app/__init__.py
Normal file
54
app/__init__.py
Normal file
@ -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
|
||||
BIN
app/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
app/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
1
app/routes/__init__.py
Normal file
1
app/routes/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# Diese Datei macht das Verzeichnis zu einem Python-Paket
|
||||
BIN
app/routes/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
app/routes/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/routes/__pycache__/main.cpython-313.pyc
Normal file
BIN
app/routes/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
99
app/routes/main.py
Normal file
99
app/routes/main.py
Normal file
@ -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
|
||||
104
app/static/css/styles.css
Normal file
104
app/static/css/styles.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
230
app/static/js/script.js
Normal file
230
app/static/js/script.js
Normal file
@ -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 = '<span class="flex items-center"><svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>Generiere PDF...</span>';
|
||||
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;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
167
app/static/js/script.js.bak
Normal file
167
app/static/js/script.js.bak
Normal file
@ -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.');
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
14
app/templates/base.html
Normal file
14
app/templates/base.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ceph Storage Calculator</title>
|
||||
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
// ... existing code ...
|
||||
</body>
|
||||
</html>
|
||||
14
app/templates/errors/404.html
Normal file
14
app/templates/errors/404.html
Normal file
@ -0,0 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 text-center">
|
||||
<h1 class="display-1">404</h1>
|
||||
<h2>Seite nicht gefunden</h2>
|
||||
<p class="lead">Die angeforderte Seite konnte leider nicht gefunden werden.</p>
|
||||
<a href="{{ url_for('main.index') }}" class="btn btn-primary">Zurück zur Startseite</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
14
app/templates/errors/500.html
Normal file
14
app/templates/errors/500.html
Normal file
@ -0,0 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 text-center">
|
||||
<h1 class="display-1">500</h1>
|
||||
<h2>Interner Serverfehler</h2>
|
||||
<p class="lead">Es ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.</p>
|
||||
<a href="{{ url_for('main.index') }}" class="btn btn-primary">Zurück zur Startseite</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
488
app/templates/index.html
Normal file
488
app/templates/index.html
Normal file
@ -0,0 +1,488 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" x-data="{ darkMode: localStorage.getItem('darkMode') === 'true' || (!localStorage.getItem('darkMode') && window.matchMedia('(prefers-color-scheme: dark)').matches) }"
|
||||
:class="{ 'dark': darkMode }"
|
||||
class="transition-colors duration-500 ease-in-out">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>Ceph Max Storage Rechner</title>
|
||||
<!-- HTMX für interaktive Elemente -->
|
||||
<script src="https://unpkg.com/htmx.org@1.9.4"></script>
|
||||
<!-- Tailwind CSS für modernes Design -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
dark: {
|
||||
bg: '#1a1a1a',
|
||||
card: '#2d2d2d',
|
||||
border: '#404040',
|
||||
text: '#e5e5e5',
|
||||
input: '#404040',
|
||||
hover: '#505050'
|
||||
}
|
||||
},
|
||||
transitionProperty: {
|
||||
'all': 'all',
|
||||
'colors': 'color, background-color, border-color, text-decoration-color, fill, stroke',
|
||||
'opacity': 'opacity',
|
||||
'shadow': 'box-shadow',
|
||||
'transform': 'transform',
|
||||
},
|
||||
transitionDuration: {
|
||||
'200': '200ms',
|
||||
'300': '300ms',
|
||||
'500': '500ms',
|
||||
'700': '700ms',
|
||||
},
|
||||
transitionTimingFunction: {
|
||||
'ease-in-out': 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<!-- Alpine.js für reaktive Benutzeroberfläche -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.0/dist/cdn.min.js"></script>
|
||||
<script src="{{ url_for('static', filename='js/script.js') }}"></script>
|
||||
</head>
|
||||
<body class="bg-gray-100 dark:bg-dark-bg min-h-screen transition-all duration-500 ease-in-out" x-data="cephCalculator()">
|
||||
<div class="w-full px-4 py-8 bg-gray-100 dark:bg-dark-bg min-h-screen transition-all duration-500 ease-in-out">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<!-- Theme Toggle -->
|
||||
<div class="fixed top-4 right-4 z-50">
|
||||
<button @click="darkMode = !darkMode; localStorage.setItem('darkMode', darkMode)"
|
||||
class="p-2 rounded-lg bg-white dark:bg-dark-card shadow-lg hover:bg-gray-100 dark:hover:bg-dark-hover transition-all duration-500 ease-in-out transform hover:scale-105">
|
||||
<!-- Sonne Icon für Light Mode -->
|
||||
<svg x-show="!darkMode" class="w-6 h-6 text-gray-800 transition-opacity duration-500 ease-in-out" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
<!-- Mond Icon für Dark Mode -->
|
||||
<svg x-show="darkMode" class="w-6 h-6 text-gray-200 transition-opacity duration-500 ease-in-out" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<header class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-center text-blue-700 dark:text-blue-400">Ceph Max Storage Kapazitätsrechner</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400 text-center mt-2">Berechnen Sie die maximale Speichernutzung Ihres Ceph-Clusters</p>
|
||||
</header>
|
||||
|
||||
<main class="bg-white dark:bg-dark-card rounded-lg shadow-lg p-6 lg:p-8 transition-colors duration-200">
|
||||
<!-- Replikationstyp -->
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-semibold mb-3 text-gray-800 dark:text-dark-text">Replikationstyp</h2>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<label class="flex items-center">
|
||||
<input type="radio" name="replication_type" value="replication"
|
||||
x-model="replicationType" class="mr-2 h-5 w-5">
|
||||
<span class="text-gray-700 dark:text-dark-text">Replikation</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="radio" name="replication_type" value="erasure_coding"
|
||||
x-model="replicationType" class="mr-2 h-5 w-5">
|
||||
<span class="text-gray-700 dark:text-dark-text">Erasure Coding</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Replikationskonfiguration -->
|
||||
<div class="mb-6" x-show="replicationType === 'replication'">
|
||||
<h2 class="text-xl font-semibold mb-3 text-gray-800 dark:text-dark-text">Replikationskonfiguration</h2>
|
||||
<div class="flex items-center mb-3">
|
||||
<label class="w-32 text-gray-700 dark:text-dark-text">Anzahl Replikate:</label>
|
||||
<div class="relative group">
|
||||
<input type="number" x-model="replicas" min="1" max="10"
|
||||
class="border rounded p-2 w-20 text-center bg-white dark:bg-dark-input dark:border-dark-border dark:text-dark-text">
|
||||
<div class="absolute hidden group-hover:block bg-gray-800 text-white p-2 rounded text-sm">
|
||||
Anzahl der Kopien, die von jedem Datenblock erstellt werden
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center mb-3">
|
||||
<label class="w-32 text-gray-700 dark:text-dark-text">Min. Replikate (min_size):</label>
|
||||
<input type="number" x-model="minSize" min="1" :max="replicas"
|
||||
class="border rounded p-2 w-20 text-center bg-white dark:bg-dark-input dark:border-dark-border dark:text-dark-text">
|
||||
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400">Minimale Anzahl von Kopien für I/O-Operationen</span>
|
||||
</div>
|
||||
<div class="flex items-center mb-3">
|
||||
<label class="w-32 text-gray-700 dark:text-dark-text">Speichereinheit:</label>
|
||||
<select x-model="storageUnit" class="border rounded p-2 bg-white dark:bg-dark-input dark:border-dark-border dark:text-dark-text">
|
||||
<option value="GB">GB</option>
|
||||
<option value="TB">TB</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Erasure Coding Konfiguration -->
|
||||
<div class="mb-6" x-show="replicationType === 'erasure_coding'">
|
||||
<h2 class="text-xl font-semibold mb-3 text-gray-800 dark:text-dark-text">Erasure Coding Konfiguration</h2>
|
||||
<div class="flex flex-wrap gap-6">
|
||||
<div class="flex items-center">
|
||||
<label class="w-32 text-gray-700 dark:text-dark-text">Datenchunks (k):</label>
|
||||
<input type="number" x-model="k" min="1" max="20"
|
||||
class="border rounded p-2 w-20 text-center bg-white dark:bg-dark-input dark:border-dark-border dark:text-dark-text">
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<label class="w-32 text-gray-700 dark:text-dark-text">Codierungschunks (m):</label>
|
||||
<input type="number" x-model="m" min="1" max="10"
|
||||
class="border rounded p-2 w-20 text-center bg-white dark:bg-dark-input dark:border-dark-border dark:text-dark-text">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center mt-4">
|
||||
<label class="w-32 text-gray-700 dark:text-dark-text">Speichereinheit:</label>
|
||||
<select x-model="storageUnit" class="border rounded p-2 bg-white dark:bg-dark-input dark:border-dark-border dark:text-dark-text">
|
||||
<option value="GB">GB</option>
|
||||
<option value="TB">TB</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nodes und OSDs -->
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-semibold mb-3 text-gray-800 dark:text-dark-text">Nodes und OSDs</h2>
|
||||
|
||||
<template x-for="(node, nodeIndex) in nodes" :key="nodeIndex">
|
||||
<div class="border p-4 rounded-lg mb-4 bg-gray-50 dark:bg-dark-input dark:border-dark-border">
|
||||
<div class="flex flex-wrap items-center gap-4 mb-3">
|
||||
<h3 class="font-medium dark:text-dark-text" x-text="`Node ${nodeIndex + 1}`"></h3>
|
||||
<button @click="removeNode(nodeIndex)"
|
||||
class="bg-red-500 text-white px-3 py-1 rounded hover:bg-red-600 ml-auto">
|
||||
Entfernen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-group">
|
||||
<label class="block text-gray-700 dark:text-dark-text mb-2">OSD Anzahl:</label>
|
||||
<input type="number" x-model="node.osd_count" min="1"
|
||||
class="border rounded p-2 w-full text-center bg-white dark:bg-dark-input dark:border-dark-border dark:text-dark-text">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="block text-gray-700 dark:text-dark-text mb-2">OSD Größe:</label>
|
||||
<div class="flex items-center">
|
||||
<input type="number" x-model="node.osd_size_gb" min="1"
|
||||
class="border rounded p-2 w-full text-center bg-white dark:bg-dark-input dark:border-dark-border dark:text-dark-text">
|
||||
<span class="ml-2 text-gray-700 dark:text-dark-text" x-text="storageUnit"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<button @click="addNode()"
|
||||
class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
|
||||
Node hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Berechnungsbutton -->
|
||||
<div class="flex justify-center mt-8 space-x-4">
|
||||
<button @click="calculateCapacity()"
|
||||
class="bg-blue-600 text-white px-6 py-3 rounded-lg font-semibold
|
||||
shadow-md hover:bg-blue-700 transition duration-300"
|
||||
:disabled="isCalculating">
|
||||
<span x-show="!isCalculating">Kapazität berechnen</span>
|
||||
<span x-show="isCalculating" class="flex items-center">
|
||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Berechne...
|
||||
</span>
|
||||
</button>
|
||||
<button @click="exportConfig()"
|
||||
class="bg-green-600 text-white px-6 py-3 rounded-lg font-semibold
|
||||
shadow-md hover:bg-green-700 transition duration-300">
|
||||
Konfiguration exportieren
|
||||
</button>
|
||||
<label class="bg-purple-600 text-white px-6 py-3 rounded-lg font-semibold
|
||||
shadow-md hover:bg-purple-700 transition duration-300 cursor-pointer">
|
||||
Konfiguration importieren
|
||||
<input type="file" @change="importConfig($event)" class="hidden" accept=".json">
|
||||
</label>
|
||||
<button @click="generatePDF()"
|
||||
class="bg-red-600 text-white px-6 py-3 rounded-lg font-semibold
|
||||
shadow-md hover:bg-red-700 transition duration-300"
|
||||
:disabled="!result.raw_total">
|
||||
PDF Report generieren
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Ergebnisbereich -->
|
||||
<div class="mt-8" x-show="result.raw_total > 0">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-800 dark:text-dark-text">Ergebnis</h2>
|
||||
<div class="bg-blue-50 dark:bg-dark-input p-6 rounded-lg shadow border border-blue-200 dark:border-dark-border">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="p-3">
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-1">Gesamt-Rohspeicher:</p>
|
||||
<p class="text-2xl font-bold text-gray-800 dark:text-dark-text">
|
||||
<span x-text="storageUnit === 'TB' ? `${(result.raw_total / 1024).toFixed(2)} TB` : `${result.raw_total} GB`"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-3 bg-green-50 dark:bg-dark-input rounded-lg border border-green-100 dark:border-dark-border">
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-1">Maximale empfohlene Speichernutzung:</p>
|
||||
<p class="text-2xl font-bold text-green-600 dark:text-green-400" x-text="`${result.max_usage_percent}%`"></p>
|
||||
<p class="text-xl font-semibold text-gray-800 dark:text-dark-text mt-2">
|
||||
<span x-text="storageUnit === 'TB' ? `${result.max_usage_tb} TB` : `${result.max_usage_gb} GB`"></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ausfalltoleranz-Informationen -->
|
||||
<div class="mt-4 border-t border-blue-200 dark:border-dark-border pt-4">
|
||||
<h3 class="text-lg font-semibold mb-3 text-gray-800 dark:text-dark-text">Ausfalltoleranz-Analyse</h3>
|
||||
|
||||
<!-- Node-Ausfalltoleranz -->
|
||||
<div class="p-3 rounded-lg mb-3"
|
||||
:class="result.node_failure_tolerance ? 'bg-green-50 dark:bg-dark-input border border-green-200 dark:border-dark-border' : 'bg-red-50 dark:bg-dark-input border border-red-200 dark:border-dark-border'">
|
||||
<div class="flex items-center mb-2">
|
||||
<span class="font-semibold text-gray-800 dark:text-dark-text mr-2">Node-Ausfalltoleranz:</span>
|
||||
<span class="px-2 py-1 rounded text-xs font-semibold"
|
||||
:class="result.node_failure_tolerance ? 'bg-green-600 text-white' : 'bg-red-600 text-white'"
|
||||
x-text="result.node_failure_tolerance ? 'Vorhanden' : 'Nicht vorhanden'"></span>
|
||||
<span x-show="result.multi_failure_tolerance" class="ml-2 px-2 py-1 rounded text-xs font-semibold bg-blue-600 text-white"
|
||||
x-text="`Multi-Node (${result.max_failure_nodes})`"></span>
|
||||
</div>
|
||||
<p class="text-sm dark:text-gray-300" x-text="result.node_failure_info"></p>
|
||||
<div class="mt-2 grid grid-cols-1 md:grid-cols-2 gap-2 text-sm">
|
||||
<div>
|
||||
<span class="text-gray-600 dark:text-gray-400">Größter Node:</span>
|
||||
<span class="font-medium ml-1 dark:text-dark-text" x-text="storageUnit === 'TB' ? `${(result.largest_node_gb / 1024).toFixed(2)} TB` : `${result.largest_node_gb} GB`"></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-600 dark:text-gray-400">Rohspeicher nach Nodeausfall:</span>
|
||||
<span class="font-medium ml-1 dark:text-dark-text" x-text="storageUnit === 'TB' ? `${(result.raw_after_failure_gb / 1024).toFixed(2)} TB` : `${result.raw_after_failure_gb} GB`"></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-600 dark:text-gray-400">Nutzbarer Speicher nach Ausfall:</span>
|
||||
<span class="font-medium ml-1 dark:text-dark-text" x-text="storageUnit === 'TB' ? `${(result.usable_after_failure_gb / 1024).toFixed(2)} TB` : `${result.usable_after_failure_gb} GB`"></span>
|
||||
</div>
|
||||
<div x-show="result.multi_failure_tolerance || result.max_failure_nodes > 1">
|
||||
<span class="text-gray-600 dark:text-gray-400">Nutzbarer Speicher nach max. Ausfällen:</span>
|
||||
<span class="font-medium ml-1 dark:text-dark-text" x-text="storageUnit === 'TB' ? `${(result.usable_after_max_failures_gb / 1024).toFixed(2)} TB` : `${result.usable_after_max_failures_gb} GB`"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Min-Size Info -->
|
||||
<div class="mt-3 p-2 bg-blue-100 dark:bg-dark-input rounded text-sm" x-show="result.min_size">
|
||||
<p class="dark:text-gray-300">
|
||||
<span class="font-semibold">Min-Size:</span>
|
||||
<span x-text="result.min_size"></span> -
|
||||
Diese Einstellung bestimmt die minimale Anzahl von Replikaten für I/O-Operationen.
|
||||
Der Cluster kann bis zu <span x-text="result.max_failure_nodes"></span> Nodes verlieren,
|
||||
bevor die Daten unzugänglich werden.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OSD-Ausfalltoleranz -->
|
||||
<div class="p-3 rounded-lg"
|
||||
:class="result.osd_failure_tolerance ? 'bg-green-50 dark:bg-dark-input border border-green-200 dark:border-dark-border' : 'bg-yellow-50 dark:bg-dark-input border border-yellow-200 dark:border-dark-border'">
|
||||
<div class="flex items-center mb-2">
|
||||
<span class="font-semibold text-gray-800 dark:text-dark-text mr-2">OSD-Ausfalltoleranz:</span>
|
||||
<span class="px-2 py-1 rounded text-xs font-semibold"
|
||||
:class="result.osd_failure_tolerance ? 'bg-green-600 text-white' : 'bg-yellow-600 text-white'"
|
||||
x-text="result.osd_failure_tolerance ? 'Vorhanden' : 'Eingeschränkt'"></span>
|
||||
</div>
|
||||
<p class="text-sm dark:text-gray-300" x-text="result.osd_failure_info"></p>
|
||||
</div>
|
||||
|
||||
<!-- Empfehlungen basierend auf Ceph-Dokumentation -->
|
||||
<div class="mt-4 p-3 bg-blue-50 dark:bg-dark-input rounded-lg border border-blue-200 dark:border-dark-border">
|
||||
<h4 class="font-semibold text-gray-800 dark:text-dark-text mb-2">Empfehlungen gemäß Ceph-Dokumentation:</h4>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 dark:text-gray-300">
|
||||
<li>Stelle sicher, dass nach Nodeausfällen genügend Kapazität vorhanden ist, um die 'full ratio' nicht zu erreichen.</li>
|
||||
<li>Bei Replikation sollte die Anzahl der Nodes größer sein als der Replikationsfaktor.</li>
|
||||
<li>Bei Erasure Coding sollte die Anzahl der Nodes größer sein als k+m (Datenchunks + Codierungschunks).</li>
|
||||
<li>Die Performance bei der Wiederherstellung hängt von der Netzwerk- und Speichergeschwindigkeit ab.</li>
|
||||
<li>Beachte, dass nach einem Nodeausfall das Wiederausbalancieren des Clusters länger dauert, je größer der ausgefallene Node ist.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="mt-8 text-center text-gray-500 dark:text-gray-400 text-sm">
|
||||
<p>Basierend auf Ceph-Dokumentation | Ergebnisse dienen nur als Richtwert</p>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
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.');
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1
app/utils/__init__.py
Normal file
1
app/utils/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# Diese Datei macht das Verzeichnis zu einem Python-Paket
|
||||
BIN
app/utils/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
app/utils/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/utils/__pycache__/ceph_calculator.cpython-313.pyc
Normal file
BIN
app/utils/__pycache__/ceph_calculator.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/utils/__pycache__/pdf_generator.cpython-313.pyc
Normal file
BIN
app/utils/__pycache__/pdf_generator.cpython-313.pyc
Normal file
Binary file not shown.
204
app/utils/ceph_calculator.py
Normal file
204
app/utils/ceph_calculator.py
Normal file
@ -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
|
||||
217
app/utils/pdf_generator.py
Normal file
217
app/utils/pdf_generator.py
Normal file
@ -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
|
||||
26
config.py
Normal file
26
config.py
Normal file
@ -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
|
||||
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@ -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
|
||||
Reference in New Issue
Block a user