zfs
This commit is contained in:
parent
ad664c32ea
commit
5ec3a6006f
3 changed files with 1419 additions and 0 deletions
390
public/calculators/zfs.js
Normal file
390
public/calculators/zfs.js
Normal file
|
@ -0,0 +1,390 @@
|
|||
import {revive, persist, labelInput, labelSelect} from '/js/util.js';
|
||||
|
||||
export default {
|
||||
id:'zfs', name:'ZFS Calculator', about:'Calculate ZFS pool configurations, performance tuning, and capacity planning.',
|
||||
render(root){
|
||||
const key='calc_zfs_v1';
|
||||
const s = revive(key,{
|
||||
poolType: 'raidz2',
|
||||
diskCount: 6,
|
||||
diskSize: 4000,
|
||||
diskSizeUnit: 'GB',
|
||||
blockSize: '128K',
|
||||
compression: 'lz4',
|
||||
dedup: false,
|
||||
ashift: 12,
|
||||
arcMax: 8192,
|
||||
arcMaxUnit: 'MB'
|
||||
});
|
||||
|
||||
const ui = document.createElement('div');
|
||||
|
||||
// Pool configuration section
|
||||
const poolSection = document.createElement('div');
|
||||
poolSection.innerHTML = `
|
||||
<h3 style="color: var(--accent); margin-bottom: 15px;">Pool Configuration</h3>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 20px;">
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--text);">
|
||||
Pool Type
|
||||
</label>
|
||||
<select name="poolType" data-ui="lite" style="width: 100%;">
|
||||
<option value="stripe">Stripe (No Redundancy)</option>
|
||||
<option value="mirror">Mirror (2-way)</option>
|
||||
<option value="raidz1">RAID-Z1 (Single Parity)</option>
|
||||
<option value="raidz2">RAID-Z2 (Double Parity)</option>
|
||||
<option value="raidz3">RAID-Z3 (Triple Parity)</option>
|
||||
<option value="mirror2x2">Mirror 2x2 (2 mirrors of 2 disks)</option>
|
||||
<option value="mirror3x2">Mirror 3x2 (3 mirrors of 2 disks)</option>
|
||||
<option value="raidz2x2">RAID-Z2 2x2 (2 RAID-Z2 vdevs)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--text);">
|
||||
Number of Disks
|
||||
</label>
|
||||
<input type="number" name="diskCount" value="${s.diskCount}" min="1" max="20"
|
||||
style="width: 100%; padding: 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 16px;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--text);">
|
||||
Disk Size
|
||||
</label>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<input type="number" name="diskSize" value="${s.diskSize}" min="1"
|
||||
style="flex: 1; padding: 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 16px;">
|
||||
<select name="diskSizeUnit" data-ui="lite" style="width: 80px;">
|
||||
<option value="GB">GB</option>
|
||||
<option value="TB">TB</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--text);">
|
||||
Block Size
|
||||
</label>
|
||||
<select name="blockSize" data-ui="lite" style="width: 100%;">
|
||||
<option value="4K">4K</option>
|
||||
<option value="8K">8K</option>
|
||||
<option value="16K">16K</option>
|
||||
<option value="32K">32K</option>
|
||||
<option value="64K">64K</option>
|
||||
<option value="128K">128K</option>
|
||||
<option value="256K">256K</option>
|
||||
<option value="512K">512K</option>
|
||||
<option value="1M">1M</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Performance tuning section
|
||||
const perfSection = document.createElement('div');
|
||||
perfSection.innerHTML = `
|
||||
<h3 style="color: var(--accent); margin-bottom: 15px;">Performance Tuning</h3>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 20px;">
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--text);">
|
||||
Compression
|
||||
</label>
|
||||
<select name="compression" data-ui="lite" style="width: 100%;">
|
||||
<option value="off">Off</option>
|
||||
<option value="lz4">LZ4 (Fast)</option>
|
||||
<option value="gzip">Gzip (Balanced)</option>
|
||||
<option value="gzip-1">Gzip-1 (Fast)</option>
|
||||
<option value="gzip-9">Gzip-9 (Best)</option>
|
||||
<option value="zstd">Zstd (Modern)</option>
|
||||
<option value="zstd-1">Zstd-1 (Fast)</option>
|
||||
<option value="zstd-19">Zstd-19 (Best)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--text);">
|
||||
Deduplication
|
||||
</label>
|
||||
<select name="dedup" data-ui="lite" style="width: 100%;">
|
||||
<option value="false">Off (Recommended)</option>
|
||||
<option value="true">On (Use with Caution)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--text);">
|
||||
Ashift Value
|
||||
</label>
|
||||
<select name="ashift" data-ui="lite" style="width: 100%;">
|
||||
<option value="9">9 (512B sectors)</option>
|
||||
<option value="12">12 (4KB sectors)</option>
|
||||
<option value="13">13 (8KB sectors)</option>
|
||||
<option value="14">14 (16KB sectors)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--text);">
|
||||
ARC Max Size
|
||||
</label>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<input type="number" name="arcMax" value="${s.arcMax}" min="64"
|
||||
style="flex: 1; padding: 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 16px;">
|
||||
<select name="arcMaxUnit" data-ui="lite" style="width: 80px;">
|
||||
<option value="MB">MB</option>
|
||||
<option value="GB">GB</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
ui.append(poolSection, perfSection);
|
||||
|
||||
// Results section
|
||||
const out = document.createElement('div');
|
||||
out.className = 'result';
|
||||
out.style.cssText = `
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
background: var(--k-bg);
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid var(--accent);
|
||||
`;
|
||||
ui.append(out);
|
||||
|
||||
|
||||
// Calculation function
|
||||
function calc(){
|
||||
const poolType = ui.querySelector('[name=poolType]').value;
|
||||
const diskCount = +ui.querySelector('[name=diskCount]').value;
|
||||
const diskSize = +ui.querySelector('[name=diskSize]').value;
|
||||
const diskSizeUnit = ui.querySelector('[name=diskSizeUnit]').value;
|
||||
const blockSize = ui.querySelector('[name=blockSize]').value;
|
||||
const compression = ui.querySelector('[name=compression]').value;
|
||||
const dedup = ui.querySelector('[name=dedup]').value === 'true';
|
||||
const ashift = +ui.querySelector('[name=ashift]').value;
|
||||
const arcMax = +ui.querySelector('[name=arcMax]').value;
|
||||
const arcMaxUnit = ui.querySelector('[name=arcMaxUnit]').value;
|
||||
|
||||
// Convert disk size to GB for calculations
|
||||
let diskSizeGB = diskSize;
|
||||
if (diskSizeUnit === 'TB') {
|
||||
diskSizeGB = diskSize * 1024;
|
||||
}
|
||||
|
||||
// Calculate usable capacity based on pool type
|
||||
let usableCapacity, redundancy, minDisks, recommendedDisks, vdevCount;
|
||||
|
||||
switch(poolType) {
|
||||
case 'stripe':
|
||||
usableCapacity = diskCount * diskSizeGB;
|
||||
redundancy = 'None';
|
||||
minDisks = 1;
|
||||
recommendedDisks = 1;
|
||||
vdevCount = 1;
|
||||
break;
|
||||
case 'mirror':
|
||||
usableCapacity = Math.floor(diskCount / 2) * diskSizeGB;
|
||||
redundancy = '50%';
|
||||
minDisks = 2;
|
||||
recommendedDisks = 2;
|
||||
vdevCount = Math.floor(diskCount / 2);
|
||||
break;
|
||||
case 'raidz1':
|
||||
usableCapacity = (diskCount - 1) * diskSizeGB;
|
||||
redundancy = '1 disk';
|
||||
minDisks = 3;
|
||||
recommendedDisks = 3;
|
||||
vdevCount = 1;
|
||||
break;
|
||||
case 'raidz2':
|
||||
usableCapacity = (diskCount - 2) * diskSizeGB;
|
||||
redundancy = '2 disks';
|
||||
minDisks = 4;
|
||||
recommendedDisks = 6;
|
||||
vdevCount = 1;
|
||||
break;
|
||||
case 'raidz3':
|
||||
usableCapacity = (diskCount - 3) * diskSizeGB;
|
||||
redundancy = '3 disks';
|
||||
minDisks = 5;
|
||||
recommendedDisks = 9;
|
||||
vdevCount = 1;
|
||||
break;
|
||||
case 'mirror2x2':
|
||||
usableCapacity = 2 * diskSizeGB; // 2 mirrors, each with 1 usable disk
|
||||
redundancy = '50%';
|
||||
minDisks = 4;
|
||||
recommendedDisks = 4;
|
||||
vdevCount = 2;
|
||||
break;
|
||||
case 'mirror3x2':
|
||||
usableCapacity = 3 * diskSizeGB; // 3 mirrors, each with 1 usable disk
|
||||
redundancy = '50%';
|
||||
minDisks = 6;
|
||||
recommendedDisks = 6;
|
||||
vdevCount = 3;
|
||||
break;
|
||||
case 'raidz2x2':
|
||||
usableCapacity = 2 * (diskCount / 2 - 2) * diskSizeGB; // 2 RAID-Z2 vdevs
|
||||
redundancy = '2 disks per vdev';
|
||||
minDisks = 8;
|
||||
recommendedDisks = 12;
|
||||
vdevCount = 2;
|
||||
break;
|
||||
}
|
||||
|
||||
// Validate disk count
|
||||
if (diskCount < minDisks) {
|
||||
out.innerHTML = `<div style="color: var(--error);">
|
||||
<strong>Error:</strong> ${poolType.toUpperCase()} requires at least ${minDisks} disks
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate compression ratios
|
||||
const compressionRatios = {
|
||||
'off': 1.0,
|
||||
'lz4': 2.1,
|
||||
'gzip': 2.5,
|
||||
'gzip-1': 2.0,
|
||||
'gzip-9': 3.0,
|
||||
'zstd': 2.8,
|
||||
'zstd-1': 2.2,
|
||||
'zstd-19': 3.5
|
||||
};
|
||||
|
||||
const compressionRatio = compressionRatios[compression] || 1.0;
|
||||
const effectiveCapacity = usableCapacity * compressionRatio;
|
||||
|
||||
// Calculate performance metrics
|
||||
const blockSizeKB = parseInt(blockSize.replace(/[^0-9]/g, ''));
|
||||
const ashiftBytes = Math.pow(2, ashift);
|
||||
|
||||
// Convert ARC max to MB
|
||||
let arcMaxMB = arcMax;
|
||||
if (arcMaxUnit === 'GB') {
|
||||
arcMaxMB = arcMax * 1024;
|
||||
}
|
||||
|
||||
// Helper function to format capacity
|
||||
function formatCapacity(gb) {
|
||||
if (gb >= 1024) {
|
||||
return `${gb.toFixed(1)} GB (${(gb / 1024).toFixed(2)} TB)`;
|
||||
}
|
||||
return `${gb.toFixed(1)} GB`;
|
||||
}
|
||||
|
||||
// Calculate I/O performance estimates
|
||||
const estimatedIOPS = Math.floor(diskCount * 100); // Rough estimate: 100 IOPS per disk
|
||||
const estimatedThroughput = Math.floor(diskCount * 150); // Rough estimate: 150 MB/s per disk
|
||||
|
||||
// Calculate memory requirements (rule of thumb: 1GB RAM per 1TB storage)
|
||||
const recommendedRAM = Math.ceil((diskCount * diskSizeGB) / 1024);
|
||||
|
||||
// Generate results
|
||||
out.innerHTML = `
|
||||
<div style="font-size: 24px; font-weight: 700; color: var(--accent); margin-bottom: 15px;">
|
||||
ZFS Pool Configuration
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px;">
|
||||
<div>
|
||||
<h4 style="color: var(--text); margin-bottom: 10px;">Capacity & Redundancy</h4>
|
||||
<div style="color: var(--text); margin-bottom: 5px;">
|
||||
<strong>Raw Capacity:</strong> ${formatCapacity(diskCount * diskSizeGB)}
|
||||
</div>
|
||||
<div style="color: var(--text); margin-bottom: 5px;">
|
||||
<strong>Usable Capacity:</strong> ${formatCapacity(usableCapacity)}
|
||||
</div>
|
||||
<div style="color: var(--text); margin-bottom: 5px;">
|
||||
<strong>Effective Capacity (with compression):</strong> ${formatCapacity(effectiveCapacity)}
|
||||
</div>
|
||||
<div style="color: var(--text); margin-bottom: 5px;">
|
||||
<strong>Redundancy:</strong> ${redundancy}
|
||||
</div>
|
||||
<div style="color: var(--text); margin-bottom: 5px;">
|
||||
<strong>Efficiency:</strong> ${((usableCapacity / (diskCount * diskSizeGB)) * 100).toFixed(1)}%
|
||||
</div>
|
||||
<div style="color: var(--text); margin-bottom: 5px;">
|
||||
<strong>Compression Savings:</strong> ${((effectiveCapacity - usableCapacity) / usableCapacity * 100).toFixed(1)}%
|
||||
</div>
|
||||
<div style="color: var(--text); margin-bottom: 5px;">
|
||||
<strong>VDev Count:</strong> ${vdevCount} ${vdevCount > 1 ? 'vdevs' : 'vdev'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 style="color: var(--text); margin-bottom: 10px;">Performance Settings</h4>
|
||||
<div style="color: var(--text); margin-bottom: 5px;">
|
||||
<strong>Block Size:</strong> ${blockSize}
|
||||
</div>
|
||||
<div style="color: var(--text); margin-bottom: 5px;">
|
||||
<strong>Ashift:</strong> ${ashift} (${ashiftBytes} bytes)
|
||||
</div>
|
||||
<div style="color: var(--text); margin-bottom: 5px;">
|
||||
<strong>Compression:</strong> ${compression} (${compressionRatio.toFixed(1)}x ratio)
|
||||
</div>
|
||||
<div style="color: var(--text); margin-bottom: 5px;">
|
||||
<strong>Deduplication:</strong> ${dedup ? 'On' : 'Off'}
|
||||
</div>
|
||||
<div style="color: var(--text); margin-bottom: 5px;">
|
||||
<strong>ARC Max:</strong> ${arcMaxMB} MB
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px;">
|
||||
<div>
|
||||
<h4 style="color: var(--text); margin-bottom: 10px;">Performance Estimates</h4>
|
||||
<div style="color: var(--text); margin-bottom: 5px;">
|
||||
<strong>Estimated IOPS:</strong> ${estimatedIOPS.toLocaleString()} (random 4K reads)
|
||||
</div>
|
||||
<div style="color: var(--text); margin-bottom: 5px;">
|
||||
<strong>Estimated Throughput:</strong> ${estimatedThroughput} MB/s (sequential)
|
||||
</div>
|
||||
<div style="color: var(--text); margin-bottom: 5px;">
|
||||
<strong>Recommended RAM:</strong> ${recommendedRAM} GB minimum
|
||||
</div>
|
||||
<div style="color: var(--text); margin-bottom: 5px;">
|
||||
<strong>Current ARC:</strong> ${(arcMaxMB / 1024).toFixed(1)} GB
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 style="color: var(--text); margin-bottom: 10px;">System Requirements</h4>
|
||||
<div style="color: var(--text); margin-bottom: 5px;">
|
||||
<strong>Minimum RAM:</strong> ${Math.max(8, recommendedRAM)} GB
|
||||
</div>
|
||||
<div style="color: var(--text); margin-bottom: 5px;">
|
||||
<strong>Recommended RAM:</strong> ${Math.max(16, recommendedRAM * 2)} GB
|
||||
</div>
|
||||
<div style="color: var(--text); margin-bottom: 5px;">
|
||||
<strong>CPU Cores:</strong> ${Math.max(4, Math.ceil(diskCount / 2))} cores recommended
|
||||
</div>
|
||||
<div style="color: var(--text); margin-bottom: 5px;">
|
||||
<strong>Network:</strong> 10 Gbps recommended for ${estimatedThroughput} MB/s throughput
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
`;
|
||||
|
||||
persist(key, {poolType, diskCount, diskSize, diskSizeUnit, blockSize, compression, dedup, ashift, arcMax, arcMaxUnit});
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
ui.querySelector('[name=poolType]').addEventListener('change', calc);
|
||||
ui.querySelector('[name=diskCount]').addEventListener('input', calc);
|
||||
ui.querySelector('[name=diskSize]').addEventListener('input', calc);
|
||||
ui.querySelector('[name=diskSizeUnit]').addEventListener('change', calc);
|
||||
ui.querySelector('[name=blockSize]').addEventListener('change', calc);
|
||||
ui.querySelector('[name=compression]').addEventListener('change', calc);
|
||||
ui.querySelector('[name=dedup]').addEventListener('change', calc);
|
||||
ui.querySelector('[name=ashift]').addEventListener('change', calc);
|
||||
ui.querySelector('[name=arcMax]').addEventListener('input', calc);
|
||||
ui.querySelector('[name=arcMaxUnit]').addEventListener('change', calc);
|
||||
|
||||
// Initial calculation
|
||||
calc();
|
||||
root.append(ui);
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ const CALCS = [
|
|||
{ id:'bandwidth', name:'Bandwidth', about:'Bits↔bytes unit conv.', path:'../calculators/bandwidth.js' },
|
||||
{ id:'nmea', name:'NMEA', about:'0183 XOR checksum', path:'../calculators/nmea.js' },
|
||||
{ id:'currency', name:'Currency Converter', about:'Convert between currencies', path:'../calculators/currency.js' },
|
||||
{ id:'zfs', name:'ZFS', about:'Pool configuration & performance', path:'../calculators/zfs.js' },
|
||||
{ id:'subnet', name:'IP Subnet', about:'IPv4/IPv6 subnet calculations', path:'../calculators/subnet.js' }
|
||||
];
|
||||
|
||||
|
|
1028
tests/test_zfs.py
Normal file
1028
tests/test_zfs.py
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue