@gtadi/k8s-node-debugger 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/public/app.js +14 -6
- package/public/health-view.js +261 -3
- package/public/style.css +85 -0
- package/src/probes.js +33 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtadi/k8s-node-debugger",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Spin up a privileged debug pod on a target Kubernetes node and inspect its network stack (iptables, resolv.conf, conntrack, routes, sockets) from a browser UI.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"k8s-node-debugger": "bin/k8s-node-debugger.js"
|
package/public/app.js
CHANGED
|
@@ -191,6 +191,7 @@ function buildSidebar(probes) {
|
|
|
191
191
|
'Health': { panelId: 'health', label: 'Node Health' },
|
|
192
192
|
'Firewall': { panelId: 'firewall', label: 'Firewall' },
|
|
193
193
|
'Conntrack': { panelId: 'conntrack', label: 'Conntrack' },
|
|
194
|
+
'Storage': { panelId: 'storage', label: 'Storage' },
|
|
194
195
|
};
|
|
195
196
|
if (GROUP_PANELS[grp]) {
|
|
196
197
|
const { panelId, label } = GROUP_PANELS[grp];
|
|
@@ -250,6 +251,7 @@ function navItem(id, label, type) {
|
|
|
250
251
|
const HEALTH_ORDER = ['cpu-stat', 'mem-info', 'disk-usage', 'mem-pressure', 'oom-kills', 'kubelet-logs'];
|
|
251
252
|
const FIREWALL_ORDER = ['iptables', 'iptables-nat', 'nftables', 'ipvs'];
|
|
252
253
|
const CONNTRACK_ORDER = ['conntrack', 'conntrack-stats', 'conntrack-count'];
|
|
254
|
+
const STORAGE_ORDER = ['storage-partitions', 'storage-du-tree', 'storage-containers'];
|
|
253
255
|
|
|
254
256
|
// Probes that get a rich custom renderer instead of a plain <pre>.
|
|
255
257
|
const FANCY_PROBES = new Set([
|
|
@@ -257,6 +259,7 @@ const FANCY_PROBES = new Set([
|
|
|
257
259
|
'conntrack', 'conntrack-stats', 'conntrack-count',
|
|
258
260
|
'mem-info', 'mem-pressure', 'oom-kills', 'kubelet-logs', 'disk-usage', 'cpu-stat',
|
|
259
261
|
'gpu-info', 'gpu-health', 'gpu-processes',
|
|
262
|
+
'storage-partitions', 'storage-du-tree', 'storage-containers',
|
|
260
263
|
]);
|
|
261
264
|
|
|
262
265
|
function buildProbePanel(probe) {
|
|
@@ -384,11 +387,14 @@ function tryFancyRender(id, output, container) {
|
|
|
384
387
|
'mem-pressure': () => renderMemPressureView(output, container),
|
|
385
388
|
'oom-kills': () => renderOomKillsView(output, container),
|
|
386
389
|
'kubelet-logs': () => renderKubeletLogsView(output, container),
|
|
387
|
-
'disk-usage':
|
|
388
|
-
'cpu-stat':
|
|
389
|
-
'gpu-info':
|
|
390
|
-
'gpu-health':
|
|
391
|
-
'gpu-processes':
|
|
390
|
+
'disk-usage': () => renderDiskView(output, container),
|
|
391
|
+
'cpu-stat': () => renderCpuView(output, container),
|
|
392
|
+
'gpu-info': () => renderGpuInfoView(output, container),
|
|
393
|
+
'gpu-health': () => renderGpuHealthView(output, container),
|
|
394
|
+
'gpu-processes': () => renderGpuProcessesView(output, container),
|
|
395
|
+
'storage-partitions': () => renderStoragePartitionsView(output, container),
|
|
396
|
+
'storage-du-tree': () => renderStorageDuTreeView(output, container),
|
|
397
|
+
'storage-containers': () => renderStorageContainersView(output, container),
|
|
392
398
|
};
|
|
393
399
|
if (!renderers[id]) return false;
|
|
394
400
|
try { renderers[id](); return true; } catch (e) { console.error('[fancy render]', id, e); return false; }
|
|
@@ -522,10 +528,11 @@ async function init() {
|
|
|
522
528
|
session = await api('/api/session');
|
|
523
529
|
const nodes = await api('/api/nodes');
|
|
524
530
|
|
|
525
|
-
const GROUP_IDS = new Set(['Health', 'Firewall', 'Conntrack']);
|
|
531
|
+
const GROUP_IDS = new Set(['Health', 'Firewall', 'Conntrack', 'Storage']);
|
|
526
532
|
const healthProbes = session.probes.filter(p => p.group === 'Health');
|
|
527
533
|
const firewallProbes = session.probes.filter(p => p.group === 'Firewall');
|
|
528
534
|
const conntrackProbes = session.probes.filter(p => p.group === 'Conntrack');
|
|
535
|
+
const storageProbes = session.probes.filter(p => p.group === 'Storage');
|
|
529
536
|
const otherProbes = session.probes.filter(p => !GROUP_IDS.has(p.group));
|
|
530
537
|
|
|
531
538
|
buildSidebar(session.probes);
|
|
@@ -533,6 +540,7 @@ async function init() {
|
|
|
533
540
|
buildGroupPanel('health', 'Node Health', healthProbes, HEALTH_ORDER);
|
|
534
541
|
buildGroupPanel('firewall', 'Firewall', firewallProbes, FIREWALL_ORDER);
|
|
535
542
|
buildGroupPanel('conntrack', 'Conntrack', conntrackProbes, CONNTRACK_ORDER);
|
|
543
|
+
buildGroupPanel('storage', 'Storage', storageProbes, STORAGE_ORDER);
|
|
536
544
|
|
|
537
545
|
renderOverview(nodes);
|
|
538
546
|
showPanel('overview');
|
package/public/health-view.js
CHANGED
|
@@ -860,6 +860,261 @@
|
|
|
860
860
|
container.appendChild(wrap);
|
|
861
861
|
}
|
|
862
862
|
|
|
863
|
+
/* ══════════════════════════════════════════════════════════════════════
|
|
864
|
+
* Storage — Partition overview (filtered df -hT)
|
|
865
|
+
* ══════════════════════════════════════════════════════════════════════ */
|
|
866
|
+
function renderStoragePartitionsView(raw, container) {
|
|
867
|
+
const lines = raw.split('\n').filter(l => l.trim());
|
|
868
|
+
if (lines.length < 2) { container.textContent = raw; return; }
|
|
869
|
+
|
|
870
|
+
const rows = [];
|
|
871
|
+
for (const line of lines.slice(1)) {
|
|
872
|
+
const parts = line.trim().split(/\s+/);
|
|
873
|
+
if (parts.length < 7) continue;
|
|
874
|
+
const [fs, type, size, used, avail, usePct, ...rest] = parts;
|
|
875
|
+
const mount = rest.join(' ');
|
|
876
|
+
const p = parseInt(usePct);
|
|
877
|
+
rows.push({ fs, type, size, used, avail, pct: isNaN(p) ? 0 : p, mount });
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (!rows.length) { container.textContent = raw; return; }
|
|
881
|
+
|
|
882
|
+
const wrap = document.createElement('div');
|
|
883
|
+
wrap.className = 'hv-wrap';
|
|
884
|
+
|
|
885
|
+
const rowsHtml = rows.map(r => {
|
|
886
|
+
const cls = colorCls(r.pct);
|
|
887
|
+
return `
|
|
888
|
+
<div class="hv-disk-row">
|
|
889
|
+
<div class="hv-disk-header">
|
|
890
|
+
<span class="hv-disk-mount">${h(r.mount)}</span>
|
|
891
|
+
<span class="hv-disk-fs">${h(r.fs)} <span style="opacity:0.5;font-size:11px">${h(r.type)}</span></span>
|
|
892
|
+
<div class="hv-disk-sizes">
|
|
893
|
+
<span class="hv-disk-pct ${cls}">${r.pct}%</span>
|
|
894
|
+
<span class="hv-disk-nums">${h(r.used)} used · ${h(r.avail)} free · ${h(r.size)} total</span>
|
|
895
|
+
</div>
|
|
896
|
+
</div>
|
|
897
|
+
<div class="hv-disk-bar"><div class="hv-disk-fill ${cls}" style="width:${r.pct}%"></div></div>
|
|
898
|
+
</div>`;
|
|
899
|
+
}).join('');
|
|
900
|
+
|
|
901
|
+
wrap.innerHTML = `<div class="hv-disk-list">${rowsHtml}</div>`;
|
|
902
|
+
container.innerHTML = '';
|
|
903
|
+
container.className = '';
|
|
904
|
+
container.appendChild(wrap);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/* ══════════════════════════════════════════════════════════════════════
|
|
908
|
+
* Storage — du tree drill-down
|
|
909
|
+
* ══════════════════════════════════════════════════════════════════════ */
|
|
910
|
+
function renderStorageDuTreeView(raw, container) {
|
|
911
|
+
const SECTION_LABELS = {
|
|
912
|
+
stateful: '/mnt/stateful_partition',
|
|
913
|
+
var: '/var',
|
|
914
|
+
varlib: '/var/lib',
|
|
915
|
+
containerd: '/var/lib/containerd',
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
const sections = {};
|
|
919
|
+
let cur = null;
|
|
920
|
+
for (const line of raw.split('\n')) {
|
|
921
|
+
const sec = line.match(/^=(stateful|var|varlib|containerd)=$/);
|
|
922
|
+
if (sec) { cur = sec[1]; sections[cur] = []; continue; }
|
|
923
|
+
if (cur && line.trim()) sections[cur].push(line.trim());
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function parseHumanSize(s) {
|
|
927
|
+
const m = (s || '').trim().match(/^([0-9.]+)\s*([KMGTP]?)/i);
|
|
928
|
+
if (!m) return 0;
|
|
929
|
+
const n = parseFloat(m[1]);
|
|
930
|
+
const mul = { '': 1, K: 1024, M: 1024 ** 2, G: 1024 ** 3, T: 1024 ** 4 }[m[2].toUpperCase()] || 1;
|
|
931
|
+
return n * mul;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const wrap = document.createElement('div');
|
|
935
|
+
wrap.className = 'hv-wrap';
|
|
936
|
+
|
|
937
|
+
const order = ['stateful', 'var', 'varlib', 'containerd'];
|
|
938
|
+
const sectionsHtml = order.filter(k => sections[k]?.length).map(key => {
|
|
939
|
+
const entries = sections[key]
|
|
940
|
+
.map(line => { const [size, ...pathParts] = line.split(/\t/); return { size: size.trim(), path: pathParts.join('\t').trim() }; })
|
|
941
|
+
.filter(e => e.size && e.path);
|
|
942
|
+
|
|
943
|
+
if (!entries.length) return '';
|
|
944
|
+
|
|
945
|
+
const maxBytes = Math.max(...entries.map(e => parseHumanSize(e.size)), 1);
|
|
946
|
+
|
|
947
|
+
const rowsHtml = entries.slice(0, 20).map(e => {
|
|
948
|
+
const bytes = parseHumanSize(e.size);
|
|
949
|
+
const pct = Math.min(Math.round(bytes / maxBytes * 100), 100);
|
|
950
|
+
const cls = pct >= 80 ? 'hv-crit' : pct >= 50 ? 'hv-warn' : 'hv-ok';
|
|
951
|
+
const name = e.path.replace(/^.*\//, '') || e.path;
|
|
952
|
+
return `
|
|
953
|
+
<div class="st-du-row">
|
|
954
|
+
<div class="st-du-header">
|
|
955
|
+
<span class="st-du-name" title="${h(e.path)}">${h(name)}</span>
|
|
956
|
+
<span class="st-du-size ${pct >= 60 ? cls : ''}">${h(e.size)}</span>
|
|
957
|
+
</div>
|
|
958
|
+
<div class="hv-gauge-bar"><div class="hv-gauge-fill ${cls}" style="width:${pct}%"></div></div>
|
|
959
|
+
</div>`;
|
|
960
|
+
}).join('');
|
|
961
|
+
|
|
962
|
+
return `
|
|
963
|
+
<div class="st-du-section">
|
|
964
|
+
<div class="st-du-section-title">${h(SECTION_LABELS[key] || key)}</div>
|
|
965
|
+
<div class="st-du-rows">${rowsHtml}</div>
|
|
966
|
+
</div>`;
|
|
967
|
+
}).join('');
|
|
968
|
+
|
|
969
|
+
wrap.innerHTML = sectionsHtml || '<div class="hv-info-note">No du data available — path may not exist on this node.</div>';
|
|
970
|
+
container.innerHTML = '';
|
|
971
|
+
container.className = '';
|
|
972
|
+
container.appendChild(wrap);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/* ══════════════════════════════════════════════════════════════════════
|
|
976
|
+
* Storage — Top containers by snapshot disk usage
|
|
977
|
+
* Parses three sections from the compound command:
|
|
978
|
+
* =SNAPS= du -d 1 …/snapshots (bytes\tpath)
|
|
979
|
+
* =MOUNTS= mount | grep snapshots
|
|
980
|
+
* =CRICTL= crictl ps -a
|
|
981
|
+
* ══════════════════════════════════════════════════════════════════════ */
|
|
982
|
+
function renderStorageContainersView(raw, container) {
|
|
983
|
+
const sections = {};
|
|
984
|
+
let cur = null;
|
|
985
|
+
for (const line of raw.split('\n')) {
|
|
986
|
+
const sec = line.match(/^=(SNAPS|MOUNTS|CRICTL)=$/);
|
|
987
|
+
if (sec) { cur = sec[1]; sections[cur] = []; continue; }
|
|
988
|
+
if (cur && line.trim()) sections[cur].push(line);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// 1. Parse snapshot sizes: "1234567\t/path/snapshots/932" → snapId → KB
|
|
992
|
+
const snapKb = new Map();
|
|
993
|
+
for (const line of (sections.SNAPS || [])) {
|
|
994
|
+
const m = line.match(/^(\d+)\s+.*\/(\d+)\s*$/);
|
|
995
|
+
if (m) snapKb.set(m[2], parseInt(m[1]));
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// 2. Build snapId → containerHash from overlay mount lines.
|
|
999
|
+
// Mount target contains: /k8s.io/{64-char-hash}/rootfs
|
|
1000
|
+
// Mount options contain: snapshots/NNN/ references
|
|
1001
|
+
const snapToHash = new Map();
|
|
1002
|
+
for (const line of (sections.MOUNTS || [])) {
|
|
1003
|
+
const hashM = line.match(/\/k8s\.io\/([a-f0-9]{64})\//);
|
|
1004
|
+
if (!hashM) continue;
|
|
1005
|
+
const hash = hashM[1];
|
|
1006
|
+
for (const m of line.matchAll(/snapshots\/(\d+)\//g)) {
|
|
1007
|
+
if (!snapToHash.has(m[1])) snapToHash.set(m[1], hash);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// 3. Parse crictl ps -a using header positions for fixed-width columns.
|
|
1012
|
+
const hashToInfo = new Map();
|
|
1013
|
+
const crictlLines = (sections.CRICTL || []).filter(l => l.trim());
|
|
1014
|
+
if (crictlLines.length > 1) {
|
|
1015
|
+
const header = crictlLines[0];
|
|
1016
|
+
const colStarts = {
|
|
1017
|
+
CONTAINER: header.indexOf('CONTAINER'),
|
|
1018
|
+
IMAGE: header.indexOf('IMAGE'),
|
|
1019
|
+
STATE: header.indexOf('STATE'),
|
|
1020
|
+
NAME: header.indexOf('NAME'),
|
|
1021
|
+
POD: header.lastIndexOf('POD'),
|
|
1022
|
+
};
|
|
1023
|
+
for (const line of crictlLines.slice(1)) {
|
|
1024
|
+
if (line.startsWith('CONTAINER')) continue;
|
|
1025
|
+
function col(start, end) {
|
|
1026
|
+
return end > start ? line.substring(start, end).trim() : line.substring(start).trim();
|
|
1027
|
+
}
|
|
1028
|
+
const id = col(colStarts.CONTAINER, colStarts.IMAGE);
|
|
1029
|
+
const state = col(colStarts.STATE, colStarts.NAME);
|
|
1030
|
+
const name = col(colStarts.NAME, colStarts.POD);
|
|
1031
|
+
const pod = line.substring(colStarts.POD).trim();
|
|
1032
|
+
if (!id) continue;
|
|
1033
|
+
const info = { state, name, pod: pod || name };
|
|
1034
|
+
hashToInfo.set(id, info);
|
|
1035
|
+
if (id.length > 12) hashToInfo.set(id.substring(0, 12), info);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// 4. Build ranked rows: snapId sorted by KB desc → join hash → join container info
|
|
1040
|
+
const rows = [...snapKb.entries()]
|
|
1041
|
+
.sort((a, b) => b[1] - a[1])
|
|
1042
|
+
.slice(0, 25)
|
|
1043
|
+
.map(([snapId, kb]) => {
|
|
1044
|
+
const hash = snapToHash.get(snapId);
|
|
1045
|
+
const info = hash
|
|
1046
|
+
? (hashToInfo.get(hash) || hashToInfo.get(hash.substring(0, 12)))
|
|
1047
|
+
: null;
|
|
1048
|
+
return { snapId, kb, hash, info };
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
const wrap = document.createElement('div');
|
|
1052
|
+
wrap.className = 'hv-wrap';
|
|
1053
|
+
|
|
1054
|
+
if (!rows.length) {
|
|
1055
|
+
wrap.innerHTML = '<div class="hv-info-note">No containerd overlayfs snapshot data found. The snapshotter path may differ on this node.</div>';
|
|
1056
|
+
container.innerHTML = '';
|
|
1057
|
+
container.appendChild(wrap);
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const totalKb = [...snapKb.values()].reduce((a, b) => a + b, 0);
|
|
1062
|
+
const mappedCount = rows.filter(r => r.info).length;
|
|
1063
|
+
const maxKb = rows[0].kb || 1;
|
|
1064
|
+
|
|
1065
|
+
const listHtml = rows.map((r, i) => {
|
|
1066
|
+
const pct = Math.min(Math.round(r.kb / maxKb * 100), 100);
|
|
1067
|
+
const cls = r.kb >= maxKb * 0.5 ? 'hv-crit' : r.kb >= maxKb * 0.2 ? 'hv-warn' : 'hv-ok';
|
|
1068
|
+
const stateCls = r.info?.state === 'Running' ? 'hv-ok' : r.info?.state ? 'hv-warn' : '';
|
|
1069
|
+
const podLabel = r.info?.pod || (r.hash ? r.hash.substring(0, 16) + '…' : '—');
|
|
1070
|
+
const nameLabel = r.info?.name || '';
|
|
1071
|
+
return `
|
|
1072
|
+
<div class="st-cont-row">
|
|
1073
|
+
<div class="st-cont-header">
|
|
1074
|
+
<span class="st-cont-rank">#${i + 1}</span>
|
|
1075
|
+
<div class="st-cont-names">
|
|
1076
|
+
<span class="st-cont-pod">${h(podLabel)}</span>
|
|
1077
|
+
${nameLabel ? `<span class="st-cont-name">${h(nameLabel)}</span>` : ''}
|
|
1078
|
+
</div>
|
|
1079
|
+
${r.info?.state ? `<span class="st-cont-state ${stateCls}">${h(r.info.state)}</span>` : ''}
|
|
1080
|
+
<span class="st-cont-size ${cls}">${hBytes(r.kb)}</span>
|
|
1081
|
+
</div>
|
|
1082
|
+
<div class="hv-gauge-bar st-cont-bar">
|
|
1083
|
+
<div class="hv-gauge-fill ${cls}" style="width:${pct}%"></div>
|
|
1084
|
+
</div>
|
|
1085
|
+
<div class="st-cont-meta">
|
|
1086
|
+
<span class="st-cont-meta-tag">snap #${h(r.snapId)}</span>
|
|
1087
|
+
${r.hash ? `<span class="st-cont-meta-tag">${h(r.hash.substring(0, 20))}…</span>` : '<span class="st-cont-meta-unmapped">unmapped</span>'}
|
|
1088
|
+
</div>
|
|
1089
|
+
</div>`;
|
|
1090
|
+
}).join('');
|
|
1091
|
+
|
|
1092
|
+
wrap.innerHTML = `
|
|
1093
|
+
<div class="hv-info-note">
|
|
1094
|
+
Containerd overlayfs snapshot layers ranked by disk usage. Active layers are mapped to pods via overlay mounts and crictl.
|
|
1095
|
+
Prune dangling images with: <code>nsenter -t 1 -m -u -i -n -p -- crictl rmi --prune</code>
|
|
1096
|
+
</div>
|
|
1097
|
+
<div class="st-summary">
|
|
1098
|
+
<div class="hv-grid-item">
|
|
1099
|
+
<div class="hv-grid-label">Snapshots tracked</div>
|
|
1100
|
+
<div class="hv-grid-val">${snapKb.size}</div>
|
|
1101
|
+
</div>
|
|
1102
|
+
<div class="hv-grid-item">
|
|
1103
|
+
<div class="hv-grid-label">Mapped to pods</div>
|
|
1104
|
+
<div class="hv-grid-val">${mappedCount} / ${rows.length}</div>
|
|
1105
|
+
</div>
|
|
1106
|
+
<div class="hv-grid-item">
|
|
1107
|
+
<div class="hv-grid-label">Total snapshot storage</div>
|
|
1108
|
+
<div class="hv-grid-val">${hBytes(totalKb)}</div>
|
|
1109
|
+
</div>
|
|
1110
|
+
</div>
|
|
1111
|
+
<div class="st-cont-list">${listHtml}</div>`;
|
|
1112
|
+
|
|
1113
|
+
container.innerHTML = '';
|
|
1114
|
+
container.className = '';
|
|
1115
|
+
container.appendChild(wrap);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
863
1118
|
/* ── Exports ─────────────────────────────────────────────────────────── */
|
|
864
1119
|
window.renderMemInfoView = renderMemInfoView;
|
|
865
1120
|
window.renderMemPressureView = renderMemPressureView;
|
|
@@ -867,8 +1122,11 @@
|
|
|
867
1122
|
window.renderKubeletLogsView = renderKubeletLogsView;
|
|
868
1123
|
window.renderDiskView = renderDiskView;
|
|
869
1124
|
window.renderCpuView = renderCpuView;
|
|
870
|
-
window.renderGpuInfoView
|
|
871
|
-
window.renderGpuHealthView
|
|
872
|
-
window.renderGpuProcessesView
|
|
1125
|
+
window.renderGpuInfoView = renderGpuInfoView;
|
|
1126
|
+
window.renderGpuHealthView = renderGpuHealthView;
|
|
1127
|
+
window.renderGpuProcessesView = renderGpuProcessesView;
|
|
1128
|
+
window.renderStoragePartitionsView = renderStoragePartitionsView;
|
|
1129
|
+
window.renderStorageDuTreeView = renderStorageDuTreeView;
|
|
1130
|
+
window.renderStorageContainersView = renderStorageContainersView;
|
|
873
1131
|
|
|
874
1132
|
})();
|
package/public/style.css
CHANGED
|
@@ -914,3 +914,88 @@ kbd {
|
|
|
914
914
|
.gpu-proc-type-g { background: #0d1f3c; color: var(--accent); border-color: #1e3a6e; }
|
|
915
915
|
.gpu-proc-cmd { color: var(--fg-dim); font-size: 12px; font-family: var(--mono); }
|
|
916
916
|
.gpu-proc-active { color: var(--ok); font-family: var(--mono); }
|
|
917
|
+
|
|
918
|
+
/* ── Storage views ──────────────────────────────────────────────────────── */
|
|
919
|
+
|
|
920
|
+
/* du tree drill-down */
|
|
921
|
+
.st-du-section { margin-bottom: 20px; }
|
|
922
|
+
.st-du-section-title {
|
|
923
|
+
font-size: 11px; font-weight: 700; text-transform: uppercase;
|
|
924
|
+
letter-spacing: .08em; color: var(--accent); font-family: var(--mono);
|
|
925
|
+
padding: 6px 0 8px;
|
|
926
|
+
border-bottom: 1px solid var(--border); margin-bottom: 10px;
|
|
927
|
+
}
|
|
928
|
+
.st-du-rows { display: flex; flex-direction: column; gap: 7px; }
|
|
929
|
+
.st-du-row { display: flex; flex-direction: column; gap: 4px; }
|
|
930
|
+
.st-du-header { display: flex; align-items: center; gap: 10px; }
|
|
931
|
+
.st-du-name {
|
|
932
|
+
flex: 1; font-family: var(--mono); font-size: 12.5px;
|
|
933
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
934
|
+
color: var(--fg);
|
|
935
|
+
}
|
|
936
|
+
.st-du-size {
|
|
937
|
+
font-family: var(--mono); font-size: 12.5px; font-weight: 600;
|
|
938
|
+
color: var(--fg-dim); flex-shrink: 0; min-width: 60px; text-align: right;
|
|
939
|
+
}
|
|
940
|
+
.st-du-size.hv-crit { color: var(--err); }
|
|
941
|
+
.st-du-size.hv-warn { color: #d29922; }
|
|
942
|
+
|
|
943
|
+
/* summary grid */
|
|
944
|
+
.st-summary {
|
|
945
|
+
display: flex; gap: 12px; flex-wrap: wrap;
|
|
946
|
+
margin-bottom: 18px;
|
|
947
|
+
}
|
|
948
|
+
.st-summary .hv-grid-item {
|
|
949
|
+
flex: 1; min-width: 140px;
|
|
950
|
+
background: var(--bg-2); border: 1px solid var(--border);
|
|
951
|
+
border-radius: 8px; padding: 10px 14px;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/* container ranked list */
|
|
955
|
+
.st-cont-list { display: flex; flex-direction: column; gap: 10px; }
|
|
956
|
+
.st-cont-row {
|
|
957
|
+
background: var(--bg-2); border: 1px solid var(--border);
|
|
958
|
+
border-radius: 8px; padding: 11px 14px;
|
|
959
|
+
display: flex; flex-direction: column; gap: 6px;
|
|
960
|
+
}
|
|
961
|
+
.st-cont-header {
|
|
962
|
+
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
|
|
963
|
+
}
|
|
964
|
+
.st-cont-rank {
|
|
965
|
+
font-family: var(--mono); font-size: 11px; font-weight: 700;
|
|
966
|
+
color: var(--fg-dim); min-width: 26px; flex-shrink: 0;
|
|
967
|
+
}
|
|
968
|
+
.st-cont-names { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
969
|
+
.st-cont-pod {
|
|
970
|
+
font-family: var(--mono); font-size: 13px; font-weight: 600;
|
|
971
|
+
color: var(--fg); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
972
|
+
}
|
|
973
|
+
.st-cont-name {
|
|
974
|
+
font-size: 11px; color: var(--fg-dim); font-family: var(--mono);
|
|
975
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
976
|
+
}
|
|
977
|
+
.st-cont-state {
|
|
978
|
+
font-size: 11px; font-weight: 700; font-family: var(--mono);
|
|
979
|
+
padding: 2px 7px; border-radius: 4px;
|
|
980
|
+
background: var(--bg-3); border: 1px solid var(--border);
|
|
981
|
+
color: var(--fg-dim); flex-shrink: 0;
|
|
982
|
+
}
|
|
983
|
+
.st-cont-state.hv-ok { background: #0d2b16; color: var(--ok); border-color: #1e5c3a; }
|
|
984
|
+
.st-cont-state.hv-warn { background: #2b2000; color: #d29922; border-color: #5a4500; }
|
|
985
|
+
.st-cont-size {
|
|
986
|
+
font-family: var(--mono); font-size: 13px; font-weight: 700;
|
|
987
|
+
flex-shrink: 0; color: var(--fg);
|
|
988
|
+
}
|
|
989
|
+
.st-cont-size.hv-crit { color: var(--err); }
|
|
990
|
+
.st-cont-size.hv-warn { color: #d29922; }
|
|
991
|
+
.st-cont-size.hv-ok { color: var(--ok); }
|
|
992
|
+
.st-cont-bar { margin: 0; }
|
|
993
|
+
.st-cont-meta { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
994
|
+
.st-cont-meta-tag {
|
|
995
|
+
font-family: var(--mono); font-size: 11px; color: var(--fg-dim);
|
|
996
|
+
background: var(--bg-3); border: 1px solid var(--border);
|
|
997
|
+
padding: 1px 7px; border-radius: 4px;
|
|
998
|
+
}
|
|
999
|
+
.st-cont-meta-unmapped {
|
|
1000
|
+
font-size: 11px; color: var(--fg-dim); font-style: italic; opacity: 0.6;
|
|
1001
|
+
}
|
package/src/probes.js
CHANGED
|
@@ -220,6 +220,39 @@ const PROBES = [
|
|
|
220
220
|
],
|
|
221
221
|
},
|
|
222
222
|
|
|
223
|
+
// ── Storage ─────────────────────────────────────────────────────────
|
|
224
|
+
{
|
|
225
|
+
id: 'storage-partitions',
|
|
226
|
+
label: 'Partitions',
|
|
227
|
+
group: 'Storage',
|
|
228
|
+
desc: 'Real physical host partitions (tmpfs, devtmpfs, shm, overlay filtered out). Identifies the true data-hosting disk.',
|
|
229
|
+
commands: [
|
|
230
|
+
"nsenter -t 1 -m -- df -hT 2>/dev/null | grep -vE '^tmpfs|^devtmpfs|^overlay|^shm'",
|
|
231
|
+
"df -hT | grep -vE '^tmpfs|^devtmpfs|^overlay|^shm'",
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
id: 'storage-du-tree',
|
|
236
|
+
label: 'Folder drill-down',
|
|
237
|
+
group: 'Storage',
|
|
238
|
+
desc: 'Layered du drill-down — stateful partition → /var → /var/lib → containerd — to pinpoint space consumers at each level.',
|
|
239
|
+
commands: [
|
|
240
|
+
"echo '=stateful='; nsenter -t 1 -m -- du -h -d 1 /mnt/stateful_partition 2>/dev/null | sort -h -r; echo '=var='; nsenter -t 1 -m -- du -h -d 1 /mnt/stateful_partition/var 2>/dev/null | sort -h -r; echo '=varlib='; nsenter -t 1 -m -- du -h -d 1 /mnt/stateful_partition/var/lib 2>/dev/null | sort -h -r; echo '=containerd='; nsenter -t 1 -m -- du -h -d 1 /mnt/stateful_partition/var/lib/containerd 2>/dev/null | sort -h -r",
|
|
241
|
+
"echo '=stateful='; du -h -d 1 /mnt/stateful_partition 2>/dev/null | sort -h -r; echo '=var='; du -h -d 1 /mnt/stateful_partition/var 2>/dev/null | sort -h -r; echo '=varlib='; du -h -d 1 /mnt/stateful_partition/var/lib 2>/dev/null | sort -h -r; echo '=containerd='; du -h -d 1 /mnt/stateful_partition/var/lib/containerd 2>/dev/null | sort -h -r",
|
|
242
|
+
"echo '=varlib='; du -h -d 1 /var/lib 2>/dev/null | sort -h -r; echo '=containerd='; du -h -d 1 /var/lib/containerd 2>/dev/null | sort -h -r",
|
|
243
|
+
],
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
id: 'storage-containers',
|
|
247
|
+
label: 'Top containers',
|
|
248
|
+
group: 'Storage',
|
|
249
|
+
desc: 'Ranked list of containers by disk usage. Maps containerd snapshot sizes to pod names via overlay mounts and crictl.',
|
|
250
|
+
commands: [
|
|
251
|
+
"echo '=SNAPS='; nsenter -t 1 -m -- du -d 1 /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots 2>/dev/null | sort -rn | head -40; echo '=MOUNTS='; nsenter -t 1 -m -- mount 2>/dev/null | grep snapshots; echo '=CRICTL='; nsenter -t 1 -m -u -i -n -p -- crictl ps -a 2>/dev/null",
|
|
252
|
+
"echo '=SNAPS='; du -d 1 /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots 2>/dev/null | sort -rn | head -40; echo '=MOUNTS='; mount 2>/dev/null | grep snapshots; echo '=CRICTL='; crictl ps -a 2>/dev/null",
|
|
253
|
+
],
|
|
254
|
+
},
|
|
255
|
+
|
|
223
256
|
// ── GPU ─────────────────────────────────────────────────────────────
|
|
224
257
|
{
|
|
225
258
|
id: 'gpu-info',
|