5 Commits

11 changed files with 274 additions and 2742 deletions

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

2
.gitignore vendored
View File

@ -1,2 +0,0 @@
node_modules/
.env

202
LICENSE-2.0.txt Normal file
View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -22,14 +22,21 @@ A modern web application for calculating the maximum allowed storage usage in a
cd ceph-calculator
```
2. Install Node.js dependencies:
2. Set up Python environment:
```
npm install
python -m venv venv
venv\Scripts\activate # Windows
source venv/bin/activate # Linux/Mac
```
3. Start application:
3. Install dependencies:
```
npm start
pip install -r requirements.txt
```
4. Start application:
```
python run.py
```
5. Open in a browser: `http://localhost:5000`
@ -47,13 +54,13 @@ A modern web application for calculating the maximum allowed storage usage in a
## Technology
- Backend: Node.js with Express
- Frontend:
- Backend: Flask
- Frontend:
- Alpine.js for reactive user interface
- Tailwind CSS for modern design
- HTMX for interactive elements
- Dark/Light Mode with Tailwind CSS
- PDF generation with pdfkit
- PDF generation with ReportLab
- Responsive design for all devices
## Detailed Features

1950
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +0,0 @@
{
"name": "ceph-calculator-node",
"version": "1.0.0",
"description": "Node.js version of Ceph calculator",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.2",
"cookie-parser": "^1.4.6",
"csurf": "^1.11.0",
"body-parser": "^1.20.2",
"pdfkit": "^0.13.0",
"ejs": "^3.1.9"
}
}

258
server.js
View File

