IPAM: IPv6-tuki + subnetin käyttöaste-laskuri

- parseNetwork() ja isSubnetOf() tukevat nyt IPv4 ja IPv6 (BigInt)
- IPv6 verkot (esim. 2001:db8::/32) sijoittuvat puuhierarkiaan
- Subnetin kohdalla näytetään käyttöaste: esim. "3/256" kertoo
  kuinka monta alisubnettia on käytössä vs kapasiteetti
- Ei-parsittavat osoitteet näytetään puun juuressa

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 09:36:11 +02:00
parent 2686377fe2
commit 7222f817ab
2 changed files with 78 additions and 27 deletions

104
script.js
View File

@@ -2973,52 +2973,100 @@ let ipamData = [];
let ipamExpandedIds = new Set();
let ipamDrillStack = []; // [{id, label}] breadcrumb
// --- IP-laskenta-apufunktiot ---
function ipToInt(ip) {
return ip.split('.').reduce((acc, oct) => (acc << 8) + parseInt(oct), 0) >>> 0;
// --- IP-laskenta-apufunktiot (IPv4 + IPv6) ---
function ipv4ToBI(ip) {
return ip.split('.').reduce((acc, oct) => (acc << 8n) + BigInt(parseInt(oct)), 0n);
}
function prefixToMask(prefix) {
return prefix === 0 ? 0 : (~0 << (32 - prefix)) >>> 0;
function ipv6ToBI(ip) {
// Expand :: shorthand
let parts = ip.split('::');
let left = parts[0] ? parts[0].split(':') : [];
let right = parts.length > 1 && parts[1] ? parts[1].split(':') : [];
const missing = 8 - left.length - right.length;
const full = [...left, ...Array(missing).fill('0'), ...right];
return full.reduce((acc, hex) => (acc << 16n) + BigInt(parseInt(hex || '0', 16)), 0n);
}
function parseNetwork(verkko) {
if (!verkko) return null;
const v = verkko.trim();
let ip, prefix;
if (v.includes('/')) {
const [ip, pfx] = v.split('/');
const prefix = parseInt(pfx);
if (isNaN(prefix) || prefix < 0 || prefix > 32) return null;
const parts = ip.split('.');
if (parts.length !== 4) return null;
return { net: ipToInt(ip), prefix };
const slash = v.lastIndexOf('/');
ip = v.substring(0, slash);
prefix = parseInt(v.substring(slash + 1));
if (isNaN(prefix) || prefix < 0) return null;
} else {
const parts = v.split('.');
if (parts.length !== 4) return null;
return { net: ipToInt(v), prefix: 32 };
ip = v;
prefix = null; // auto-detect
}
// IPv6?
if (ip.includes(':')) {
const maxBits = 128;
if (prefix === null) prefix = 128;
if (prefix > maxBits) return null;
try {
return { net: ipv6ToBI(ip), prefix, bits: maxBits, v6: true };
} catch { return null; }
}
// IPv4
const parts = ip.split('.');
if (parts.length !== 4) return null;
const maxBits = 32;
if (prefix === null) prefix = 32;
if (prefix > maxBits) return null;
return { net: ipv4ToBI(ip), prefix, bits: maxBits, v6: false };
}
function isSubnetOf(childNet, childPrefix, parentNet, parentPrefix) {
function isSubnetOf(childNet, childPrefix, childBits, parentNet, parentPrefix, parentBits) {
if (childBits !== parentBits) return false; // eri perhe (v4 vs v6)
if (childPrefix <= parentPrefix) return false;
const mask = prefixToMask(parentPrefix);
return (childNet & mask) === (parentNet & mask);
const shift = BigInt(parentBits - parentPrefix);
return (childNet >> shift) === (parentNet >> shift);
}
// Laske subnetin käyttöaste: kuinka monta lasta (direct children) vs kapasiteetti
function subnetUsageHtml(node) {
if (node.entry.tyyppi !== 'subnet' || node.children.length === 0) return '';
const childCount = node.children.length;
// Laske kuinka monta "slottia" tässä subnetissa on seuraavalla tasolla
// Etsi yleisin lapsi-prefix
const childPrefixes = node.children.filter(c => c.prefix > node.prefix).map(c => c.prefix);
if (childPrefixes.length === 0) return `<span class="ipam-usage">${childCount}</span>`;
// Käytä pienintä child-prefixiä (isoimpia aliverkkoja) kapasiteetin laskuun
const commonPrefix = Math.min(...childPrefixes);
const bits = node.entry.tyyppi === 'subnet' ? (node.children[0]?.bits || 32) : 32;
const slotBits = commonPrefix - node.prefix;
if (slotBits <= 0 || slotBits > 20) return `<span class="ipam-usage">${childCount}</span>`;
const totalSlots = 1 << slotBits; // 2^slotBits
const sameLevel = node.children.filter(c => c.prefix === commonPrefix).length;
const freeSlots = totalSlots - sameLevel;
return `<span class="ipam-usage" title="${sameLevel}/${totalSlots} /${commonPrefix} käytössä">${sameLevel}/${totalSlots}</span>`;
}
// --- Puurakenne ---
function buildIpamTree(entries) {
// Parsitaan verkko-osoitteet ja suodatetaan validit
const items = entries.map(e => {
// Parsitaan verkko-osoitteet; ei-parsittavat lisätään juureen sellaisenaan
const unparsed = [];
const items = [];
for (const e of entries) {
const parsed = parseNetwork(e.verkko);
return parsed ? { entry: e, net: parsed.net, prefix: parsed.prefix, children: [] } : null;
}).filter(Boolean);
// Järjestetään: pienin prefix (isoin verkko) ensin, sitten IP-osoitteen mukaan
items.sort((a, b) => a.prefix - b.prefix || a.net - b.net);
if (parsed) {
items.push({ entry: e, net: parsed.net, prefix: parsed.prefix, bits: parsed.bits, v6: parsed.v6, children: [] });
} else {
unparsed.push({ entry: e, net: 0n, prefix: 0, bits: 0, v6: false, children: [] });
}
}
// Järjestetään: v4 ennen v6, pienin prefix ensin, sitten osoitteen mukaan
items.sort((a, b) => {
if (a.v6 !== b.v6) return a.v6 ? 1 : -1;
if (a.prefix !== b.prefix) return a.prefix - b.prefix;
return a.net < b.net ? -1 : a.net > b.net ? 1 : 0;
});
const roots = [];
for (const item of items) {
let placed = false;
// Etsi lähin parent (suurin prefix joka sisältää tämän)
const findParent = (nodes) => {
for (let i = nodes.length - 1; i >= 0; i--) {
const node = nodes[i];
if (isSubnetOf(item.net, item.prefix, node.net, node.prefix)) {
if (isSubnetOf(item.net, item.prefix, item.bits, node.net, node.prefix, node.bits)) {
// Tarkista ensin onko jokin lapsi tarkempi parent
if (!findParent(node.children)) {
node.children.push(item);
@@ -3032,6 +3080,8 @@ function buildIpamTree(entries) {
roots.push(item);
}
}
// Lisää ei-parsittavat (esim. virheelliset osoitteet) juureen
roots.push(...unparsed);
return roots;
}
@@ -3057,7 +3107,7 @@ function flattenTree(nodes, depth, drillId) {
for (const node of list) {
const hasChildren = node.children.length > 0;
const expanded = ipamExpandedIds.has(node.entry.id);
rows.push({ entry: node.entry, depth: d, hasChildren, expanded });
rows.push({ entry: node.entry, depth: d, hasChildren, expanded, node });
if (hasChildren && expanded) {
render(node.children, d + 1);
}
@@ -3140,7 +3190,7 @@ function renderIpam() {
<td style="padding-left:${indent}rem;white-space:nowrap;">
${toggleIcon}${typeTag}
</td>
<td><code class="ipam-network">${esc(e.verkko || '-')}</code>${drillBtn}</td>
<td><code class="ipam-network">${esc(e.verkko || '-')}</code>${drillBtn} ${subnetUsageHtml(r.node)}</td>
<td>${esc(e.nimi || '-')}</td>
<td>${vlanRefHtml(e.vlan_id)}</td>
<td>${e.site_name ? esc(e.site_name) : '<span style="color:#ccc;">—</span>'}</td>