Revert "Port Python app to Node.js"
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
node_modules/
|
||||
.env
|
||||
21
README.md
21
README.md
@ -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
1950
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@ -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
258
server.js
@ -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}`);
|
||||
});
|
||||
@ -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>
|
||||
@ -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>
|
||||
486
views/index.ejs
486
views/index.ejs
@ -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 © 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>
|
||||
Reference in New Issue
Block a user