Compare commits
5 Commits
codex/lara
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6721f63225 | |||
| 141c0fa13a | |||
| b4905a48f0 | |||
| ab17056dd2 | |||
| 0b5a1ce743 |
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
- OS: [e.g. iOS]
|
||||||
|
- Browser [e.g. chrome, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Smartphone (please complete the following information):**
|
||||||
|
- Device: [e.g. iPhone6]
|
||||||
|
- OS: [e.g. iOS8.1]
|
||||||
|
- Browser [e.g. stock browser, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +0,0 @@
|
|||||||
node_modules/
|
|
||||||
.env
|
|
||||||
202
LICENSE-2.0.txt
Normal file
202
LICENSE-2.0.txt
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
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
|
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`
|
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
|
## Technology
|
||||||
|
|
||||||
- Backend: Node.js with Express
|
- Backend: Flask
|
||||||
- Frontend:
|
- Frontend:
|
||||||
- Alpine.js for reactive user interface
|
- Alpine.js for reactive user interface
|
||||||
- Tailwind CSS for modern design
|
- Tailwind CSS for modern design
|
||||||
- HTMX for interactive elements
|
- HTMX for interactive elements
|
||||||
- Dark/Light Mode with Tailwind CSS
|
- Dark/Light Mode with Tailwind CSS
|
||||||
- PDF generation with pdfkit
|
- PDF generation with ReportLab
|
||||||
- Responsive design for all devices
|
- Responsive design for all devices
|
||||||
|
|
||||||
## Detailed Features
|
## 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