259 lines
9.6 KiB
JavaScript
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}`);
|
|
});
|