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:
Samuel Müller
2025-03-26 08:40:32 +01:00
commit f00c1f48e9
26 changed files with 1744 additions and 0 deletions

39
.cursorrules Normal file
View 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
View 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
View 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.

Binary file not shown.

54
app/__init__.py Normal file
View 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

Binary file not shown.

1
app/routes/__init__.py Normal file
View File

@ -0,0 +1 @@
# Diese Datei macht das Verzeichnis zu einem Python-Paket

Binary file not shown.

Binary file not shown.

99
app/routes/main.py Normal file
View 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
View 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
View 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
View 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
View 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>

View 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 %}

View 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
View 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
View File

@ -0,0 +1 @@
# Diese Datei macht das Verzeichnis zu einem Python-Paket

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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
View 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
View 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
View 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

6
run.py Normal file
View File

@ -0,0 +1,6 @@
from app import create_app, db
app = create_app()
if __name__ == '__main__':
app.run(debug=True)