From f00c1f48e921ffd7c62d91cdc70d32d3544e8970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20M=C3=BCller?= Date: Wed, 26 Mar 2025 08:40:32 +0100 Subject: [PATCH] =?UTF-8?q?Erstelle=20die=20Grundstruktur=20f=C3=BCr=20die?= =?UTF-8?q?=20Ceph=20Max=20Storage=20Rechner-Anwendung=20mit=20Flask=20und?= =?UTF-8?q?=20HTMX.=20F=C3=BCge=20Konfigurationsdateien,=20Routen,=20Model?= =?UTF-8?q?le,=20Templates=20und=20statische=20Dateien=20hinzu.=20Implemen?= =?UTF-8?q?tiere=20grundlegende=20Funktionen=20zur=20Berechnung=20der=20Sp?= =?UTF-8?q?eichernutzung=20und=20zur=20PDF-Generierung.=20Integriere=20CSR?= =?UTF-8?q?F-Schutz=20und=20Logging.=20Stelle=20sicher,=20dass=20die=20Anw?= =?UTF-8?q?endung=20modular=20und=20wartbar=20ist.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursorrules | 39 ++ .env | 4 + README.md | 56 ++ __pycache__/config.cpython-313.pyc | Bin 0 -> 1550 bytes app/__init__.py | 54 ++ app/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 3106 bytes app/routes/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 157 bytes app/routes/__pycache__/main.cpython-313.pyc | Bin 0 -> 4991 bytes app/routes/main.py | 99 ++++ app/static/css/styles.css | 104 ++++ app/static/js/script.js | 230 +++++++++ app/static/js/script.js.bak | 167 ++++++ app/templates/base.html | 14 + app/templates/errors/404.html | 14 + app/templates/errors/500.html | 14 + app/templates/index.html | 488 ++++++++++++++++++ app/utils/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 156 bytes .../ceph_calculator.cpython-313.pyc | Bin 0 -> 6894 bytes .../__pycache__/pdf_generator.cpython-313.pyc | Bin 0 -> 10819 bytes app/utils/ceph_calculator.py | 204 ++++++++ app/utils/pdf_generator.py | 217 ++++++++ config.py | 26 + requirements.txt | 6 + run.py | 6 + 26 files changed, 1744 insertions(+) create mode 100644 .cursorrules create mode 100644 .env create mode 100644 README.md create mode 100644 __pycache__/config.cpython-313.pyc create mode 100644 app/__init__.py create mode 100644 app/__pycache__/__init__.cpython-313.pyc create mode 100644 app/routes/__init__.py create mode 100644 app/routes/__pycache__/__init__.cpython-313.pyc create mode 100644 app/routes/__pycache__/main.cpython-313.pyc create mode 100644 app/routes/main.py create mode 100644 app/static/css/styles.css create mode 100644 app/static/js/script.js create mode 100644 app/static/js/script.js.bak create mode 100644 app/templates/base.html create mode 100644 app/templates/errors/404.html create mode 100644 app/templates/errors/500.html create mode 100644 app/templates/index.html create mode 100644 app/utils/__init__.py create mode 100644 app/utils/__pycache__/__init__.cpython-313.pyc create mode 100644 app/utils/__pycache__/ceph_calculator.cpython-313.pyc create mode 100644 app/utils/__pycache__/pdf_generator.cpython-313.pyc create mode 100644 app/utils/ceph_calculator.py create mode 100644 app/utils/pdf_generator.py create mode 100644 config.py create mode 100644 requirements.txt create mode 100644 run.py diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..1c83f19 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,39 @@ +// HTMX with Flask .cursorrules + +// HTMX and Flask best practices + +const htmxFlaskBestPractices = [ + "Use Flask's render_template for server-side rendering", + "Implement Flask-WTF for form handling", + "Utilize Flask's url_for for generating URLs", + "Use Flask's jsonify for JSON responses", + "Implement Flask-SQLAlchemy for database operations", + "Utilize Flask's Blueprint for modular applications", +]; + +// Folder structure + +const folderStructure = ` +app/ + templates/ + static/ + css/ + js/ + models/ + routes/ + __init__.py +config.py +run.py +`; + +// Additional instructions + +const additionalInstructions = ` +1. Use Jinja2 templating with HTMX attributes +2. Implement proper CSRF protection with Flask-WTF +3. Utilize Flask's request object for handling HTMX requests +4. Use Flask-Migrate for database migrations +5. Implement proper error handling and logging +6. Follow Flask's application factory pattern +7. Use environment variables for configuration +`; \ No newline at end of file diff --git a/.env b/.env new file mode 100644 index 0000000..7e867c5 --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +SECRET_KEY=your-secure-secret-key-here +DATABASE_URL=sqlite:///app.db +FLASK_ENV=development +FLASK_DEBUG=1 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2c5405b --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# Ceph Max Storage Rechner + +Eine moderne Web-Anwendung zur Berechnung der maximal zulässigen Speichernutzung in einem Ceph-Cluster. + +## Funktionen + +- Berechnung der maximalen Speichernutzung basierend auf Ceph-Dokumentation +- Unterstützung für Replikation und Erasure Coding +- Dynamisches Hinzufügen und Entfernen von Nodes und OSDs +- Benutzerfreundliche Oberfläche mit modernem Design + +## Installation + +1. Repository klonen: + ``` + git clone + cd ceph-calculator + ``` + +2. Python-Umgebung einrichten: + ``` + python -m venv venv + venv\Scripts\activate # Windows + source venv/bin/activate # Linux/Mac + ``` + +3. Abhängigkeiten installieren: + ``` + pip install -r requirements.txt + ``` + +4. Anwendung starten: + ``` + python run.py + ``` + +5. Öffnen Sie in einem Browser: `http://localhost:5000` + +## Verwendung + +1. Wählen Sie den Replikationstyp: Replikation oder Erasure Coding +2. Geben Sie die entsprechenden Parameter ein (Anzahl der Replikate oder k/m-Werte für EC) +3. Fügen Sie Nodes hinzu und konfigurieren Sie deren OSDs mit entsprechenden Speichergrößen +4. Klicken Sie auf "Kapazität berechnen", um das Ergebnis zu sehen + +## Technologie + +- Backend: Flask +- Frontend: HTMX, Alpine.js, Tailwind CSS +- Responsive Design für alle Geräte + +## Hinweis + +Die Berechnungen basieren auf den allgemeinen Empfehlungen aus der Ceph-Dokumentation und +dienen als Richtwert. Für genaue Kapazitätsplanung sollten Sie die Ceph-Dokumentation konsultieren +und Ihre spezifischen Clusteranforderungen berücksichtigen. \ No newline at end of file diff --git a/__pycache__/config.cpython-313.pyc b/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b5d98457d3d5b68662ab1493d10afe57c147d38d GIT binary patch literal 1550 zcmb6ZO>Y`UbQabuEFbX~!J#R}w5da+n1h>CiqcjX)(+;YWgxAkRYzdPWG&bwvrD78 zIrvo7CznHneW`tPe@rE+?SV|y%4$!!S@j{;&hn8GCpmbe_vU@idvD&%xz{TKw!ZrH z@0x4};2(<_Eys{J?=j*TfB_pR8QN#ZW!i=k85-v4r zxYAN<;Tqm*sa32lP){;mjIYF@gi3|Xj;KGbD+az7i^X;vU)Y@ZsS_c!+`tG?5ut`wZPhK>gV5tvxjxifL1;@Mx=~kD ztZGbc({w7Zky41JQhN;>WKhe)a=nEmLNr48X4!Z|`E5;6sZc5FxT+8ufL~_fnZyGr zyMe|ENYT)kqF9J0(kPoxrdCslcrlgFL1Mi@VofFWLkU7ALy3GoostmiR;eJ-s|6`5 zWmlxa1$p3t`GaEdi+nD#LFX0U^Q8Tx z^R)f+ca)Ytf@2IC4Ie_XlwO z2|}l$XMb^TaX+>fJBS=D9xfim4r3=b&ET>re!MF@2kswN_Sg2-_A`5#gTV1erYHJ4 Qxb>2s;XeEqFw{TnKLSNgo&W#< literal 0 HcmV?d00001 diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..aab4056 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,54 @@ +import logging +from logging.handlers import RotatingFileHandler +import os +from flask import Flask, render_template +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate +from flask_wtf.csrf import CSRFProtect +from config import Config + +db = SQLAlchemy() +migrate = Migrate() +csrf = CSRFProtect() + +def create_app(config_class=Config): + app = Flask(__name__) + app.config.from_object(config_class) + + # Initialisierung der Erweiterungen + db.init_app(app) + migrate.init_app(app, db) + csrf.init_app(app) + + # Logging-Konfiguration + if not app.debug and not app.testing: + if not os.path.exists('logs'): + os.mkdir('logs') + file_handler = RotatingFileHandler('logs/app.log', maxBytes=10240, backupCount=10) + file_handler.setFormatter(logging.Formatter( + '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]' + )) + file_handler.setLevel(logging.INFO) + app.logger.addHandler(file_handler) + app.logger.setLevel(logging.INFO) + app.logger.info('Anwendung startet') + + # Blueprints registrieren + from app.routes.main import bp as main_bp + app.register_blueprint(main_bp) + + # Fehlerbehandlung + @app.errorhandler(404) + def not_found_error(error): + return render_template('errors/404.html'), 404 + + @app.errorhandler(500) + def internal_error(error): + db.session.rollback() + return render_template('errors/500.html'), 500 + + @app.context_processor + def utility_processor(): + return dict(enumerate=enumerate) + + return app \ No newline at end of file diff --git a/app/__pycache__/__init__.cpython-313.pyc b/app/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..80e5e51a7221e1224f3d7d99e401a8b7a7ab7e52 GIT binary patch literal 3106 zcmb^zO>YxNbauUKuRr2VNcak*n52Oi+K?6k0wE-tHc&gH#jEtou2vg+?JTT!-Pv_R zkcxtOpp-*$fGS9la^X;J{R#aARa`7342k-AXl_)2LvMYv-t{_s_0o}c-kbNe^WMDo zW;WZ}A_zwIr{7Drf(ZRV7v5l9Wb->f?jQ+COd4Sd*))@4F`H=60yqG)K$=VOm`??9 z(4lkbP)fjpgY)TdDuN>p9!y75F&uO7P`WJ@$MFQp3B738{T&u_k(O0^F4nT@$iPICfUgPpRb@ScNWVCEQ>{N2JW?huE~$ybYY-h-nnHQl4@5M*)Ni1>oDf2lE}2 zZSt~S(9p0;Gnfe`!%?NJBNN)XrmKmjRsv-OkDrNpm=7ly&pt@TJo<|zeM`gc?m#F0 zY8~G??7rQT_2Gbv7XE=3~P` zVT(4C@f`EnD08@F;i41le`@{z`*`#DeMljHACe%2(>qhUi~z)e$sB!NdLrOUgM?Sx zErL?SlaQj_&Ehw>mq^E&(rqo$JV+mJG3ugHyhSEB!Mu)=;yB@Svt(V3(fNo{sSE<5 zAvlE%+5(|f2xaBUJ2SRwk?5q7zg(@1m{r5RI?_L&Sb1A3t4S--KcK5$s=A@j)hSq( zRm)OJl$F&CSXUH#%H@KrW~hdlEPQ^o#)Y@PC442qrEcg`|zbdL2o7g&XZ0OkFlwH;_t=A-jc>wnr!Z+Bo9VN){ z$f?|frD7{*m8+@_Ju>&MnXi^r!?toGWR`vUq%1IKByFwDIJn zet!aN@S93GBja{1Z%tu_I{3`+5)C?Bv%kJ7X2U~6jhO8MLrIRXg-H^!;7m2sAObdZ zou+~lN=%!a<%AK_en#S&VXN3sbT=lwFN(=l8Cq9XA50JuAu<=a8Cenb)Py~Cp=a&c z5aFF5mxr%8=q>WXtnhCgi8)ELouxNoST*2aolA`+TtUm*H~|*!Bgb;XhmNYP>6$$w zSFo7}w@pm?U*yJfVsC>bs|_Z4xSfkyO&nMj1{}-Dy_oKl2(nBoL6%7{?_8lITEu2q zHYYE^&5CIUCxVk>kVPa^Zd|~G%Uife_<}lFEs+qEBfZoKYg&Y(#YKYZ70t3O!j~@> zG>mDfkeH>~qb4pZHk=3vL1mYyQrM>nATdTumvHZokA6UcR0d=tN}=Gs76_*qMU#j$ zr?FXuKM$4_&A=l4!#1pz;J5IEnAEFk1;e?Lm{SW=jkgBr04HtritUFo86vTU=W-ri zAr=WxM`J~Jpvaz9i2g#Jtl;hN7fkOgO!LE{*_&vL#z{r|W5H*ENiT3qfpnZjYZ39b zaO=qI$SN1Qk-DB*;ks*F_rmEqH}E8g_|T2?_4It8&ULME(G{+%#&yk0i@kNOXN?nX zT)2K=zOT;hcpOAAaqi^Y$pvXCyOdoMJLaZuPS0Jtd2OM%BKFtB{lKldJ8ja}Rfuzisyhz&=<7au{Pr5ZeLnwy@BW)*_!FA}6x{QeV$ZYLCL6HFf}`DCYbYk0ib*FVv#CZ> zcVe3Iq}|P}LH7VHCjl`nY{D1mThmE?r(=+Ihag*D=t|?EiRr_Uv^B_X!ee^WB;KG+ z+r>ee|5yMbrcVeQrV}j|OdBdbL?@a6m_AinN?ABvHVaicyzwzSNxmosMq~lg{HuNyG?dz{*xnKA8K1BnJ=-B=j DMtOvC literal 0 HcmV?d00001 diff --git a/app/routes/__init__.py b/app/routes/__init__.py new file mode 100644 index 0000000..04a34a8 --- /dev/null +++ b/app/routes/__init__.py @@ -0,0 +1 @@ +# Diese Datei macht das Verzeichnis zu einem Python-Paket \ No newline at end of file diff --git a/app/routes/__pycache__/__init__.cpython-313.pyc b/app/routes/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..70048d13b4eb9a846ec6b7d1e826a2205523d6ef GIT binary patch literal 157 zcmey&%ge<81aB8VO1A>ik3k$5V1zP0vj7=W8PXXv8U0o=6fpsLpFvW$Y@DrPLW@(2 zierj%OH*@lQj20-@{>z*Q}arSW1RC-Qe&J`3o>F73kqV2@=Hrni(}&BGxIV_;^XxS hDsOSvuVPSyl{Ud}L;1WGrF^vH;0xD2)IB literal 0 HcmV?d00001 diff --git a/app/routes/__pycache__/main.cpython-313.pyc b/app/routes/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4e959fad00768b7a0c5baca50a887bad3887b23f GIT binary patch literal 4991 zcmb_gT}&HS7QQp~*kfaR0Aoy`4TeC1!(W?_hE^e6{u`h)VPdphTe2qhU}j^+^v*aT zsM^i;Wr_L_`Vf_UD%$NsR_bc0b{~+Mhjc^QNc%FxsbHrh8|_9b<&B6uRO!o}JN6id z->$S*aOTdv=bU@)nRCx~t~cCnCxX`g_kSh+>_Oi~n90UNdj$UHr0A7C+S^euyq0Vj4Eed}P+fD5~fJ~QaXZs27GJ=ha* zj~q0Y+kDodNbw0+lQdisp=uFtgAqD6QW_~8Ig;%cQr^z9trYKgljfb3=tOP`m+it` z#Jh6b?gDP#F5I3xH&?)2o?wE-PsvI_o9^gJro~At$*Rs_QI3l^s)`enNkJ8LI~G4k zi;Ak-f2pKoY3z#bP=F;mCM89^D3-<`phkts$sncsV?r{PCSuW;I5{4T36nxhQm??2 zHzCR*CNq=qu_zWNQ&@#zd)F0JRL;CNtXn4pN!F=W-O}HCp3F>$>Ub)y#0ni{ffw?> zQ32!z3g)rn+~!dBq^BiaCp^!DzzA|XY@N<^7Ri^-&j zBR#2DdIEN#M7mRPG13jDM_@w{oJxan5n@=&&%Vc>e`yCKW{n-lKyRRfC=3JHHj;}H^PN0p6haNTfjEay*oBXcGbpg)fRS`x^~fP>wY* zFYkc&71&$eM()gk^_+G#snGi)6-eVCweg? zr=%1UR4FA#)hmdkQ6l_q{kAXieXrO`W(CybYqf%bSnV^Y$II@Fz6P*4ovce?gYEiVlQ#CZcC;T z3Glez;8RqrJ9?)88Hj6jhl+)mctMC=)SY7~JRzu2aVi#c>ejfR3Ol+W0D7{1ATH2t zSX9#RAt+HHX*G~nhu1!wu|DFwYu?Ja zey#qLR@IU5p1y6-yxp3s`*BtEXYIdh*XrKLRGrk=ibq`a&WumI<+JLiQ=d%DJ8yPo z{4E)8>kRwchTO%o)=$|_*g5Zl*91?z-mE{HJdzw_pG2(&D)0>wmz$1Kygmf9{<>_lI)L-;m*g zH;-#v%Xe(ck84F`L=qfoiXQvx@r4im^2S2LV(qQ>S31t!?>Lv~;I&idHDBlfJ8G!X zY6dj_V1^sI7t^?N-?8U@TxW=q7fKg=sE_Jgp*&pg0o^-r7uXBmE(e8lCs+;;4o`q?f+hs$^=Z@m&FI-1OpJ#KInzy z#GPA7RZ55f0on`#|5}o$kPYU_VhLPlN(JIfQ^bEAu6%)Bp|tr$sKQ+gFA#-NVGxkTTyfN z^3wt%K?^Ar;GzRnhVB-WsGzDsY@FOhbay;;Sx%;ecvKc9#ObCz;C_oSfcYkbn+3r( z4qOiw;=@3KC0Rsg0mh?wRL4P>(w&kb$%-n-F;TZBp&kHimSQR<`2>@j1}0@KZX~3M z5CZhLnUEGjS_yd#NYH{`hllPUm%j_-%7>4`yF!vMpmr9oU1q)}S48|aOr0h?w{nnu zC=1yuT=jjfI>XiMf!lzE#bwv0u1#qbM_0JUWv+2&%zVsw3D=x`g$pcmfiIj3ZHx9V zJC^EJ8v2(T`u7%MSGbyGuI7u%g`UNVFNc?UR)S}igJ*I=Z);UQ%Xm9)AJ@EnnyU|1 zntkWmud*WZjVoNsGS`x09nz}a&3K2kv%KczH5U)K?Q6Jc&6G9IIR3*`#b*Jxk zyXG5uz`pyx;Q7g0mc`DmYztG1cqzEjHG02mG}HBSt@C}&_lpN?cpp6H;VXW0wC5=L zQyq}6OHTp)8%O9J*AYV70zj91WrQAe_H&k{w)Wm*mb*<9^zR;X6Z&>1)9wGcW-`j+Ea6S+`IW^-naqf<_;rj)&;vLsb4f@_dhqA3KUy z9F_MSm2>SGN8MH1KN!! ({ + osd_count: parseInt(node.osd_count), + osd_size_gb: this.getOsdSizeInGB(parseFloat(node.osd_size_gb)) + })), + min_size: parseInt(this.minSize), + storage_unit: this.storageUnit + }; + + this.isCalculating = true; + + fetch('/calculate', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') + }, + body: JSON.stringify(data) + }) + .then(response => response.json()) + .then(data => { + this.result = data; + }) + .catch(error => { + console.error('Fehler bei der Berechnung:', error); + alert('Es ist ein Fehler bei der Berechnung aufgetreten. Bitte versuchen Sie es erneut.'); + }) + .finally(() => { + this.isCalculating = false; + }); + }, + + exportConfig() { + const config = { + replicationType: this.replicationType, + replicas: this.replicas, + minSize: this.minSize, + k: this.k, + m: this.m, + storageUnit: this.storageUnit, + nodes: this.nodes, + result: this.result + }; + + const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'ceph-config.json'; + a.click(); + URL.revokeObjectURL(url); + }, + + importConfig(event) { + const file = event.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + try { + const config = JSON.parse(e.target.result); + Object.assign(this, config); + // Berechne automatisch nach dem Import + this.calculateCapacity(); + } catch (error) { + alert('Fehler beim Importieren der Konfiguration. Bitte überprüfen Sie das Dateiformat.'); + } + }; + reader.readAsText(file); + }, + + generatePDF() { + if (!this.result.raw_total) { + alert('Bitte führen Sie zuerst eine Berechnung durch.'); + return; + } + + // Validiere die Daten vor dem Senden + if (!this.nodes || this.nodes.length === 0) { + alert('Bitte fügen Sie mindestens einen Node hinzu.'); + return; + } + + // Validiere die Node-Daten + for (let i = 0; i < this.nodes.length; i++) { + const node = this.nodes[i]; + if (!node.osd_count || node.osd_count < 1) { + alert(`Bitte geben Sie eine gültige Anzahl von OSDs für Node ${i + 1} ein.`); + return; + } + if (!node.osd_size_gb || node.osd_size_gb <= 0) { + alert(`Bitte geben Sie eine gültige OSD-Größe für Node ${i + 1} ein.`); + return; + } + } + + const data = { + replication_type: this.replicationType, + replicas: parseInt(this.replicas), + min_size: parseInt(this.minSize), + k: parseInt(this.k), + m: parseInt(this.m), + storage_unit: this.storageUnit, + nodes: this.nodes.map(node => ({ + osd_count: parseInt(node.osd_count), + osd_size_gb: parseFloat(this.getOsdSizeInGB(parseFloat(node.osd_size_gb))) + })), + result: { + raw_total: parseFloat(this.result.raw_total), + max_usage_percent: parseFloat(this.result.max_usage_percent), + max_usage_gb: parseFloat(this.result.max_usage_gb), + max_usage_tb: parseFloat(this.result.max_usage_tb), + node_failure_tolerance: Boolean(this.result.node_failure_tolerance), + node_failure_info: String(this.result.node_failure_info || ''), + multi_failure_tolerance: Boolean(this.result.multi_failure_tolerance), + max_failure_nodes: parseInt(this.result.max_failure_nodes || 0), + osd_failure_tolerance: Boolean(this.result.osd_failure_tolerance), + osd_failure_info: String(this.result.osd_failure_info || '') + } + }; + + // Zeige Ladeindikator + const button = document.querySelector('button[onclick="generatePDF()"]'); + const originalText = button.innerHTML; + button.innerHTML = 'Generiere PDF...'; + button.disabled = true; + + // Hole CSRF-Token + const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); + if (!csrfToken) { + alert('CSRF-Token nicht gefunden. Bitte laden Sie die Seite neu.'); + button.innerHTML = originalText; + button.disabled = false; + return; + } + + fetch('/generate-pdf', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken + }, + body: JSON.stringify(data) + }) + .then(response => { + if (!response.ok) { + return response.json().then(err => { + throw new Error(err.error || `HTTP error! status: ${response.status}`); + }); + } + return response.blob(); + }) + .then(blob => { + if (!blob || blob.size === 0) { + throw new Error('Die generierte PDF-Datei ist leer'); + } + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'ceph-report.pdf'; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + }) + .catch(error => { + console.error('Fehler bei der PDF-Generierung:', error); + alert(`Fehler bei der PDF-Generierung: ${error.message}`); + }) + .finally(() => { + // Stelle den Button wieder her + button.innerHTML = originalText; + button.disabled = false; + }); + } + }; +} \ No newline at end of file diff --git a/app/static/js/script.js.bak b/app/static/js/script.js.bak new file mode 100644 index 0000000..aca56c4 --- /dev/null +++ b/app/static/js/script.js.bak @@ -0,0 +1,167 @@ +function cephCalculator() { + return { + replicationType: 'replication', + replicas: 3, + minSize: 2, + k: 4, + m: 2, + storageUnit: 'TB', + nodes: [ + { + osd_count: 4, + osd_size_gb: 1000 + } + ], + result: { + max_usage_percent: 0, + max_usage_gb: 0, + max_usage_tb: 0, + raw_total: 0 + }, + isCalculating: false, + + init() { + const savedConfig = localStorage.getItem('lastConfig'); + if (savedConfig) { + const config = JSON.parse(savedConfig); + Object.assign(this, config); + } + }, + + addNode() { + this.nodes.push({ + osd_count: 4, + osd_size_gb: this.storageUnit === 'TB' ? 1000 : 1 + }); + }, + + removeNode(nodeIndex) { + this.nodes.splice(nodeIndex, 1); + if (this.nodes.length === 0) { + this.addNode(); + } + }, + + getOsdSizeInGB(size) { + return this.storageUnit === 'TB' ? size * 1024 : size; + }, + + calculateCapacity() { + const data = { + replication_type: this.replicationType, + replicas: parseInt(this.replicas), + k: parseInt(this.k), + m: parseInt(this.m), + nodes: this.nodes.map(node => ({ + osd_count: parseInt(node.osd_count), + osd_size_gb: this.getOsdSizeInGB(parseFloat(node.osd_size_gb)) + })), + min_size: parseInt(this.minSize), + storage_unit: this.storageUnit + }; + + this.isCalculating = true; + + fetch('/calculate', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') + }, + body: JSON.stringify(data) + }) + .then(response => response.json()) + .then(data => { + this.result = data; + }) + .catch(error => { + console.error('Fehler bei der Berechnung:', error); + alert('Es ist ein Fehler bei der Berechnung aufgetreten. Bitte versuchen Sie es erneut.'); + }) + .finally(() => { + this.isCalculating = false; + }); + }, + + exportConfig() { + const config = { + replicationType: this.replicationType, + replicas: this.replicas, + minSize: this.minSize, + k: this.k, + m: this.m, + storageUnit: this.storageUnit, + nodes: this.nodes, + result: this.result + }; + + const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'ceph-config.json'; + a.click(); + URL.revokeObjectURL(url); + }, + + importConfig(event) { + const file = event.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + try { + const config = JSON.parse(e.target.result); + Object.assign(this, config); + // Berechne automatisch nach dem Import + this.calculateCapacity(); + } catch (error) { + alert('Fehler beim Importieren der Konfiguration. Bitte überprüfen Sie das Dateiformat.'); + } + }; + reader.readAsText(file); + }, + + generatePDF() { + if (!this.result.raw_total) { + alert('Bitte führen Sie zuerst eine Berechnung durch.'); + return; + } + + const data = { + replication_type: this.replicationType, + replicas: this.replicas, + min_size: this.minSize, + k: this.k, + m: this.m, + storage_unit: this.storageUnit, + nodes: this.nodes, + result: this.result + }; + + fetch('/generate-pdf', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') + }, + body: JSON.stringify(data) + }) + .then(response => response.blob()) + .then(blob => { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'ceph-report.pdf'; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + }) + .catch(error => { + console.error('Fehler bei der PDF-Generierung:', error); + alert('Es ist ein Fehler bei der PDF-Generierung aufgetreten. Bitte versuchen Sie es erneut.'); + }); + } + }; +} \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..02c9f37 --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,14 @@ + + + + + + Ceph Storage Calculator + + + + + + // ... existing code ... + + \ No newline at end of file diff --git a/app/templates/errors/404.html b/app/templates/errors/404.html new file mode 100644 index 0000000..165dca2 --- /dev/null +++ b/app/templates/errors/404.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+

404

+

Seite nicht gefunden

+

Die angeforderte Seite konnte leider nicht gefunden werden.

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

500

+

Interner Serverfehler

+

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

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

Ceph Max Storage Kapazitätsrechner

+

Berechnen Sie die maximale Speichernutzung Ihres Ceph-Clusters

+
+ +
+ +
+

Replikationstyp

+
+ + +
+
+ + +
+

Replikationskonfiguration

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

Erasure Coding Konfiguration

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

Nodes und OSDs

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

Ergebnis

+
+
+
+

Gesamt-Rohspeicher:

+

+ +

+
+
+

Maximale empfohlene Speichernutzung:

+

+

+ +

+
+
+ + +
+

Ausfalltoleranz-Analyse

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

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

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

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

+
+ + +
+

Empfehlungen gemäß Ceph-Dokumentation:

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

Basierend auf Ceph-Dokumentation | Ergebnisse dienen nur als Richtwert

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