From 7222f817ab290543e45fe9025d9dd07d3e665075 Mon Sep 17 00:00:00 2001 From: Jukka Lampikoski Date: Wed, 11 Mar 2026 09:36:11 +0200 Subject: [PATCH] =?UTF-8?q?IPAM:=20IPv6-tuki=20+=20subnetin=20k=C3=A4ytt?= =?UTF-8?q?=C3=B6aste-laskuri?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- script.js | 104 ++++++++++++++++++++++++++++++++++++++++-------------- style.css | 1 + 2 files changed, 78 insertions(+), 27 deletions(-) diff --git a/script.js b/script.js index 1485e9f..26e8293 100644 --- a/script.js +++ b/script.js @@ -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 `${childCount}`; + // 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 `${childCount}`; + const totalSlots = 1 << slotBits; // 2^slotBits + const sameLevel = node.children.filter(c => c.prefix === commonPrefix).length; + const freeSlots = totalSlots - sameLevel; + return `${sameLevel}/${totalSlots}`; } // --- 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() { ${toggleIcon}${typeTag} - ${esc(e.verkko || '-')}${drillBtn} + ${esc(e.verkko || '-')}${drillBtn} ${subnetUsageHtml(r.node)} ${esc(e.nimi || '-')} ${vlanRefHtml(e.vlan_id)} ${e.site_name ? esc(e.site_name) : ''} diff --git a/style.css b/style.css index 9e7f6fa..0a78ca5 100644 --- a/style.css +++ b/style.css @@ -1089,6 +1089,7 @@ span.empty { .ipam-bc-link:hover { text-decoration:underline; } .ipam-bc-current { color:#333; font-weight:600; cursor:default; } .ipam-bc-current:hover { text-decoration:none; } +.ipam-usage { display:inline-block; margin-left:0.5rem; padding:1px 6px; border-radius:8px; font-size:0.72rem; font-weight:600; background:#f0f4ff; color:#4338ca; } /* Role badge */ .role-badge {