Files
Ceph-Calculator/server.js
𝓜𝓪𝓬𝓮™ c04a651b8d Port backend to Node.js
2025-06-11 14:07:20 +02:00

259 lines
9.6 KiB
JavaScript

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}`);
});