Revert "Port Python app to Node.js"

This commit is contained in:
𝓜𝓪𝓬𝓮™
2025-06-11 14:23:11 +02:00
committed by GitHub
parent 0b5a1ce743
commit ab17056dd2
8 changed files with 14 additions and 2742 deletions

2
.gitignore vendored
View File

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

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>