From 9388e576be58ffcf2795851da2316df06f2edb25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20M=C3=BCller?= Date: Wed, 26 Mar 2025 11:15:48 +0100 Subject: [PATCH] =?UTF-8?q?Aktualisiere=20README=20und=20Code=20f=C3=BCr?= =?UTF-8?q?=20den=20Ceph=20Max=20Storage=20Rechner:=20=C3=9Cbersetzung=20i?= =?UTF-8?q?ns=20Englische,=20Verbesserung=20der=20Fehlerbehandlung=20in=20?= =?UTF-8?q?den=20Routen,=20Optimierung=20der=20PDF-Generierung=20und=20Anp?= =?UTF-8?q?assung=20der=20Benutzeroberfl=C3=A4che.=20Entferne=20veraltete?= =?UTF-8?q?=20JavaScript-Datei=20und=20aktualisiere=20die=20Struktur=20der?= =?UTF-8?q?=20Templates=20f=C3=BCr=20bessere=20Lesbarkeit=20und=20Benutzer?= =?UTF-8?q?freundlichkeit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 112 +-- app/routes/__pycache__/main.cpython-313.pyc | Bin 4991 -> 6071 bytes app/routes/main.py | 83 ++- app/static/img/tk-logo.png | Bin 0 -> 10461 bytes app/static/js/script.js.bak | 167 ----- app/templates/base.html | 4 +- app/templates/index.html | 642 +++++++++--------- .../ceph_calculator.cpython-313.pyc | Bin 6894 -> 7860 bytes .../__pycache__/pdf_generator.cpython-313.pyc | Bin 10819 -> 13223 bytes app/utils/ceph_calculator.py | 149 ++-- app/utils/pdf_generator.py | 331 ++++++--- 11 files changed, 727 insertions(+), 761 deletions(-) create mode 100644 app/static/img/tk-logo.png delete mode 100644 app/static/js/script.js.bak diff --git a/README.md b/README.md index e7015f3..2883081 100644 --- a/README.md +++ b/README.md @@ -1,96 +1,96 @@ -# Ceph Max Storage Rechner +# Ceph Max Storage Calculator -Eine moderne Web-Anwendung zur Berechnung der maximal zulässigen Speichernutzung in einem Ceph-Cluster. +A modern web application for calculating the maximum allowed storage usage in a Ceph cluster. -## Funktionen +## Features -- 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 -- Dark/Light Mode Unterstützung -- Export/Import von Konfigurationen -- PDF-Report Generierung -- Responsive Design für alle Geräte -- Ausfalltoleranz-Analyse für Nodes und OSDs +- Calculation of maximum storage usage based on Ceph documentation +- Support for Replication and Erasure Coding +- Dynamic addition and removal of nodes and OSDs +- User-friendly interface with modern design +- Dark/Light Mode support +- Export/Import of configurations +- PDF report generation +- Responsive design for all devices +- Fault tolerance analysis for nodes and OSDs ## Installation -1. Repository klonen: +1. Clone repository: ``` git clone cd ceph-calculator ``` -2. Python-Umgebung einrichten: +2. Set up Python environment: ``` python -m venv venv venv\Scripts\activate # Windows source venv/bin/activate # Linux/Mac ``` -3. Abhängigkeiten installieren: +3. Install dependencies: ``` pip install -r requirements.txt ``` -4. Anwendung starten: +4. Start application: ``` python run.py ``` -5. Öffnen Sie in einem Browser: `http://localhost:5000` +5. Open in a browser: `http://localhost:5000` -## Verwendung +## Usage -1. Wählen Sie den Replikationstyp: Replikation oder Erasure Coding -2. Geben Sie die entsprechenden Parameter ein: - - Bei Replikation: Anzahl der Replikate und min_size - - Bei Erasure Coding: k (Datenchunks) und m (Codierungschunks) -3. Fügen Sie Nodes hinzu und konfigurieren Sie deren OSDs mit entsprechenden Speichergrößen -4. Wählen Sie die gewünschte Speichereinheit (GB/TB) -5. Klicken Sie auf "Kapazität berechnen", um das Ergebnis zu sehen -6. Optional: Exportieren Sie die Konfiguration oder generieren Sie einen PDF-Report +1. Select the replication type: Replication or Erasure Coding +2. Enter the corresponding parameters: + - For Replication: Number of replicas and min_size + - For Erasure Coding: k (data chunks) and m (coding chunks) +3. Add nodes and configure their OSDs with appropriate storage sizes +4. Select the desired storage unit (GB/TB) +5. Click on "Calculate Capacity" to see the result +6. Optional: Export the configuration or generate a PDF report -## Technologie +## Technology - Backend: Flask - Frontend: - - Alpine.js für reaktive Benutzeroberfläche - - Tailwind CSS für modernes Design - - HTMX für interaktive Elemente -- Dark/Light Mode mit Tailwind CSS -- PDF-Generierung mit ReportLab -- Responsive Design für alle Geräte + - Alpine.js for reactive user interface + - Tailwind CSS for modern design + - HTMX for interactive elements +- Dark/Light Mode with Tailwind CSS +- PDF generation with ReportLab +- Responsive design for all devices -## Features im Detail +## Detailed Features -### Replikation -- Konfigurierbare Anzahl von Replikaten (1-10) -- Einstellbare min_size für I/O-Operationen -- Automatische Berechnung der Ausfalltoleranz +### Replication +- Configurable number of replicas (1-10) +- Adjustable min_size for I/O operations +- Automatic calculation of fault tolerance ### Erasure Coding -- Konfigurierbare k/m-Werte -- Optimierte Speichernutzung -- Ausfalltoleranz-Analyse +- Configurable k/m values +- Optimized storage usage +- Fault tolerance analysis -### Ausfalltoleranz-Analyse -- Node-Ausfalltoleranz -- OSD-Ausfalltoleranz -- Multi-Node Ausfalltoleranz -- Detaillierte Informationen zur Speichernutzung nach Ausfällen +### Fault Tolerance Analysis +- Node fault tolerance +- OSD fault tolerance +- Multi-node fault tolerance +- Detailed information about storage usage after failures -### Benutzerfreundlichkeit -- Intuitive Benutzeroberfläche +### User-Friendliness +- Intuitive user interface - Dark/Light Mode -- Tooltips mit Erklärungen -- Responsive Design -- Export/Import von Konfigurationen -- PDF-Report Generierung +- Tooltips with explanations +- Responsive design +- Export/Import of configurations +- PDF report generation -## Hinweis +## Note -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 +The calculations are based on general recommendations from the Ceph documentation and +serve as a guideline. For precise capacity planning, you should consult the Ceph documentation +and consider your specific cluster requirements. \ No newline at end of file diff --git a/app/routes/__pycache__/main.cpython-313.pyc b/app/routes/__pycache__/main.cpython-313.pyc index e883777031012d4e248c2d0bbcacd809d20e4cc4..a669a15591bb272755995bd50906d28dd2c20373 100644 GIT binary patch delta 2973 zcmaJ@U2GHC6}~fLJ9gs4b|#LU*u;~>37Cb1LV(0^$mZ8hfGo{mpp8^Bu_wXQ8M}AJ z*~CKfwl59Z2eO($d~@d9bHD3vJO9{Sw_RIX4e<5J?SIYF^}nn8Jx#~VYP342enVfPT2z5q zsEJd2b@&_rf)C*5HnqdVy56IByJUL9(^Sjb57Pt=fWuVzZUmauI+KU0Q>RV41;+uZ z;^UbfItbzx7R0guNL9QuUe%BDQWgMsS6LTa0L+-nSlCMO^*i%rngc;FW2;2Ot5m1S z9ddrR65bvTW9(LpSFhgE10oYLTBnKS`S?Xgbv(g+m3E}W^jMYSKL;?z_7G>&fK zFh{)(iD5y&O5AmimAt|xc#~lin&ZWDJbcz zlu(v);xq5`D+@;~+(KHG)6%?dHDZfNL4M5OZ_up^X(=J63*uuk*?OG_1vwAJL^7Kq z;nYu|VW$>!vy@GVa)H!fXk1F1OG{!(w`JuNj?YVqUPDxZmYAO{@DtLakV&VE@N(UA zS-2o5U0iWYNM0!07xJ>gT@V&U?j;N-NGap#1rnEo*_;TKv?zBK1}doU@uLQ47dM<$ zE^tNxMkdMP4A@zbn}?#Hh>%k*2oiUoOSj8P77E3AF_D+jicUv|Lp0n?KAk>2Twp8U zIROd_B2El*p(@yn{g5m<#M=wNHX_=IXd$8#i$ZWj$RzU_V+}Z5EcsF~#mV_(Qk3Pn zd?vFT<_b0Y@Y3P6BK;pISof7XIZ)_0LmE|1HkO)8 zLlT#l#HE~=RPejYK-pw64lNA)b z8W)onjaTWm(|CBwDkCcp5+c$qne6<$2z66hn#<};MwFlj+toi*wHIr26PeK0T4wZU zXWw)anINNaIwk5BD9ZVaA`{-?UR{vTiEgX3K!gMM*@1uZpYcq70JcnFbClY2G+w!M z`O=E%k%Qf2)l*h;@hI~3YHVNW5MqxZ`>}ue0>AD3buS7Y(R@dd)%(a1*kqfpMp4Hg z^1Z9EL$`3?II@p#S^T!+)TXETY7a?Rn_QN4WogRTIl4jnAOpb zV;{w?@@uU{ts%JK>_GhKm7#~BzV8OS=Eo53CL{6e+N{Mowsa*})+KM*Em72>ns4fGc-Qk;uNR5j%O`KKXo$ zA#u;2a=_lW4QO-srnT;hoxN;Fp2$u9mg~luQVhArHmu`n&emGQlSBN({ev&s*M;ky z>)ulR&4~w*Q}-gLw8%8-=aKvLhBc0>x>|)=2a)Gp%|V96F@E1V{(Q?!Zby?sQQ^ z?=}%fkD2Iw4ACRilda5MOYekKfZ=P(u9;p5|QzVR3 za8($L>@b5LVT9OUGFy4C86MpRh4&LmRuM7<4%2#Nr{QA+)0D9#Ibl&qXN1{|Sa6>j z9p~mn37-(;pyB4QLrlSU2)aYa<CNA#zJ|f$hjJa&r4laAs4MP|f?!}@)SyoI_ ziI=Pe_p9d}?;JTmq7D&32nZe|;s_B(iRi^5#K1m$(`~ubTw-=v5oOq~e(4BAV%T~T z|KuB(7YUiPm&xRTBXG|V&>XEV7{wrpHyB0rJDVJDkcT?z>~!=ZU!TT?OCyLKMD{^- z8Qa3h*RQdWQWCL4$UdZg?)0}rkuRpP6L&H3OdiHpSkWGCQZ_9xUVP5d~Qu7Bz)>65=KjPdN!w+EQEHn3^xiF+s9OaJ(57E{f9_rBUK?NQ{lJT;eNX7%i>Uo&71m>{{{j8-QgHwP delta 1894 zcmaJ?UrZE77~j3UKYQoh?ZMu%C|34>1K|X!g3v~7Ipwewjc`3uOOxvY!?E?4qqBR8 zgnCVrCXgl-I}hTMCVeq|qS~Y+@?awxA6JitWJ9P)6BE*;q0xtiKD09jr?wh5+273k zzHh#n`S$nC{?zg#QQs4I2ybcm6PbY@5VY_7MJxJC0YlkmQNG zTW!>nVOrVE7TSrW#ReVU3Nq`%hP{sU<8^;sPKXDJzR=abZPS}!5^Tbj;1c$PGvP?M z;w5jnAm8tJX61`3Lr3+)&^BW%fO^@ehh3#y_7jqV06ML!(b$BSp1fwzQ&Oy zE%pqeX|~u=iyzfsQr9NHT7hxE%n;Bga$BpOq$6br3~NvW$gsS+kwIz-B(qR6EL&H! z-i3CMNN6X8T@)%QR8deUR1=t}>r>O(5Q&h5DV^>JAY}k(BrsB<0D-{~NGxwF6BV&d zWA0T}i}h^Ob!t$_LV(Vx$N&bJzcoYC4Moikr+|S74cDHM{M7pp{qmOmQ#}KblXf}c zU~w8Wked!YWMf&-NfT{3)0tF?V=BaT^Q`okmJQV{ha@>ImdzNK$1jav^IY%9%T0N)d5VAP zWPE`c`(^$TKP%3v`GAu5hNjpRukbQSe1r1KA=aHtiBt$6U#rRo!c&e_AxIW13ldBD z&ccfRIeT7io$@|);uUXILqMK6`*qIbcm1`pO?txE9L+1d|Dt!=Ys!Zg^xJ!Hot+;s zrLHCZ7@qNka^$c@^S&SV&wY0Dz})Wns)f_bhk70!>d7C9n+HEIrNk27N0Kt@GDGj1 z^6|XTeSgptdVb-1{$6+3NX9nwC`sa%;iCd`y_UewLR2Kpo$3g|MZptm;TE^Icgoz| z5|;Gu%07xU9fBj)%-yS}+`VQ;tmWpU|937&MVplfBw*^!LfX($K%oWDSg1X;kDAN> zYwkoTFaR~3CDwagChFf*m`_Lj2wti zVh;tXe$+^zi9$1lHwn~pXfJ6j_t?;Ia^SoHbhICDh?T*22z!{k`ofl!s~x?#F}NM? zmV{EGmq761`1H70-mol09taUCBm8-=mYVU6VC^UGm=*8l#g5ziO)+MAV~+%J=GgRw zWg+xH2wjUT3r!D%rmbq|HY-l##gpc#xGBa>Z=A?i+I`)g4>nG@9}DGMUY%w|EH8H5 z7frF-^mgMmX`gUp@yLDlPV9EODRnRLC#VKvFKDoBft&BR<(wOzM~n5#(Y}Y#zI^mU zv*WZWeYC{)zfObrSLtD#X327stEIbeg;;zt3Dr@MOcru#*#^ce2g+ng;nRObq52Wc z8=^lMR7#<6L+}SjGeg-lIE>!IcKLkC9=4Z#!ZEgnbsO!je~iD8<A@lRU~;R>_= E1v~gFy#N3J diff --git a/app/routes/main.py b/app/routes/main.py index df465b8..ba0fb21 100644 --- a/app/routes/main.py +++ b/app/routes/main.py @@ -17,30 +17,44 @@ def calculate(): try: data = request.json if not data: - return jsonify({"error": "Keine Daten empfangen"}), 400 + return jsonify({"error": "No data received"}), 400 - replication_type = data.get('replication_type') # 'replication' oder 'erasure_coding' + replication_type = data.get('replication_type') # 'replication' or 'erasure_coding' if not replication_type: - return jsonify({"error": "Replikationstyp fehlt"}), 400 + return jsonify({"error": "Replication type missing"}), 400 - replicas = int(data.get('replicas', 3)) # Standardwert: 3 Replikate + # Basic parameter validation + try: + replicas = int(data.get('replicas', 3)) # Default value: 3 replicas + min_size = int(data.get('min_size', 2)) # Default value: 2 + + # For Erasure Coding + k = int(data.get('k', 0)) if replication_type == 'erasure_coding' else 0 + m = int(data.get('m', 0)) if replication_type == 'erasure_coding' else 0 + + # Nodes and OSDs + nodes = data.get('nodes', []) + if not nodes: + return jsonify({"error": "No nodes defined"}), 400 + + # Validate node data format + for node in nodes: + if 'osd_count' not in node or 'osd_size_gb' not in node: + return jsonify({"error": "Invalid node data format. Each node must have osd_count and osd_size_gb properties."}), 400 + + # Simple validity check + if int(node.get('osd_count', 0)) <= 0 or float(node.get('osd_size_gb', 0)) <= 0: + return jsonify({"error": "Invalid OSD data. Both count and size must be greater than 0."}), 400 + + # Storage unit + storage_unit = data.get('storage_unit', 'GB') + if storage_unit not in ['GB', 'TB']: + storage_unit = 'GB' # Default fallback + + except (ValueError, TypeError) as e: + return jsonify({"error": f"Invalid parameter: {str(e)}"}), 400 - # 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 + # Perform calculation result = calculate_ceph_capacity( replication_type=replication_type, replicas=replicas, @@ -51,39 +65,40 @@ def calculate(): storage_unit=storage_unit ) + logger.info(f"Calculation performed successfully: {replication_type}, replicas={replicas}, nodes={len(nodes)}") return jsonify(result) except ValueError as e: - logger.error(f"Validierungsfehler bei der Berechnung: {str(e)}") + logger.error(f"Validation error during calculation: {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 + logger.error(f"Error during calculation: {str(e)}\n{traceback.format_exc()}") + return jsonify({"error": "An unexpected error occurred"}), 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 + return jsonify({"error": "No data received"}), 400 - # Validiere die Daten + # Validate data if not data.get('replication_type'): - return jsonify({"error": "Replikationstyp fehlt"}), 400 + return jsonify({"error": "Replication type missing"}), 400 if not data.get('nodes') or not isinstance(data['nodes'], list): - return jsonify({"error": "Ungültige Node-Daten"}), 400 + return jsonify({"error": "Invalid node data"}), 400 if not data.get('result') or not isinstance(data['result'], dict): - return jsonify({"error": "Ungültige Ergebnisdaten"}), 400 + return jsonify({"error": "Invalid result data"}), 400 if not data['result'].get('raw_total'): - return jsonify({"error": "Keine Berechnungsergebnisse vorhanden"}), 400 + return jsonify({"error": "No calculation results available"}), 400 - # Generiere PDF + # Generate PDF pdf_bytes = generate_pdf_report(data) if not pdf_bytes: - return jsonify({"error": "PDF-Generierung fehlgeschlagen"}), 500 + return jsonify({"error": "PDF generation failed"}), 500 return send_file( BytesIO(pdf_bytes), @@ -92,8 +107,8 @@ def generate_pdf(): download_name='ceph-report.pdf' ) except ValueError as e: - logger.error(f"Validierungsfehler bei der PDF-Generierung: {str(e)}") + logger.error(f"Validation error during PDF generation: {str(e)}") return jsonify({"error": str(e)}), 400 except Exception as e: - logger.error(f"Fehler bei der PDF-Generierung: {str(e)}\n{traceback.format_exc()}") - return jsonify({"error": "Ein unerwarteter Fehler ist aufgetreten"}), 500 \ No newline at end of file + logger.error(f"Error during PDF generation: {str(e)}\n{traceback.format_exc()}") + return jsonify({"error": "An unexpected error occurred"}), 500 \ No newline at end of file diff --git a/app/static/img/tk-logo.png b/app/static/img/tk-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d4f8d793de8340c8ad256306ee84084095a1d4ce GIT binary patch literal 10461 zcmb_i^^Xx;sTmT0lU0BP9d@1&Jw=BB_k7jaE`xa-=9Bjf_+p1V%^*BLrl0 zN!NFNzJI{?m+#%{b?<&S=e(ci{ha&6dG6as2HKQlY-9icfKumyhA{vDRKWjlAR)%z zb+n;+_y@6#p0)No*&*s+_LzaD8-U(mo2)~Zl~p2P%b7H$nz1O<9bye2qlPAwPsg{N+w)JYOLGAF z75d_ME@1fZN5Y471+NYG(JZ6ElhrrE!l}0)L{+&XAI-J4T{moqvM>B639*AZ*7?!ULCz15M1jtK<*ti%&vZ} za1c4%Efo-ckXQ`SuGf~=OBW$qyHxZeD~@6iJw!W zJ_5$E;h3D_%JI@^fefXowb9YgQA>K)rslLb@_ET~W0ji5gsUqtuX4SD2rxx`-EwV# z!pPuHN4uRTS}VbQr)!pxYvM7ukLFY-SA8kmT~VAaE;%)EhKkeoi;{p-B&TI{;mIi{ zh~W7j>8Z51_=DH;OH}?z0IX)3><{tJo_21AS}S*wM^0$3?ky|@wNPck^~8}$Ja_+8N{snEQ%`|}rWLduDq zH7q5ZhIxz^_&EJ(+YzW zi%8=8`IXeuiz%8a5W6DpSx*=#%uTdViq)B3urGH+FmZieV!sO&_zkpV9-ijUeIdL! zFCmO?kQ&|+t{o$b0kZWGTH6nBll7F};l*&mhR~I^gB0-AnA4Z7bh|enNL4coQ>F+8gXDxt#^6=BVuagMqT}>hZlsf6#YMXo$C^>=lX6Mh_{O^# zd>gKi##pv=+yJM85DW0{ z*-MPqTb%!yE-Q*5~G4oM0v30S}-8>3y$X&4>v4q(6k|yZc)qK8{2l8Ts zuY%C6pN%{2RvGG3ua{=*Vh<&!?@7JDY;zsn#k?zWsIGo)UQ;)b&Rj+7LA?j6vYjI` z#_*G(jhcvKf3zAA>vgo*+U>j}QBe>riGl}P+3I&Nb=C5%XNE&h8VP$^!2EWbE_#ZU zBlhMD@cz)s&!iqMvDE8`3>P-~WMw&iTqAnS`0Z_3=^T2xs`}i{7+X1_oX|>Pfmt z*BCMzZvCRsH~8g`25;{@EEAam5$p*8HaY|9gCEtxQ7C4}w^A!Wm&U01N0%+@Zpymk z>}>ZpAFvRP0se#(Zfiq~HN5ZUD+Ddww5hWXR8vv>#ⅈxM$hJ6(HxihaLal6=4(g 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 N(9tx|s8q8K{U31yO)dZc literal 0 HcmV?d00001 diff --git a/app/static/js/script.js.bak b/app/static/js/script.js.bak deleted file mode 100644 index aca56c4..0000000 --- a/app/static/js/script.js.bak +++ /dev/null @@ -1,167 +0,0 @@ -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 index 02c9f37..081d505 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -1,5 +1,5 @@ - + @@ -9,6 +9,6 @@ - // ... existing code ... + \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html index 3e039bb..eb8f5ff 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1,15 +1,15 @@ - - Ceph Max Storage Rechner - + Ceph Max Storage Calculator + - + - +
- -
- + +
+ Thomas-Krenn.AG Logo
-

