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:
104
script.js
104
script.js
@@ -2973,52 +2973,100 @@ let ipamData = [];
|
|||||||
let ipamExpandedIds = new Set();
|
let ipamExpandedIds = new Set();
|
||||||
let ipamDrillStack = []; // [{id, label}] breadcrumb
|
let ipamDrillStack = []; // [{id, label}] breadcrumb
|
||||||
|
|
||||||
// --- IP-laskenta-apufunktiot ---
|
// --- IP-laskenta-apufunktiot (IPv4 + IPv6) ---
|
||||||
function ipToInt(ip) {
|
function ipv4ToBI(ip) {
|
||||||
return ip.split('.').reduce((acc, oct) => (acc << 8) + parseInt(oct), 0) >>> 0;
|
return ip.split('.').reduce((acc, oct) => (acc << 8n) + BigInt(parseInt(oct)), 0n);
|
||||||
}
|
}
|
||||||
function prefixToMask(prefix) {
|
function ipv6ToBI(ip) {
|
||||||
return prefix === 0 ? 0 : (~0 << (32 - prefix)) >>> 0;
|
// 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) {
|
function parseNetwork(verkko) {
|
||||||
if (!verkko) return null;
|
if (!verkko) return null;
|
||||||
const v = verkko.trim();
|
const v = verkko.trim();
|
||||||
|
let ip, prefix;
|
||||||
if (v.includes('/')) {
|
if (v.includes('/')) {
|
||||||
const [ip, pfx] = v.split('/');
|
const slash = v.lastIndexOf('/');
|
||||||
const prefix = parseInt(pfx);
|
ip = v.substring(0, slash);
|
||||||
if (isNaN(prefix) || prefix < 0 || prefix > 32) return null;
|
prefix = parseInt(v.substring(slash + 1));
|
||||||
|
if (isNaN(prefix) || prefix < 0) return null;
|
||||||
|
} else {
|
||||||
|
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('.');
|
const parts = ip.split('.');
|
||||||
if (parts.length !== 4) return null;
|
if (parts.length !== 4) return null;
|
||||||
return { net: ipToInt(ip), prefix };
|
const maxBits = 32;
|
||||||
} else {
|
if (prefix === null) prefix = 32;
|
||||||
const parts = v.split('.');
|
if (prefix > maxBits) return null;
|
||||||
if (parts.length !== 4) return null;
|
return { net: ipv4ToBI(ip), prefix, bits: maxBits, v6: false };
|
||||||
return { net: ipToInt(v), prefix: 32 };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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;
|
if (childPrefix <= parentPrefix) return false;
|
||||||
const mask = prefixToMask(parentPrefix);
|
const shift = BigInt(parentBits - parentPrefix);
|
||||||
return (childNet & mask) === (parentNet & mask);
|
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 ---
|
// --- Puurakenne ---
|
||||||
function buildIpamTree(entries) {
|
function buildIpamTree(entries) {
|
||||||
// Parsitaan verkko-osoitteet ja suodatetaan validit
|
// Parsitaan verkko-osoitteet; ei-parsittavat lisätään juureen sellaisenaan
|
||||||
const items = entries.map(e => {
|
const unparsed = [];
|
||||||
|
const items = [];
|
||||||
|
for (const e of entries) {
|
||||||
const parsed = parseNetwork(e.verkko);
|
const parsed = parseNetwork(e.verkko);
|
||||||
return parsed ? { entry: e, net: parsed.net, prefix: parsed.prefix, children: [] } : null;
|
if (parsed) {
|
||||||
}).filter(Boolean);
|
items.push({ entry: e, net: parsed.net, prefix: parsed.prefix, bits: parsed.bits, v6: parsed.v6, children: [] });
|
||||||
// Järjestetään: pienin prefix (isoin verkko) ensin, sitten IP-osoitteen mukaan
|
} else {
|
||||||
items.sort((a, b) => a.prefix - b.prefix || a.net - b.net);
|
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 = [];
|
const roots = [];
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
let placed = false;
|
|
||||||
// Etsi lähin parent (suurin prefix joka sisältää tämän)
|
// Etsi lähin parent (suurin prefix joka sisältää tämän)
|
||||||
const findParent = (nodes) => {
|
const findParent = (nodes) => {
|
||||||
for (let i = nodes.length - 1; i >= 0; i--) {
|
for (let i = nodes.length - 1; i >= 0; i--) {
|
||||||
const node = nodes[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
|
// Tarkista ensin onko jokin lapsi tarkempi parent
|
||||||
if (!findParent(node.children)) {
|
if (!findParent(node.children)) {
|
||||||
node.children.push(item);
|
node.children.push(item);
|
||||||
@@ -3032,6 +3080,8 @@ function buildIpamTree(entries) {
|
|||||||
roots.push(item);
|
roots.push(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Lisää ei-parsittavat (esim. virheelliset osoitteet) juureen
|
||||||
|
roots.push(...unparsed);
|
||||||
return roots;
|
return roots;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3057,7 +3107,7 @@ function flattenTree(nodes, depth, drillId) {
|
|||||||
for (const node of list) {
|
for (const node of list) {
|
||||||
const hasChildren = node.children.length > 0;
|
const hasChildren = node.children.length > 0;
|
||||||
const expanded = ipamExpandedIds.has(node.entry.id);
|
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) {
|
if (hasChildren && expanded) {
|
||||||
render(node.children, d + 1);
|
render(node.children, d + 1);
|
||||||
}
|
}
|
||||||
@@ -3140,7 +3190,7 @@ function renderIpam() {
|
|||||||
<td style="padding-left:${indent}rem;white-space:nowrap;">
|
<td style="padding-left:${indent}rem;white-space:nowrap;">
|
||||||
${toggleIcon}${typeTag}
|
${toggleIcon}${typeTag}
|
||||||
</td>
|
</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>${esc(e.nimi || '-')}</td>
|
||||||
<td>${vlanRefHtml(e.vlan_id)}</td>
|
<td>${vlanRefHtml(e.vlan_id)}</td>
|
||||||
<td>${e.site_name ? esc(e.site_name) : '<span style="color:#ccc;">—</span>'}</td>
|
<td>${e.site_name ? esc(e.site_name) : '<span style="color:#ccc;">—</span>'}</td>
|
||||||
|
|||||||
@@ -1089,6 +1089,7 @@ span.empty {
|
|||||||
.ipam-bc-link:hover { text-decoration:underline; }
|
.ipam-bc-link:hover { text-decoration:underline; }
|
||||||
.ipam-bc-current { color:#333; font-weight:600; cursor:default; }
|
.ipam-bc-current { color:#333; font-weight:600; cursor:default; }
|
||||||
.ipam-bc-current:hover { text-decoration:none; }
|
.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 */
|
||||||
.role-badge {
|
.role-badge {
|
||||||
|
|||||||
Reference in New Issue
Block a user