@ -1,258 +0,0 @@
const express = require('express');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const csrf = require('csurf');
const path = require('path');
const PDFDocument = require('pdfkit');
const fs = require('fs');
const app = express();
const port = process.env.PORT || 5000;
// CSRF protection using cookies
const csrfProtection = csrf({ cookie: true });
app.use(cookieParser());
app.use(bodyParser.json());
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.use('/static', express.static(path.join(__dirname, 'app', 'static')));
// Ceph calculation logic translated from Python
function calculateCephCapacity({
replication_type,
replicas = 3,
k = 0,
m = 0,
nodes = [],
min_size = 2,
storage_unit = 'GB'
}) {
if (!nodes || nodes.length === 0) {
return {
max_usage_percent: 0,
max_usage_gb: 0,
max_usage_tb: 0,
raw_total: 0,
node_failure_tolerance: false,
node_failure_info: 'No nodes in the cluster',
storage_unit
};
}
let raw_total_gb = 0;
const node_capacities = [];
nodes.forEach(n => {
const osdCount = parseInt(n.osd_count) || 0;
const osdSize = parseFloat(n.osd_size_gb) || 0;
const nodeCap = osdCount * osdSize;
node_capacities.push(nodeCap);
raw_total_gb += nodeCap;
});
const largest_node_capacity = Math.max(...node_capacities);
let usable_capacity_gb = 0;
if (replication_type === 'replication') {
usable_capacity_gb = raw_total_gb / replicas;
} else {
if (k <= 0 || m <= 0 || k + m <= 0) {
throw new Error('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));
}
const max_recommended_usage_percent = 80;
const osds_per_server = nodes.reduce((max, n) => Math.max(max, parseInt(n.osd_count) || 0), 0);
const osd_usage_percent = (osds_per_server * max_recommended_usage_percent) / (osds_per_server + 1);
const largest_osd_size = nodes.reduce((max, n) => Math.max(max, parseFloat(n.osd_size_gb) || 0), 0);
const raw_after_osd_failure = raw_total_gb - largest_osd_size;
let usable_after_osd_failure = 0;
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));
}
let max_usage_gb = Math.min(
usable_capacity_gb * (osd_usage_percent / 100),
usable_after_osd_failure * 0.8
);
let max_usage_tb = max_usage_gb / 1024;
let max_failure_nodes = 0;
if (replication_type === 'replication') {
max_failure_nodes = Math.min(nodes.length - min_size, replicas - min_size);
} else {
max_failure_nodes = Math.min(nodes.length - (k + 1), m);
}
const node_capacities_sorted = [...node_capacities].sort((a,b) => b-a);
let raw_after_max_failures_gb = raw_total_gb;
for (let i = 0; i < Math.min(max_failure_nodes, node_capacities_sorted.length); i++) {
raw_after_max_failures_gb -= node_capacities_sorted[i];
}
let usable_after_max_failures_gb = 0;
if (replication_type === 'replication') {
usable_after_max_failures_gb = raw_after_max_failures_gb / min_size;
} else {
const remaining_m = m - max_failure_nodes;
if (remaining_m <= 0) {
throw new Error('Invalid Erasure Coding configuration: remaining coding chunks must be positive');
}
usable_after_max_failures_gb = raw_after_max_failures_gb * (k / (k + remaining_m));
}
const raw_after_failure_gb = raw_total_gb - largest_node_capacity;
let usable_after_failure_gb = 0;
if (replication_type === 'replication') {
usable_after_failure_gb = raw_after_failure_gb / min_size;
} else {
usable_after_failure_gb = raw_after_failure_gb * (k / (k + m));
}
let node_failure_tolerance = true;
if (replication_type === 'replication') {
if (nodes.length < min_size) {
node_failure_tolerance = false;
} else if (usable_after_failure_gb < max_usage_gb) {
node_failure_tolerance = false;
}
} else {
if (nodes.length <= k) {
node_failure_tolerance = false;
} else if (usable_after_failure_gb < max_usage_gb) {
node_failure_tolerance = false;
}
}
let multi_failure_tolerance = false;
if (max_failure_nodes > 0) {
multi_failure_tolerance = (usable_after_max_failures_gb >= max_usage_gb && nodes.length > max_failure_nodes);
}
let safe_usage_percent = 0;
let safe_usage_gb = 0;
let safe_usage_tb = 0;
let 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 && max_failure_nodes > 1) {
node_failure_info = `The cluster can tolerate failure of up to ${max_failure_nodes} nodes (min_size=${min_size}).`;
} else {
node_failure_info = `The cluster can tolerate failure of the largest node (min_size=${min_size}).`;
}
} else {
safe_usage_percent = Math.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 (nodes.length <= (replication_type === 'replication' ? min_size : k + m - Math.min(m,1))) {
node_failure_info = `CRITICAL: Too few nodes (${nodes.length}) for the configured min_size=${min_size}. `;
node_failure_info += `At least ${replication_type === 'replication' ? min_size + 1 : k + m + 1 - Math.min(m,1)} nodes needed.`;
} else {
const unit_display = storage_unit === 'TB' ? 'TB' : 'GB';
const node_size_display = storage_unit === 'TB' ? (largest_node_capacity / 1024).toFixed(2) : largest_node_capacity.toFixed(2);
node_failure_info = `WARNING: The cluster does not have enough free capacity to tolerate a failure of the largest node (${node_size_display} ${unit_display}). Maximum safe usage: ${safe_usage_percent}%`;
}
}
let osd_failure_tolerance = false;
let osd_failure_info = 'No OSDs in the cluster';
if (nodes.some(n => parseInt(n.osd_count) > 0)) {
osd_failure_tolerance = usable_after_osd_failure >= max_usage_gb;
const unit_display = storage_unit === 'TB' ? 'TB' : 'GB';
const osd_size_display = storage_unit === 'TB' ? (largest_osd_size / 1024).toFixed(2) : largest_osd_size.toFixed(2);
osd_failure_info = osd_failure_tolerance
? `The cluster can tolerate failure of the largest OSD (min_size=${min_size}).`
: `WARNING: The cluster does not have enough free capacity to tolerate failure of the largest OSD (${osd_size_display} ${unit_display}).`;
}
return {
max_usage_percent: parseFloat(osd_usage_percent.toFixed(2)),
max_usage_gb: parseFloat(max_usage_gb.toFixed(2)),
max_usage_tb: parseFloat(max_usage_tb.toFixed(2)),
raw_total: parseFloat((storage_unit === 'TB' ? raw_total_gb / 1024 : raw_total_gb).toFixed(2)),
node_failure_tolerance,
node_failure_info,
multi_failure_tolerance,
max_failure_nodes,
osd_failure_tolerance,
osd_failure_info,
largest_node_gb: parseFloat((storage_unit === 'TB' ? largest_node_capacity / 1024 : largest_node_capacity).toFixed(2)),
raw_after_failure_gb: parseFloat((storage_unit === 'TB' ? raw_after_failure_gb / 1024 : raw_after_failure_gb).toFixed(2)),
usable_after_failure_gb: parseFloat((storage_unit === 'TB' ? usable_after_failure_gb / 1024 : usable_after_failure_gb).toFixed(2)),
raw_after_max_failures_gb: parseFloat((storage_unit === 'TB' ? raw_after_max_failures_gb / 1024 : raw_after_max_failures_gb).toFixed(2)),
usable_after_max_failures_gb: parseFloat((storage_unit === 'TB' ? usable_after_max_failures_gb / 1024 : usable_after_max_failures_gb).toFixed(2)),
min_size,
osds_per_server,
storage_unit
};
}
// Routes
app.get('/', csrfProtection, (req, res) => {
res.render('index', { csrfToken: req.csrfToken() });
});
app.post('/calculate', csrfProtection, (req, res) => {
try {
const result = calculateCephCapacity(req.body);
res.json(result);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
app.post('/generate-pdf', csrfProtection, (req, res) => {
try {
const data = req.body;
const result = data.result;
if (!result || !result.raw_total) {
return res.status(400).json({ error: 'No calculation results available' });
}
// Simple PDF generation
const doc = new PDFDocument({ size: 'A4', margin: 50 });
const chunks = [];
doc.on('data', chunk => chunks.push(chunk));
doc.on('end', () => {
const pdfBuffer = Buffer.concat(chunks);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', 'attachment; filename="ceph-report.pdf"');
res.send(pdfBuffer);
});
doc.fontSize(20).text('Ceph Storage Capacity Report', { align: 'center' });
doc.moveDown();
doc.fontSize(12).text(`Replication Type: ${data.replication_type}`);
doc.text(`Storage Unit: ${data.storage_unit}`);
if (data.replication_type === 'replication') {
doc.text(`Replicas: ${data.replicas}`);
doc.text(`Min Size: ${data.min_size}`);
} else {
doc.text(`k: ${data.k}`);
doc.text(`m: ${data.m}`);
}
doc.moveDown();
doc.fontSize(14).text('Results');
doc.fontSize(12).text(`Total Raw Storage: ${result.raw_total} ${data.storage_unit}`);
doc.text(`Max Recommended Usage: ${result.max_usage_percent}%`);
doc.text(`Max Usable Storage: ${data.storage_unit === 'TB' ? result.max_usage_tb : result.max_usage_gb} ${data.storage_unit}`);
doc.end();
} catch (err) {
res.status(500).json({ error: 'An unexpected error occurred' });
}
});
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
});

View File

@ -1,11 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Not Found</title>
</head>
<body>
<h1>404 - Page Not Found</h1>
<a href="/">Back to home</a>
</body>
</html>

View File

@ -1,11 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Error</title>
</head>
<body>
<h1>500 - Server Error</h1>
<a href="/">Back to home</a>
</body>
</html>

View File

@ -1,486 +0,0 @@
<!DOCTYPE html>
<html lang="en" x-data="{ darkMode: localStorage.getItem('darkMode') === 'true' || (!localStorage.getItem('darkMode') && window.matchMedia('(prefers-color-scheme: dark)').matches) }"
:class="{ 'dark': darkMode }"
class="transition-colors duration-500 ease-in-out">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="<%= csrfToken %>">
<title>Ceph Max Storage Calculator</title>
<!-- HTMX for interactive elements -->
<script src="https://unpkg.com/htmx.org@1.9.4"></script>
<!-- Tailwind CSS for modern design -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
dark: {
bg: '#1a1a1a',
card: '#2d2d2d',
border: '#404040',
text: '#e5e5e5',
input: '#404040',
hover: '#505050'
}
},
transitionProperty: {
'all': 'all',
'colors': 'color, background-color, border-color, text-decoration-color, fill, stroke',
'opacity': 'opacity',
'shadow': 'box-shadow',
'transform': 'transform',
},
transitionDuration: {
'200': '200ms',
'300': '300ms',
'500': '500ms',
'700': '700ms',
},
transitionTimingFunction: {
'ease-in-out': 'cubic-bezier(0.4, 0, 0.2, 1)',
}
}
}
}
</script>
<!-- Alpine.js for reactive user interface -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.0/dist/cdn.min.js"></script>
<script src="/static/js/script.js"></script>
</head>
<body class="bg-gray-100 dark:bg-dark-bg min-h-screen transition-all duration-500 ease-in-out" x-data="cephCalculator()">
<div class="w-full px-4 py-8 bg-gray-100 dark:bg-dark-bg min-h-screen transition-all duration-500 ease-in-out">
<div class="max-w-7xl mx-auto">
<!-- Logo -->
<div class="flex justify-center mb-8">
<img src="/static/img/tk-logo.png" alt="Thomas-Krenn.AG Logo" class="h-25 w-auto">
</div>
<header class="mb-8">
<h1 class="text-3xl font-bold text-center text-[#333333] dark:text-[#F7941D]">Ceph Max Storage Capacity Calculator</h1>
<p class="text-gray-600 dark:text-gray-400 text-center mt-2">Calculate the maximum storage usage of your Ceph cluster</p>
</header>
<main class="bg-white dark:bg-dark-card rounded-lg shadow-lg p-6">
<!-- Configuration Section -->
<div class="mb-8">
<h2 class="text-xl font-semibold mb-4 text-[#F7941D] dark:text-[#F7941D]">Configuration</h2>
<!-- Replication Type Selection -->
<div class="mb-6">
<label class="block text-[#333333] dark:text-dark-text mb-2">Replication Type</label>
<div class="flex space-x-4">
<button @click="replicationType = 'replication'"
:class="replicationType === 'replication' ? 'bg-[#F7941D] text-white' : 'bg-gray-200 text-[#333333] hover:bg-[#F7941D] hover:text-white'"
class="px-4 py-2 rounded-lg transition-colors duration-200">
Replication
</button>
<button @click="replicationType = 'erasure_coding'"
:class="replicationType === 'erasure_coding' ? 'bg-[#F7941D] text-white' : 'bg-gray-200 text-[#333333] hover:bg-[#F7941D] hover:text-white'"
class="px-4 py-2 rounded-lg transition-colors duration-200">
Erasure Coding
</button>
</div>
</div>
<!-- Replication Parameters -->
<div x-show="replicationType === 'replication'" class="space-y-4">
<div>
<label class="block text-[#333333] dark:text-dark-text mb-2">Number of Replicas</label>
<input type="number" x-model="replicas" min="1"
class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:border-[#F7941D] focus:ring-[#F7941D]">
</div>
<div>
<label class="block text-[#333333] dark:text-dark-text mb-2">Minimum Size</label>
<input type="number" x-model="minSize" min="1"
class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:border-[#F7941D] focus:ring-[#F7941D]">
</div>
</div>
<!-- Erasure Coding Parameters -->
<div x-show="replicationType === 'erasure_coding'" class="space-y-4">
<div>
<label class="block text-[#333333] dark:text-dark-text mb-2">Data Chunks (k)</label>
<input type="number" x-model="k" min="1"
class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:border-[#F7941D] focus:ring-[#F7941D]">
</div>
<div>
<label class="block text-[#333333] dark:text-dark-text mb-2">Coding Chunks (m)</label>
<input type="number" x-model="m" min="1"
class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:border-[#F7941D] focus:ring-[#F7941D]">
</div>
</div>
<!-- Storage Unit Selection -->
<div class="mt-6">
<label class="block text-[#333333] dark:text-dark-text mb-2">Storage Unit</label>
<div class="flex space-x-4">
<button @click="storageUnit = 'TB'"
:class="storageUnit === 'TB' ? 'bg-[#F7941D] text-white' : 'bg-gray-200 text-[#333333] hover:bg-[#F7941D] hover:text-white'"
class="px-4 py-2 rounded-lg transition-colors duration-200">
TB
</button>
<button @click="storageUnit = 'GB'"
:class="storageUnit === 'GB' ? 'bg-[#F7941D] text-white' : 'bg-gray-200 text-[#333333] hover:bg-[#F7941D] hover:text-white'"
class="px-4 py-2 rounded-lg transition-colors duration-200">
GB
</button>
</div>
</div>
</div>
<!-- Nodes Configuration -->
<div class="mb-8">
<h2 class="text-xl font-semibold mb-4 text-[#F7941D] dark:text-[#F7941D]">Nodes Configuration</h2>
<div class="space-y-4">
<template x-for="(node, index) in nodes" :key="index">
<div class="bg-gray-50 dark:bg-dark-input p-4 rounded-lg">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium text-[#333333] dark:text-dark-text">Node <span x-text="index + 1"></span></h3>
<button @click="removeNode(index)" x-show="nodes.length > 1"
class="text-red-600 hover:text-red-800">
Remove Node
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-[#333333] dark:text-dark-text mb-2">Number of OSDs</label>
<input type="number" x-model="node.osd_count" min="1"
class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:border-[#F7941D] focus:ring-[#F7941D]">
</div>
<div>
<label class="block text-[#333333] dark:text-dark-text mb-2">OSD Size</label>
<div class="flex">
<input type="number" x-model="node.osd_size_gb" min="1" step="0.1"
class="w-full px-4 py-2 rounded-l-lg border border-gray-300 focus:border-[#F7941D] focus:ring-[#F7941D]">
<span class="inline-flex items-center px-4 py-2 bg-gray-200 text-[#333333] rounded-r-lg border border-l-0 border-gray-300">
<span x-text="storageUnit"></span>
</span>
</div>
</div>
</div>
</div>
</template>
</div>
<button @click="addNode()"
class="mt-4 px-6 py-2 bg-[#F7941D] text-white rounded-lg hover:bg-[#e88616] transition-colors duration-200">
Add Node
</button>
</div>
<!-- Actions -->
<div class="flex flex-wrap gap-4 mb-8">
<button @click="calculateCapacity()"
class="px-6 py-2 bg-[#F7941D] text-white rounded-lg hover:bg-[#e88616] transition-colors duration-200 flex items-center">
<span x-show="!isCalculating">Calculate</span>
<span x-show="isCalculating">Calculating...</span>
</button>
<button @click="generatePDF()"
class="px-6 py-2 border border-[#F7941D] text-[#F7941D] rounded-lg hover:bg-[#F7941D] hover:text-white transition-colors duration-200"
x-bind:disabled="!result.raw_total">
Generate PDF Report
</button>
<button @click="exportConfig()"
class="px-6 py-2 border border-[#F7941D] text-[#F7941D] rounded-lg hover:bg-[#F7941D] hover:text-white transition-colors duration-200">
Export Configuration
</button>
<label class="px-6 py-2 border border-[#F7941D] text-[#F7941D] rounded-lg hover:bg-[#F7941D] hover:text-white transition-colors duration-200 cursor-pointer">
Import Configuration
<input type="file" @change="importConfig($event)" class="hidden" accept=".json">
</label>
</div>
<!-- Results -->
<div x-show="result.raw_total" class="space-y-6">
<h2 class="text-xl font-semibold text-[#F7941D] dark:text-[#F7941D]">Results</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div class="p-3">
<p class="text-gray-600 dark:text-gray-400 mb-1">Total Raw Storage:</p>
<p class="text-2xl font-bold text-[#333333] dark:text-dark-text">
<span x-text="storageUnit === 'TB' ? `${result.raw_total} TB` : `${result.raw_total} GB`"></span>
</p>
</div>
<div class="p-3 bg-gray-50 dark:bg-dark-input rounded-lg">
<p class="text-gray-600 dark:text-gray-400 mb-1">Maximum Recommended Usage:</p>
<p class="text-2xl font-bold text-[#F7941D] dark:text-[#F7941D]" x-text="`${result.max_usage_percent}%`"></p>
<p class="text-xl font-semibold text-[#333333] dark:text-dark-text mt-2">
<span x-text="storageUnit === 'TB' ? `${result.max_usage_tb} TB` : `${result.max_usage_gb} GB`"></span>
</p>
</div>
</div>
<!-- Fault Tolerance -->
<div class="mt-8">
<h2 class="text-xl font-semibold mb-4 text-[#F7941D] dark:text-[#F7941D]">Fault Tolerance</h2>
<div class="space-y-4">
<div class="p-4 rounded-lg" :class="result.node_failure_tolerance ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'">
<p class="font-semibold mb-2" :class="result.node_failure_tolerance ? 'text-green-700' : 'text-red-700'">
Node Fault Tolerance: <span x-text="result.node_failure_tolerance ? 'Available' : 'Not Available'"></span>
</p>
<p class="text-gray-600" x-text="result.node_failure_info"></p>
</div>
<div class="p-4 rounded-lg" :class="result.osd_failure_tolerance ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'">
<p class="font-semibold mb-2" :class="result.osd_failure_tolerance ? 'text-green-700' : 'text-red-700'">
OSD Fault Tolerance: <span x-text="result.osd_failure_tolerance ? 'Available' : 'Limited'"></span>
</p>
<p class="text-gray-600" x-text="result.osd_failure_info"></p>
</div>
</div>
</div>
<!-- Recommendations -->
<div class="mt-8">
<h2 class="text-xl font-semibold mb-4 text-[#F7941D] dark:text-[#F7941D]">Recommendations</h2>
<div class="bg-gray-50 dark:bg-dark-input p-6 rounded-lg border border-gray-200 dark:border-dark-border">
<ul class="list-disc pl-5 space-y-2 text-[#333333] dark:text-dark-text">
<li>Ensure that after node failures, enough capacity remains to avoid reaching the 'full ratio'.</li>
<li>For replication, the number of nodes should be greater than the replication factor.</li>
<li>For erasure coding, the number of nodes should be greater than k+m (data chunks + coding chunks).</li>
<li>Recovery performance depends on network and storage speed.</li>
<li>Note that after a node failure, rebalancing the cluster takes longer the larger the failed node is.</li>
</ul>
</div>
</div>
</div>
<!-- Footer -->
<footer class="mt-12 text-center text-gray-500 dark:text-gray-400 text-sm">
<p>Ceph Storage Calculator &copy; 2025 Thomas-Krenn.AG</p>
</footer>
</main>
</div>
</div>
<script>
function cephCalculator() {
return {
replicationType: 'replication',
replicas: 3,
minSize: 2,
k: 4,
m: 2,
storageUnit: 'TB',
nodes: [
{ osd_count: 4, osd_size_gb: 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,
node_failure_tolerance: false,
node_failure_info: '',
osd_failure_tolerance: false,
osd_failure_info: '',
multi_failure_tolerance: false,
max_failure_nodes: 0
},
isCalculating: false,
addNode() {
this.nodes.push({ osd_count: 3, osd_size_gb: 2000 });
},
removeNode(index) {
if (this.nodes.length > 1) {
this.nodes.splice(index, 1);
}
},
getOsdSizeInGB(size) {
return this.storageUnit === 'TB' ? size * 1024 : size;
},
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))
})),
storage_unit: this.storageUnit
};
// Send request to server
fetch('/calculate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify(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('Error:', error);
this.isCalculating = false;
alert('Error during calculation: ' + error.message);
});
},
exportConfig() {
const data = {
replication_type: this.replicationType,
replicas: this.replicas,
min_size: this.minSize,
k: this.k,
m: this.m,
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-CSRFToken': csrfToken
},
body: JSON.stringify(data)
})
.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);
})
.catch(error => {
console.error('Error:', error);
alert('Error generating PDF: ' + error.message);
});
}
};
}
</script>
</body>
</html>