Ceph Max Storage Kapazitätsrechner

-

Berechnen Sie die maximale Speichernutzung Ihres Ceph-Clusters

+

Ceph Max Storage Capacity Calculator

+

Calculate the maximum storage usage of your Ceph cluster

-
- -
-

Replikationstyp

-
- - -
-
- - -
-

Replikationskonfiguration

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

Erasure Coding Konfiguration

-
-
- - -
-
- - -
-
-
- - -
-
- - -
-

Nodes und OSDs

+
+ +
+

Configuration

- +
+
- -
- - -
- -
-

Ergebnis

-
-
-
-

Gesamt-Rohspeicher:

-

- -

-
-
-

Maximale empfohlene Speichernutzung:

-

-

- -

-
+ +
+

Results

+ +
+
+

Total Raw Storage:

+

+ +

+
+

Maximum Recommended Usage:

+

+

+ +

+
+
- -
-

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

-
+ +
+

Fault Tolerance

+
+
+

+ Node Fault Tolerance: +

+

- - -
-
- 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.
  • -
+
+

+ OSD Fault Tolerance: +

+

-
-
-
-

Basierend auf Ceph-Dokumentation | Ergebnisse dienen nur als Richtwert

-
+ +
+

Recommendations

+
+
    +
  • Ensure that after node failures, enough capacity remains to avoid reaching the 'full ratio'.
  • +
  • For replication, the number of nodes should be greater than the replication factor.
  • +
  • For erasure coding, the number of nodes should be greater than k+m (data chunks + coding chunks).
  • +
  • Recovery performance depends on network and storage speed.
  • +
  • Note that after a node failure, rebalancing the cluster takes longer the larger the failed node is.
  • +
