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