+
+
+
+ + +
+

Ceph Storage Calculator © 2025 Thomas-Krenn.AG

+
+
@@ -325,38 +264,31 @@ m: 2, storageUnit: 'TB', nodes: [ - { - osd_count: 4, - osd_size_gb: 1 - } + { osd_count: 4, osd_size_gb: 1 }, + { osd_count: 4, osd_size_gb: 1 }, + { osd_count: 4, osd_size_gb: 1 } ], result: { + raw_total: 0, max_usage_percent: 0, max_usage_gb: 0, max_usage_tb: 0, - raw_total: 0 + node_failure_tolerance: false, + node_failure_info: '', + osd_failure_tolerance: false, + osd_failure_info: '', + multi_failure_tolerance: false, + max_failure_nodes: 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: 1 - }); + this.nodes.push({ osd_count: 3, osd_size_gb: 2000 }); }, - removeNode(nodeIndex) { - this.nodes.splice(nodeIndex, 1); - if (this.nodes.length === 0) { - this.addNode(); + removeNode(index) { + if (this.nodes.length > 1) { + this.nodes.splice(index, 1); } }, @@ -365,120 +297,186 @@ }, calculateCapacity() { + this.isCalculating = true; + + // Validate Erasure Coding parameters + if (this.replicationType === 'erasure_coding') { + if (parseInt(this.k) <= 0 || parseInt(this.m) <= 0) { + alert('Für Erasure Coding müssen k und m größer als 0 sein.'); + this.isCalculating = false; + return; + } + if (parseInt(this.k) + parseInt(this.m) <= 0) { + alert('Die Summe von k und m muss größer als 0 sein.'); + this.isCalculating = false; + return; + } + if (this.nodes.length <= parseInt(this.k)) { + alert('Die Anzahl der Nodes muss größer als k sein.'); + this.isCalculating = false; + return; + } + } + + // Validate OSD sizes + for (let node of this.nodes) { + if (parseFloat(node.osd_size_gb) <= 0) { + alert('Die OSD-Größe muss größer als 0 sein.'); + this.isCalculating = false; + return; + } + if (parseInt(node.osd_count) <= 0) { + alert('Die Anzahl der OSDs muss größer als 0 sein.'); + this.isCalculating = false; + return; + } + } + + // Get CSRF token + const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); + + // Prepare data for sending const data = { replication_type: this.replicationType, replicas: parseInt(this.replicas), + min_size: parseInt(this.minSize), 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; - + // Send request to server fetch('/calculate', { method: 'POST', headers: { 'Content-Type': 'application/json', - 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') + 'X-CSRFToken': csrfToken }, body: JSON.stringify(data) }) - .then(response => response.json()) - .then(data => { - this.result = data; + .then(response => { + if (!response.ok) { + throw new Error('Calculation failed'); + } + return response.json(); + }) + .then(result => { + this.result = result; + this.isCalculating = false; }) .catch(error => { - console.error('Fehler bei der Berechnung:', error); - alert('Es ist ein Fehler bei der Berechnung aufgetreten. Bitte versuchen Sie es erneut.'); - }) - .finally(() => { + console.error('Error:', error); this.isCalculating = false; + alert('Error during calculation: ' + error.message); }); }, - + 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, + storage_unit: this.storageUnit, result: this.result }; - + + const dataStr = JSON.stringify(data, null, 2); + const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr); + + const exportFileDefaultName = 'ceph-config.json'; + + const linkElement = document.createElement('a'); + linkElement.setAttribute('href', dataUri); + linkElement.setAttribute('download', exportFileDefaultName); + linkElement.click(); + }, + + 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); + + // Update local state with imported configuration + this.replicationType = config.replication_type || 'replication'; + this.replicas = config.replicas || 3; + this.minSize = config.min_size || 2; + this.k = config.k || 4; + this.m = config.m || 2; + this.nodes = config.nodes || [{ osd_count: 3, osd_size_gb: 2000 }]; + this.storageUnit = config.storage_unit || 'TB'; + + // If results are included, update those too + if (config.result && config.result.raw_total) { + this.result = config.result; + } + + alert('Configuration successfully imported!'); + } catch (error) { + console.error('Error importing configuration:', error); + alert('Error importing configuration. Please check the file format.'); + } + }; + reader.readAsText(file); + }, + + generatePDF() { + // Get CSRF token + const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); + + // Prepare data for sending + const data = { + replication_type: this.replicationType, + replicas: parseInt(this.replicas), + min_size: parseInt(this.minSize), + 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)) + })), + storage_unit: this.storageUnit, + result: this.result + }; + + // Send request to server fetch('/generate-pdf', { method: 'POST', headers: { 'Content-Type': 'application/json', - 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') + 'X-CSRFToken': csrfToken }, body: JSON.stringify(data) }) - .then(response => response.blob()) + .then(response => { + if (!response.ok) { + throw new Error('PDF generation failed'); + } + return response.blob(); + }) .then(blob => { const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); + a.style.display = 'none'; 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.'); + console.error('Error:', error); + alert('Error generating PDF: ' + error.message); }); } }; diff --git a/app/utils/__pycache__/ceph_calculator.cpython-313.pyc b/app/utils/__pycache__/ceph_calculator.cpython-313.pyc index eb68309024897a3369c4ad75cc7415b4ab08993a..f2ab2d8687bfd45f58e93afab6dcd85cab1105f5 100644 GIT binary patch literal 7860 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 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& diff --git a/app/utils/ceph_calculator.py b/app/utils/ceph_calculator.py index 07c53c6..7e117fc 100644 --- a/app/utils/ceph_calculator.py +++ b/app/utils/ceph_calculator.py @@ -1,19 +1,24 @@ +import logging + +logger = logging.getLogger(__name__) + 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') + Calculate the maximum allowed storage usage for a Ceph cluster considering node failures. + Args: + replication_type (str): Either 'replication' or 'erasure_coding' + replicas (int): Number of replicas for replication pools (default: 3) + k (int): Number of data chunks for EC + m (int): Number of coding chunks for EC + nodes (list): List of dictionaries with 'osd_count' and 'osd_size_gb' keys + [{'osd_count': 4, 'osd_size_gb': 1000}, ...] + min_size (int): Minimum number of replicas for I/O operations (default: 2) + storage_unit (str): Storage unit, either 'GB' or 'TB' (default: 'GB') + Returns: - - Dictionary mit max_usage_percent, max_usage_gb, max_usage_tb, raw_total und zusätzlichen Informationen zur Ausfallsicherheit + dict: Dictionary with max_usage_percent, max_usage_gb, max_usage_tb, raw_total and additional + information about fault tolerance """ if nodes is None or len(nodes) == 0: return { @@ -22,14 +27,11 @@ def calculate_ceph_capacity(replication_type, replicas=3, k=0, m=0, nodes=None, 'max_usage_tb': 0, 'raw_total': 0, 'node_failure_tolerance': False, - 'node_failure_info': 'Keine Nodes im Cluster', + 'node_failure_info': 'No nodes in the 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 + # Calculate total raw storage and information for each node raw_total_gb = 0 node_capacities = [] @@ -40,80 +42,89 @@ def calculate_ceph_capacity(replication_type, replicas=3, k=0, m=0, nodes=None, node_capacities.append(node_capacity) raw_total_gb += node_capacity - # Größter Node (worst-case Szenario bei Ausfall) + # Largest node (worst-case scenario during failure) largest_node_capacity = max(node_capacities) if node_capacities else 0 - # Berechne die nutzbare Kapazität ohne Ausfall + # Calculate usable capacity without failure if replication_type == 'replication': - # Bei Replikation ist der nutzbare Speicher = Rohspeicher / Anzahl der Replikate + # For replication, usable storage = raw storage / number of replicas usable_capacity_gb = raw_total_gb / replicas else: # Erasure Coding - # Bei EC ist der nutzbare Speicher = Rohspeicher * (k / (k + m)) + # For EC, usable storage = raw storage * (k / (k + m)) + if k <= 0 or m <= 0 or (k + m) <= 0: + raise ValueError("Invalid Erasure Coding parameters: k and m must be positive and their sum must be greater than 0") usable_capacity_gb = raw_total_gb * (k / (k + m)) - # Die empfohlene maximale Auslastung für den Normalfall (ohne Ausfall) + # Recommended maximum utilization for normal case (without failure) 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 + # Calculate OSD utilization using the formula x = (s × p) / (s + 1) + # where s = number of OSDs per server and p = percentage of total utilization + osds_per_server = int(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) + # Find largest OSD size for calculating capacity after OSD failure + largest_osd_size = max((float(node.get('osd_size_gb', 0)) for node in nodes), default=0) - # Berechne die nutzbare Kapazität nach OSD-Ausfall + # Calculate usable capacity after OSD failure 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 + # Calculate maximum safe usage considering OSD failure 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 + usable_capacity_gb * (osd_usage_percent / 100), # OSD utilization based on formula + usable_after_osd_failure * 0.8 # 80% of capacity after OSD failure ) - max_usage_tb = max_usage_gb / 1024 - # Berechnung der Ausfalltoleranz unter Berücksichtigung von min_size + # Convert to TB if storage_unit is TB + if storage_unit == 'TB': + max_usage_tb = max_usage_gb / 1024 + else: + max_usage_tb = max_usage_gb / 1024 # Always calculate TB for display + + # Calculate fault tolerance considering 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 + len(nodes) - min_size, # Maximum failures based on min_size + replicas - min_size # Maximum failures based on replication factor ) 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 + len(nodes) - (k + 1), # At least k+1 nodes must remain available + m # Maximum number of coding chunks that can fail ) - # Sortiere die Nodes nach Größe absteigend für Worst-Case-Analyse + # Sort nodes by size in descending order for worst-case analysis node_capacities_sorted = sorted(node_capacities, reverse=True) - # Kapazität nach Ausfall der größten N Nodes + # Capacity after failure of the largest 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 + # Usable capacity after maximum tolerable failures 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 + if remaining_m <= 0: + raise ValueError("Invalid Erasure Coding configuration: remaining coding chunks must be positive") 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 + # Calculate usable capacity after failure of largest node 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 + + # Check if there is enough storage space after a node failure node_failure_tolerance = True - # Prüfe die Mindestanforderungen für Nodes + # Check minimum requirements for nodes if replication_type == 'replication': if len(nodes) < min_size: node_failure_tolerance = False @@ -125,7 +136,7 @@ def calculate_ceph_capacity(replication_type, replicas=3, k=0, m=0, nodes=None, elif usable_after_failure_gb < max_usage_gb: node_failure_tolerance = False - # Für multiple Ausfälle prüfen + # Check for multiple failures multi_failure_tolerance = False if max_failure_nodes > 0: multi_failure_tolerance = ( @@ -133,7 +144,7 @@ def calculate_ceph_capacity(replication_type, replicas=3, k=0, m=0, nodes=None, len(nodes) > max_failure_nodes ) - # Maximale sichere Nutzung unter Berücksichtigung eines möglichen Nodeausfalls + # Maximum safe usage considering a possible node failure safe_usage_percent = 0 safe_usage_gb = 0 safe_usage_tb = 0 @@ -145,57 +156,57 @@ def calculate_ceph_capacity(replication_type, replicas=3, k=0, m=0, nodes=None, 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})." + node_failure_info = f"The cluster can tolerate failure of up to {max_failure_nodes} nodes (min_size={min_size})." else: - node_failure_info = f"Der Cluster kann einen Ausfall des größten Nodes tolerieren (min_size={min_size})." + node_failure_info = f"The cluster can tolerate failure of the largest node (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." + node_failure_info = f"CRITICAL: Too few nodes ({len(nodes)}) for the configured min_size={min_size}. " + node_failure_info += f"At least {min_size + 1 if replication_type == 'replication' else k + m + 1 - min(m, 1)} nodes needed." else: - # Unit für die Anzeige + # Unit for display 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}%") + node_failure_info = (f"WARNING: The cluster does not have enough free capacity to tolerate a failure of the largest node " + f"({node_size_display} {unit_display}). " + f"Maximum safe usage: {safe_usage_percent}%") - # Berechnung für den Ausfall eines einzelnen OSD + # Calculate for single OSD failure osd_failure_tolerance = False - osd_failure_info = "Keine OSDs im Cluster" + osd_failure_info = "No OSDs in the cluster" - if nodes and any(node.get('osd_count', 0) > 0 for node in nodes): + if nodes and any(int(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 for display 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." + osd_failure_info = f"The cluster can tolerate failure of the largest OSD (min_size={min_size})." if osd_failure_tolerance else \ + f"WARNING: The cluster does not have enough free capacity to tolerate failure of the largest OSD ({osd_size_display} {unit_display})." - # Rückgabewerte + # Return values with proper unit conversion 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), + 'max_usage_gb': round(max_usage_gb, 2), + 'max_usage_tb': round(max_usage_tb, 2), + 'raw_total': round(raw_total_gb / 1024, 2) if storage_unit == 'TB' else 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), + 'largest_node_gb': round(largest_node_capacity / 1024, 2) if storage_unit == 'TB' else round(largest_node_capacity, 2), + 'raw_after_failure_gb': round(raw_after_failure_gb / 1024, 2) if storage_unit == 'TB' else round(raw_after_failure_gb, 2), + 'usable_after_failure_gb': round(usable_after_failure_gb / 1024, 2) if storage_unit == 'TB' else round(usable_after_failure_gb, 2), + 'raw_after_max_failures_gb': round(raw_after_max_failures_gb / 1024, 2) if storage_unit == 'TB' else round(raw_after_max_failures_gb, 2), + 'usable_after_max_failures_gb': round(usable_after_max_failures_gb / 1024, 2) if storage_unit == 'TB' else round(usable_after_max_failures_gb, 2), 'min_size': min_size, 'osds_per_server': osds_per_server, 'storage_unit': storage_unit diff --git a/app/utils/pdf_generator.py b/app/utils/pdf_generator.py index 0bdcc8e..728d0d8 100644 --- a/app/utils/pdf_generator.py +++ b/app/utils/pdf_generator.py @@ -1,68 +1,78 @@ from reportlab.lib import colors from reportlab.lib.pagesizes import A4 -from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle +from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.units import inch from datetime import datetime import logging +import os +from reportlab.pdfgen import canvas logger = logging.getLogger(__name__) +# Thomas-Krenn Farbschema +TK_ORANGE = colors.HexColor('#F7941D') +TK_DARK_GRAY = colors.HexColor('#333333') +TK_LIGHT_GRAY = colors.HexColor('#F5F5F5') +TK_WHITE = colors.white +TK_GREEN = colors.HexColor('#059669') +TK_RED = colors.HexColor('#dc2626') + def validate_data(data): - """Überprüft, ob alle erforderlichen Daten vorhanden sind.""" + """Checks if all required data is present.""" required_fields = ['replication_type', 'nodes', 'result'] for field in required_fields: if field not in data: - raise ValueError(f"Fehlendes Feld: {field}") + raise ValueError(f"Missing field: {field}") if not data['nodes']: - raise ValueError("Keine Nodes definiert") + raise ValueError("No nodes defined") if not data['result'].get('raw_total'): - raise ValueError("Keine Berechnungsergebnisse vorhanden") + raise ValueError("No calculation results available") def safe_int(value, default=0): - """Konvertiert einen Wert sicher in einen Integer.""" + """Safely converts a value to an 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}") + logger.warning(f"Could not convert value '{value}' to integer. Using default value {default}") return default def safe_float(value, default=0.0): - """Konvertiert einen Wert sicher in einen Float.""" + """Safely converts a value to a 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}") + logger.warning(f"Could not convert value '{value}' to float. Using default value {default}") return default def generate_pdf_report(data): """ - Generiert einen PDF-Report mit den Ceph-Konfigurationsdaten und Ergebnissen. + Generates a PDF report with Ceph configuration data and results. Args: - data (dict): Dictionary mit Konfigurations- und Ergebnisdaten + data (dict): Dictionary with configuration and result data Returns: - bytes: PDF-Datei als Bytes + bytes: PDF file as bytes Raises: - ValueError: Wenn erforderliche Daten fehlen + ValueError: If required data is missing """ try: - # Validiere Eingabedaten + # Validate input data validate_data(data) - # Erstelle einen temporären Buffer für die PDF + # Create a temporary buffer for the PDF from io import BytesIO buffer = BytesIO() - # Erstelle das PDF-Dokument + # Create the PDF document doc = SimpleDocTemplate( buffer, pagesize=A4, @@ -70,148 +80,247 @@ def generate_pdf_report(data): leftMargin=72, topMargin=72, bottomMargin=72, - title="Ceph Storage Kapazitätsreport" + title="Ceph Storage Capacity Report" ) - # Styles definieren + # Define styles styles = getSampleStyleSheet() - title_style = styles['Heading1'] - heading_style = styles['Heading2'] - normal_style = styles['Normal'] - # Custom Style für Warnungen + # Custom styles mit weißer Textfarbe aufgrund des grauen Hintergrunds + title_style = ParagraphStyle( + 'CustomTitle', + parent=styles['Heading1'], + fontSize=24, + spaceAfter=30, + textColor=TK_WHITE + ) + + heading_style = ParagraphStyle( + 'CustomHeading', + parent=styles['Heading2'], + fontSize=16, + spaceAfter=12, + textColor=TK_ORANGE + ) + + normal_style = ParagraphStyle( + 'CustomNormal', + parent=styles['Normal'], + fontSize=10, + spaceAfter=6, + textColor=TK_WHITE + ) + + # Custom Style for warnings warning_style = ParagraphStyle( 'Warning', parent=styles['Normal'], - textColor=colors.red, - fontSize=10 + textColor=TK_RED, + fontSize=10, + spaceAfter=6 ) - # Story (Inhalt) erstellen + # Create story (content) 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)) + # Hintergrundfarbe und Canvas-Anpassung + # Definiere die Hintergrundfarbe + TK_BACKGROUND_GRAY = colors.HexColor('#1f2937') # Dunkelgrau wie in der Website (dark-bg) + + # Funktion zum Zeichnen des Hintergrunds + def add_page_background(canvas, doc): + canvas.saveState() + canvas.setFillColor(TK_BACKGROUND_GRAY) + canvas.rect(0, 0, doc.pagesize[0], doc.pagesize[1], fill=True, stroke=False) + canvas.restoreState() + + # Title with modern styling + story.append(Paragraph("Ceph Storage Capacity Report", title_style)) + story.append(Paragraph(f"Generated on: {datetime.now().strftime('%d.%m.%Y %H:%M')}", normal_style)) story.append(Spacer(1, 20)) - # Konfigurationsübersicht - story.append(Paragraph("Konfigurationsübersicht", heading_style)) + # Configuration overview with modern styling + story.append(Paragraph("Configuration Overview", heading_style)) story.append(Spacer(1, 12)) - # Replikationstyp + # Replication type with modern table style config_data = [ - ["Replikationstyp", data['replication_type']], - ["Speichereinheit", data.get('storage_unit', 'GB')] + ["Replication Type", data['replication_type']], + ["Storage Unit", data.get('storage_unit', 'GB')] ] + # Add replication-specific parameters 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)))] + ["Number of Replicas", str(data.get('replicas', 3))], + ["Minimum Size", str(data.get('min_size', 2))] ]) - else: + else: # Erasure Coding config_data.extend([ - ["Datenchunks (k)", str(safe_int(data.get('k', 4)))], - ["Codierungschunks (m)", str(safe_int(data.get('m', 2)))] + ["Data Chunks (k)", str(data.get('k', 4))], + ["Coding Chunks (m)", str(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]) + # Modern table style for configurations + t = Table(config_data, colWidths=[3*inch, 3*inch]) t.setStyle(TableStyle([ - ('BACKGROUND', (0, 0), (-1, -1), colors.white), - ('TEXTCOLOR', (0, 0), (-1, -1), colors.black), + ('BACKGROUND', (0, 0), (-1, 0), TK_ORANGE), + ('TEXTCOLOR', (0, 0), (-1, 0), TK_WHITE), ('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) + ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), + ('FONTSIZE', (0, 0), (-1, 0), 12), + ('BOTTOMPADDING', (0, 0), (-1, 0), 12), + ('BACKGROUND', (0, 1), (-1, -1), TK_DARK_GRAY), + ('TEXTCOLOR', (0, 1), (-1, -1), TK_WHITE), + ('FONTNAME', (0, 1), (-1, -1), 'Helvetica'), + ('FONTSIZE', (0, 1), (-1, -1), 10), + ('ALIGN', (0, 1), (-1, -1), 'LEFT'), + ('GRID', (0, 0), (-1, -1), 1, TK_LIGHT_GRAY) ])) story.append(t) - # Ausfalltoleranz + # Add nodes information with modern styling story.append(Spacer(1, 20)) - story.append(Paragraph("Ausfalltoleranz-Analyse", heading_style)) + story.append(Paragraph("Nodes Configuration", 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)) + nodes_data = [["Node", "OSD Count", f"OSD Size ({data.get('storage_unit', 'GB')})"]] + for i, node in enumerate(data.get('nodes', []), 1): + osd_size = safe_float(node.get('osd_size_gb', 0)) + if data.get('storage_unit') == 'TB': + osd_size = osd_size / 1024 # Convert GB to TB for display + nodes_data.append([ + f"Node {i}", + str(node.get('osd_count', 0)), + f"{osd_size:.2f}" + ]) + + # Nodes table + t = Table(nodes_data, colWidths=[2*inch, 2*inch, 2*inch]) + t.setStyle(TableStyle([ + ('BACKGROUND', (0, 0), (-1, 0), TK_ORANGE), + ('TEXTCOLOR', (0, 0), (-1, 0), TK_WHITE), + ('ALIGN', (0, 0), (-1, -1), 'CENTER'), + ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), + ('FONTSIZE', (0, 0), (-1, 0), 12), + ('BOTTOMPADDING', (0, 0), (-1, 0), 12), + ('BACKGROUND', (0, 1), (-1, -1), TK_DARK_GRAY), + ('TEXTCOLOR', (0, 1), (-1, -1), TK_WHITE), + ('FONTNAME', (0, 1), (-1, -1), 'Helvetica'), + ('FONTSIZE', (0, 1), (-1, -1), 10), + ('ALIGN', (0, 1), (-1, -1), 'CENTER'), + ('GRID', (0, 0), (-1, -1), 1, TK_LIGHT_GRAY) + ])) + story.append(t) + + # Results with modern styling + story.append(Spacer(1, 20)) + story.append(Paragraph("Results", heading_style)) + story.append(Spacer(1, 12)) + + result_data = [ + ["Total Raw Storage", f"{safe_float(data['result']['raw_total'])} {data.get('storage_unit', 'GB')}"], + ["Max. Recommended Usage", f"{safe_float(data['result'].get('max_usage_percent', 0))}%"], + ["Max. Usable Storage", f"{safe_float(data['result'].get('max_usage_tb' if data.get('storage_unit') == 'TB' else 'max_usage_gb', 0))} {data.get('storage_unit', 'GB')}"] + ] + + # Results table + t = Table(result_data, colWidths=[3*inch, 3*inch]) + t.setStyle(TableStyle([ + ('BACKGROUND', (0, 0), (-1, 0), TK_ORANGE), + ('TEXTCOLOR', (0, 0), (-1, 0), TK_WHITE), + ('ALIGN', (0, 0), (-1, -1), 'LEFT'), + ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), + ('FONTSIZE', (0, 0), (-1, 0), 12), + ('BOTTOMPADDING', (0, 0), (-1, 0), 12), + ('BACKGROUND', (0, 1), (-1, -1), TK_DARK_GRAY), + ('TEXTCOLOR', (0, 1), (-1, -1), TK_WHITE), + ('FONTNAME', (0, 1), (-1, -1), 'Helvetica'), + ('FONTSIZE', (0, 1), (-1, -1), 10), + ('ALIGN', (0, 1), (-1, -1), 'LEFT'), + ('GRID', (0, 0), (-1, -1), 1, TK_LIGHT_GRAY) + ])) + story.append(t) + + # Fault tolerance with modern styling + story.append(Spacer(1, 20)) + story.append(Paragraph("Fault Tolerance Analysis", heading_style)) + story.append(Spacer(1, 12)) + + # Node fault tolerance with status indicator + node_tolerance = "Available" if data['result'].get('node_failure_tolerance', False) else "Not Available" + status_color = TK_GREEN if node_tolerance == "Available" else TK_RED + status_style = ParagraphStyle( + 'Status', + parent=normal_style, + textColor=status_color, + fontSize=12, + spaceAfter=6 + ) + story.append(Paragraph(f"Node Fault Tolerance: {node_tolerance}", status_style)) + story.append(Paragraph(data['result'].get('node_failure_info', 'No information available'), 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)) + story.append(Paragraph(f"Multi-Node Fault Tolerance: {safe_int(data['result'].get('max_failure_nodes', 0))} Nodes", normal_style)) - # OSD-Ausfalltoleranz + # OSD fault tolerance with status indicator 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)) + osd_tolerance = "Available" if data['result'].get('osd_failure_tolerance', False) else "Limited" + status_color = TK_GREEN if osd_tolerance == "Available" else TK_RED + status_style = ParagraphStyle( + 'Status', + parent=normal_style, + textColor=status_color, + fontSize=12, + spaceAfter=6 + ) + story.append(Paragraph(f"OSD Fault Tolerance: {osd_tolerance}", status_style)) + story.append(Paragraph(data['result'].get('osd_failure_info', 'No information available'), normal_style)) - # Empfehlungen + # Recommendations mit besserem Kontrast für den dunklen Hintergrund story.append(Spacer(1, 20)) - story.append(Paragraph("Empfehlungen", heading_style)) + story.append(Paragraph("Recommendations", 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." + "Ensure that after node failures, enough capacity is available to avoid reaching the 'full ratio'.", + "For replication, the number of nodes should be greater than the replication factor.", + "For erasure coding, the number of nodes should be greater than k+m (data chunks + coding chunks).", + "Recovery performance depends on network and storage speed.", + "Note that after a node failure, rebalancing the cluster takes longer the larger the failed node is." ] + # Erstelle Box für Empfehlungen mit besserer Lesbarkeit + recommendations_text = [] for rec in recommendations: - story.append(Paragraph(f"• {rec}", normal_style)) + recommendations_text.append(Paragraph(f"• {rec}", normal_style)) - # PDF generieren - doc.build(story) + recommendation_table = Table([[item] for item in recommendations_text], colWidths=[6*inch]) + recommendation_table.setStyle(TableStyle([ + ('BACKGROUND', (0, 0), (-1, -1), TK_DARK_GRAY), + ('TEXTCOLOR', (0, 0), (-1, -1), TK_WHITE), + ('ALIGN', (0, 0), (-1, -1), 'LEFT'), + ('VALIGN', (0, 0), (-1, -1), 'TOP'), + ('GRID', (0, 0), (-1, -1), 1, TK_LIGHT_GRAY) + ])) + story.append(recommendation_table) - # PDF-Bytes zurückgeben + # Footer with Thomas-Krenn.AG branding + story.append(Spacer(1, 30)) + footer_style = ParagraphStyle( + 'Footer', + parent=normal_style, + fontSize=8, + textColor=colors.HexColor('#9ca3af') # Helleres Grau für bessere Lesbarkeit auf dunklem Hintergrund + ) + story.append(Paragraph("Generated by Thomas-Krenn.AG Ceph Storage Calculator", footer_style)) + + # Generate PDF with background + doc.build(story, onFirstPage=add_page_background, onLaterPages=add_page_background) + + # Return PDF bytes return buffer.getvalue() except Exception as e: - logger.error(f"Fehler bei der PDF-Generierung: {str(e)}") + logger.error(f"Error during PDF generation: {str(e)}") raise \ No newline at end of file