Merge pull request #265 from MacRimi/develop

update documentation
This commit is contained in:
MacRimi
2026-07-24 00:04:33 +02:00
committed by GitHub
53 changed files with 1266 additions and 327 deletions

View File

@@ -32,6 +32,20 @@ type NIC = {
rx: number // MB/s
tx: number
status?: "up" | "down" // present → drives the active state instead of the rate
// Set when this NIC is enslaved to a bond. The diagram then routes it
// through the bond node instead of straight to the host.
bond?: string
// Current role INSIDE the bond, re-read on every poll so a failover
// flips the labels. "member" = every slave transmits (LACP, balance-*),
// so no active/standby distinction exists.
role?: "active" | "standby" | "member"
}
type Bond = {
id: string
mode?: string // "active-backup", "balance-alb", "802.3ad", …
rx: number
tx: number
status?: "up" | "down"
}
type Bridge = {
id: string
@@ -48,6 +62,7 @@ type Guest = {
}
export type NetworkFlowData = {
nics: NIC[]
bonds?: Bond[]
bridges: Bridge[]
consumers: Guest[] // includes the host pseudo-entry
}
@@ -57,6 +72,9 @@ const ICONS: Record<string, string> = {
nic: `<path d="m15 20 3-3h2a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h2l3 3z"/><path d="M6 8v1"/><path d="M10 8v1"/><path d="M14 8v1"/><path d="M18 8v1"/>`,
bridge: `<rect x="16" y="16" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="9" y="2" width="6" height="6" rx="1"/><path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"/><path d="M12 12V8"/>`,
host: `<rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6" rx="1"/><path d="M15 2v2"/><path d="M15 20v2"/><path d="M2 15h2"/><path d="M2 9h2"/><path d="M20 15h2"/><path d="M20 9h2"/><path d="M9 2v2"/><path d="M9 20v2"/>`,
// Router — same glyph the interface cards use for a bond, so the node
// in the diagram and the row in the list read as the same thing.
bond: `<rect width="20" height="8" x="2" y="14" rx="2"/><path d="M6.01 18H6"/><path d="M10.01 18H10"/><path d="M15 10v4"/><path d="M17.84 7.17a4 4 0 0 0-5.66 0"/><path d="M20.66 4.34a8 8 0 0 0-11.31 0"/>`,
lxc: `<path d="M22 7.7c0-.6-.4-1.2-.8-1.5l-6.3-3.9a1.72 1.72 0 0 0-1.7 0l-10.3 6c-.5.2-.9.8-.9 1.4v6.6c0 .5.4 1.2.8 1.5l6.3 3.9a1.72 1.72 0 0 0 1.7 0l10.3-6c.5-.3.9-1 .9-1.5Z"/><path d="M10 21.9V14L2.1 9.1"/><path d="m10 14 11.9-6.9"/><path d="M14 19.8v-8.1"/><path d="M18 17.5V9.4"/>`,
vm: `<rect width="20" height="8" x="2" y="2" rx="2"/><rect width="20" height="8" x="2" y="14" rx="2"/><path d="M6 6h.01"/><path d="M6 18h.01"/>`,
}
@@ -66,10 +84,58 @@ const COLORS = {
lxc: "var(--cyan-500, #06b6d4)",
vm: "var(--purple-500, #a855f7)",
nic: "var(--amber-500, #f59e0b)",
// Warm like the NICs (both live on the physical side of the topology)
// but clearly its own hue, so the aggregation node doesn't read as
// just another NIC.
bond: "var(--orange-500, #f97316)",
bridge: "var(--cyan-500, #06b6d4)",
gray: "#525252",
}
// Bond membership resolved once and shared by both layouts. Only bonds
// that actually have a rendered slave are kept — an orphan bond node
// with nothing feeding it would be noise.
function resolveBonds(data: NetworkFlowData): {
bonds: Bond[]
bondOfNic: Map<string, string>
nics: NIC[]
} {
const declared = data.bonds || []
const byId = new Map(declared.map((b) => [b.id, b]))
const bondOfNic = new Map<string, string>()
data.nics.forEach((n) => {
if (n.bond && byId.has(n.bond)) bondOfNic.set(n.id, n.bond)
})
const bonds = declared.filter((b) =>
data.nics.some((n) => bondOfNic.get(n.id) === b.id)
)
// Group slaves of the same bond together so its node sits next to the
// NICs feeding it and the links don't cross unrelated ones. Array sort
// is stable, so NICs within a group keep their backend order.
const nics = [...data.nics].sort((a, b) => {
const ba = bondOfNic.get(a.id) || ""
const bb = bondOfNic.get(b.id) || ""
if (ba === bb) return 0
if (!ba) return 1 // unbonded NICs last
if (!bb) return -1
return ba < bb ? -1 : 1
})
return { bonds, bondOfNic, nics }
}
// Sub-label under a NIC. In active-backup the role is the useful bit
// (which cable is actually carrying traffic right now); in every other
// mode all slaves transmit, so the link speed stays.
function nicSubLabel(n: NIC): string {
if (n.status === "down") return "down"
const role = n.role === "standby" || n.role === "active" ? n.role : ""
if (!role) return n.link
// A NIC that doesn't report a negotiated speed renders its link as
// "—"; pairing that with the role would read as "— · active".
if (!n.link || n.link === "—") return role
return `${n.link} · ${role}`
}
function fmt(v: number): string {
if (!v) return "0 B/s"
// Below 1 KB/s show B/s — the previous "—" hid real-but-low traffic.
@@ -152,6 +218,7 @@ function renderHorizontal(data: NetworkFlowData, W: number): { svg: string; puls
const top = activeConsumers(data.consumers)
const bridges = visibleBridges(data.bridges, top)
const host = data.consumers.find((c) => c.kind === "host")
const { bonds, bondOfNic, nics } = resolveBonds(data)
const tight = W < 1100
const nicX = tight ? 70 : 90
@@ -165,6 +232,11 @@ function renderHorizontal(data: NetworkFlowData, W: number): { svg: string; puls
const radHost = tight ? 38 : 44
const radBridge = tight ? 22 : 26
const radGuest = 22
const radBond = tight ? 22 : 26
// Bond column sits halfway between the NICs and the host — the gap
// between those two columns was already wide enough to host it, so
// nothing downstream (bridges, bus, guests) has to move.
const bondX = Math.round((nicX + hostX) / 2)
// More vertical breathing room around each guest. The previous 110
// crammed circle+label+sub close to the bus; bumping to 135 lets
@@ -226,7 +298,7 @@ function renderHorizontal(data: NetworkFlowData, W: number): { svg: string; puls
return { b, guests, buses, sectionTop, sectionBot, bridgeY }
})
const nicCount = data.nics.length
const nicCount = nics.length
// Vertical pitch per NIC = circle + label + sub + breathing
// room. Was 118; bumped to 132 because the new 9-px-wide trunk
// line passing near sibling NICs ate into the label/sub area
@@ -255,18 +327,56 @@ function renderHorizontal(data: NetworkFlowData, W: number): { svg: string; puls
const linksPulse: PulseData[] = []
const nodes: string[] = []
data.nics.forEach((n, i) => {
const nicYById = new Map<string, number>()
nics.forEach((n, i) => {
const y = nicY0 + i * nicSpacing
nicYById.set(n.id, y)
// NIC is "active" when the kernel reports the link up — independent
// of current rate (a NIC with 100 B/s of background traffic should
// not render as gray).
const active = n.status ? n.status === "up" : n.rx + n.tx > 0
const stroke = active ? COLORS.nic : COLORS.gray
nodes.push(`<g data-node-id="${n.id}" data-node-kind="nic" style="cursor:pointer;opacity:${active ? 1 : 0.45}">
// A healthy standby is NOT a fault: it keeps its colour and is only
// slightly dimmed, so it stays clearly distinct from a down link.
const opacity = !active ? 0.45 : n.role === "standby" ? 0.72 : 1
nodes.push(`<g data-node-id="${n.id}" data-node-kind="nic" style="cursor:pointer;opacity:${opacity}">
<circle class="nf-circle" cx="${nicX}" cy="${y}" r="${radNic}" stroke="${stroke}" />
${svgIcon("nic", nicX, y, 18, stroke)}
<text class="nf-label" x="${nicX}" y="${y + radNic + 14}">${n.id}</text>
<text class="nf-sub" x="${nicX}" y="${y + radNic + 26}">${n.link}</text>
<text class="nf-sub" x="${nicX}" y="${y + radNic + 26}">${nicSubLabel(n)}</text>
</g>`)
})
// Bond node — vertically centred on the slaves feeding it, but lifted
// when unbonded NICs are also present. Those run their own trunk up to
// the host, and orthLink turns them at the midpoint of the NIC→host
// gap — which is the bond's own column. Their vertical leg therefore
// occupies everything BELOW hostY at that x, so the bond's three-line
// label has to sit above hostY to stay out of it. With every NIC in a
// bond there is no such trunk and the node keeps its natural centre.
const BOND_TEXT_BLOCK = 38 // label + mode + rate under the circle
const hasLooseNics = nics.some((n) => !bondOfNic.has(n.id))
const bondCeiling = hostY - radBond - BOND_TEXT_BLOCK - 16
const bondYById = new Map<string, number>()
bonds.forEach((b) => {
const ys = nics
.filter((n) => bondOfNic.get(n.id) === b.id)
.map((n) => nicYById.get(n.id) as number)
if (!ys.length) return
const centre = ys.reduce((a, v) => a + v, 0) / ys.length
const y = Math.max(
radBond + 8,
hasLooseNics ? Math.min(centre, bondCeiling) : centre,
)
bondYById.set(b.id, y)
const up = b.status ? b.status === "up" : true
const stroke = up ? COLORS.bond : COLORS.gray
nodes.push(`<g data-node-id="${b.id}" data-node-kind="bond" style="cursor:pointer;opacity:${up ? 1 : 0.45}">
<circle class="nf-circle" cx="${bondX}" cy="${y}" r="${radBond}" stroke="${stroke}" />
${svgIcon("bond", bondX, y, 16, stroke)}
<text class="nf-label" x="${bondX}" y="${y + radBond + 14}">${b.id}</text>
<text class="nf-sub" x="${bondX}" y="${y + radBond + 26}">${b.mode || "bond"}</text>
<text class="nf-sub" x="${bondX}" y="${y + radBond + 38}">${fmt(b.rx + b.tx)}</text>
</g>`)
})
@@ -431,13 +541,18 @@ function renderHorizontal(data: NetworkFlowData, W: number): { svg: string; puls
// background noise floor so the animation stays solid.
const NIC_PULSE_MIN_MBPS = 0.002 // 2 KB/s
data.nics.forEach((n, i) => {
const y = nicY0 + i * nicSpacing
nics.forEach((n) => {
const y = nicYById.get(n.id) as number
const active = n.status ? n.status === "up" : n.rx + n.tx > 0
if (!active) return
// Add a 5-px gap between each circle and the trunk line so the
// 9-px-wide static lane doesn't visually overlap the node ring.
const path = orthLink(nicX + radNic + 5, y, hostX - radHost - 5, hostY, 14)
// A slave stops at its bond; only a NIC with no bond goes straight
// to the host. Add a 5-px gap between each circle and the trunk
// line so the static lane doesn't visually overlap the node ring.
const bondId = bondOfNic.get(n.id)
const bondY = bondId !== undefined ? bondYById.get(bondId) : undefined
const path = bondY !== undefined
? orthLink(nicX + radNic + 5, y, bondX - radBond - 5, bondY, 14)
: orthLink(nicX + radNic + 5, y, hostX - radHost - 5, hostY, 14)
linksStatic.push(`<path class="nf-link" d="${path}" stroke-width="${TRUNK_WIDTH}" />`)
// Per-NIC pulse speed mirrors the per-guest logic: more traffic
// on this NIC → faster pulse on its line. Each NIC reads
@@ -447,6 +562,17 @@ function renderHorizontal(data: NetworkFlowData, W: number): { svg: string; puls
if (n.tx > NIC_PULSE_MIN_MBPS) linksPulse.push({ d: path, type: "tx", strokeWidth: TRUNK_PULSE_WIDTH, animDur: nicDur, key: `nic-tx-${n.id}` })
})
// Bond → host: one aggregated lane carrying the sum of its slaves.
bonds.forEach((b) => {
const y = bondYById.get(b.id)
if (y === undefined) return
const path = orthLink(bondX + radBond + 5, y, hostX - radHost - 5, hostY, 14)
linksStatic.push(`<path class="nf-link" d="${path}" stroke-width="${TRUNK_WIDTH}" />`)
const dur = durFor(b.rx + b.tx)
if (b.rx > NIC_PULSE_MIN_MBPS) linksPulse.push({ d: path, type: "rx", strokeWidth: TRUNK_PULSE_WIDTH, animDur: dur, key: `bond-rx-${b.id}` })
if (b.tx > NIC_PULSE_MIN_MBPS) linksPulse.push({ d: path, type: "tx", strokeWidth: TRUNK_PULSE_WIDTH, animDur: dur, key: `bond-tx-${b.id}` })
})
// Static svg = lines + nodes. Pulses are returned SEPARATELY as
// data so NetworkFlow can render them as JSX <path>s. That keeps
// each pulse's DOM node stable across re-renders → CSS animations
@@ -473,6 +599,7 @@ function renderVertical(data: NetworkFlowData): { svg: string; pulses: PulseData
const top = activeConsumers(data.consumers)
const bridges = visibleBridges(data.bridges, top)
const host = data.consumers.find((c) => c.kind === "host")
const { bonds, bondOfNic, nics } = resolveBonds(data)
const linksStatic: string[] = []
const linksPulse: PulseData[] = []
@@ -496,6 +623,17 @@ function renderVertical(data: NetworkFlowData): { svg: string; pulses: PulseData
const RAD_NIC = 22
const RAD_BRIDGE = 24
const RAD_GUEST = 20
const RAD_BOND = 22
// Extra rows the bond level needs when at least one bond exists:
// the gap between where the NIC lines start and the bond circle, and
// the space its three texts (name / mode / rate) occupy underneath.
const BOND_ROW_GAP = 48
const BOND_TEXT_H = 44
// Same split the NICs use (see NIC_PATH_START_OFFSET / NIC_VERTICAL_LEG):
// the link starts BELOW the rate text instead of on top of it, then runs
// a visible stretch before curving toward the host.
const BOND_PATH_START_OFFSET = BOND_TEXT_H + 14
const BOND_VERTICAL_LEG = 40
const NIC_TOP_Y = 6
const NIC_PITCH_X_PREFERRED = 88
const NIC_LEFT_MARGIN = 8
@@ -563,7 +701,7 @@ function renderVertical(data: NetworkFlowData): { svg: string; pulses: PulseData
// sits directly above the host — straight vertical path, no weird
// S-curve. When the row is too wide to be centred there without
// falling off the canvas, it slides right (or left, clamped).
const nicCount = data.nics.length
const nicCount = nics.length
const fitWidth = W - 2 * (NIC_LEFT_MARGIN + RAD_NIC)
const dynamicPitch = nicCount > 1
? Math.min(NIC_PITCH_X_PREFERRED, fitWidth / (nicCount - 1))
@@ -574,7 +712,7 @@ function renderVertical(data: NetworkFlowData): { svg: string; pulses: PulseData
const idealStart = HOST_X - rowWidth / 2
const startX = Math.max(minStart, Math.min(maxStart, idealStart))
const nicCy = NIC_TOP_Y + RAD_NIC
const nicGeom = data.nics.map((n, i) => ({
const nicGeom = nics.map((n, i) => ({
n, cx: startX + i * dynamicPitch, cy: nicCy, r: RAD_NIC,
}))
@@ -583,14 +721,39 @@ function renderVertical(data: NetworkFlowData): { svg: string; pulses: PulseData
// NIC path (NIC_VERTICAL_LEG) is what makes the line visibly run
// a stretch BEFORE turning into the curve.
const nicPathStartY = nicCy + RAD_NIC + NIC_PATH_START_OFFSET
const convergeY = nicPathStartY + NIC_VERTICAL_LEG
// ─── 1b. Bond row — between the NICs and the convergence row ──
// Each bond sits horizontally at the centroid of its own slaves, so
// its links stay short and don't cross a neighbouring bond's.
const hasBonds = bonds.length > 0
const bondCy = nicPathStartY + BOND_ROW_GAP + RAD_BOND
const bondGeom = hasBonds
? bonds.map((b) => {
const xs = nicGeom
.filter(({ n }) => bondOfNic.get(n.id) === b.id)
.map(({ cx }) => cx)
return { b, cx: xs.reduce((a, v) => a + v, 0) / xs.length, cy: bondCy, r: RAD_BOND }
})
: []
const bondCxById = new Map(bondGeom.map(({ b, cx }) => [b.id, cx]))
// Slaves bend into their bond one row above the bond circle.
const bondConvergeY = bondCy - RAD_BOND - 20
// With a bond level present the shared convergence row moves below
// the bond's texts; without bonds the geometry is exactly as before.
const convergeY = hasBonds
? bondCy + RAD_BOND + BOND_PATH_START_OFFSET + BOND_VERTICAL_LEG
: nicPathStartY + NIC_VERTICAL_LEG
const hostY = convergeY + HOST_GAP_FROM_CONVERGE + RAD_HOST
const hostTopY = hostY - RAD_HOST - 4
// NIC → host orth+Q via shared convergence row.
// NIC → bond (if enslaved) or NIC → host, both orth+Q.
nicGeom.forEach(({ n, cx, cy, r }) => {
const startX = cx, startY = cy + r + NIC_PATH_START_OFFSET
const pathD = vPath(startX, startY, HOST_X, hostTopY, convergeY)
const bondId = bondOfNic.get(n.id)
const bondCx = bondId !== undefined ? bondCxById.get(bondId) : undefined
const pathD = bondCx !== undefined
? vPath(startX, startY, bondCx, bondCy - RAD_BOND - 4, bondConvergeY)
: vPath(startX, startY, HOST_X, hostTopY, convergeY)
linksStatic.push(`<path class="nf-link" d="${pathD}" stroke-width="2.5" />`)
const active = n.status ? n.status === "up" : n.rx + n.tx > 0
if (active) {
@@ -600,11 +763,30 @@ function renderVertical(data: NetworkFlowData): { svg: string; pulses: PulseData
}
const isDown = n.status === "down"
const color = isDown ? COLORS.gray : COLORS.nic
nodes.push(`<g data-node-id="${n.id}" data-node-kind="nic" style="cursor:pointer;opacity:${isDown ? 0.45 : 1}">
const opacity = isDown ? 0.45 : n.role === "standby" ? 0.72 : 1
nodes.push(`<g data-node-id="${n.id}" data-node-kind="nic" style="cursor:pointer;opacity:${opacity}">
<circle class="nf-circle" cx="${cx}" cy="${cy}" r="${r}" stroke="${color}" />
${svgIcon("nic", cx, cy, 13, color)}
<text class="nf-label" x="${cx}" y="${cy + r + LABEL_OFFSET_Y}" text-anchor="middle" style="font-size:12.5px">${n.id}</text>
<text class="nf-sub" x="${cx}" y="${cy + r + SUB_OFFSET_Y}" text-anchor="middle" style="font-size:12.5px">${isDown ? "down" : n.link}</text>
<text class="nf-sub" x="${cx}" y="${cy + r + SUB_OFFSET_Y}" text-anchor="middle" style="font-size:11px">${nicSubLabel(n)}</text>
</g>`)
})
// Bond node + its aggregated link down to the host.
bondGeom.forEach(({ b, cx, cy, r }) => {
const pathD = vPath(cx, cy + r + BOND_PATH_START_OFFSET, HOST_X, hostTopY, convergeY)
linksStatic.push(`<path class="nf-link" d="${pathD}" stroke-width="2.5" />`)
const rate = b.rx + b.tx
if (b.rx > 0.001) linksPulse.push({ d: pathD, type: "rx", strokeWidth: 3, key: `m-bond-rx-${b.id}`, rate })
if (b.tx > 0.001) linksPulse.push({ d: pathD, type: "tx", strokeWidth: 3, key: `m-bond-tx-${b.id}`, rate })
const isDown = b.status === "down"
const color = isDown ? COLORS.gray : COLORS.bond
nodes.push(`<g data-node-id="${b.id}" data-node-kind="bond" style="cursor:pointer;opacity:${isDown ? 0.45 : 1}">
<circle class="nf-circle" cx="${cx}" cy="${cy}" r="${r}" stroke="${color}" />
${svgIcon("bond", cx, cy, 13, color)}
<text class="nf-label" x="${cx}" y="${cy + r + LABEL_OFFSET_Y}" text-anchor="middle" style="font-size:12.5px">${b.id}</text>
<text class="nf-sub" x="${cx}" y="${cy + r + SUB_OFFSET_Y}" text-anchor="middle" style="font-size:11px">${b.mode || "bond"}</text>
<text class="nf-sub" x="${cx}" y="${cy + r + BOND_TEXT_H}" text-anchor="middle" style="font-size:11px">${fmt(rate)}</text>
</g>`)
})
@@ -752,7 +934,7 @@ export function NetworkFlow({
// Fires when the user taps/clicks any circle in the diagram. The
// parent component looks up the matching NetworkInterface and
// opens the per-interface details modal.
onNodeClick?: (name: string, kind: "nic" | "host" | "bridge" | "lxc" | "vm") => void
onNodeClick?: (name: string, kind: "nic" | "host" | "bond" | "bridge" | "lxc" | "vm") => void
}) {
const ref = useRef<HTMLDivElement>(null)
const [width, setWidth] = useState(1320)
@@ -785,14 +967,20 @@ export function NetworkFlow({
return SB.find((b) => b >= raw) ?? 2.5
}
const sig = (r: number) => (r > 0.005 ? 1 : 0)
// bond + role are part of the signature so a failover (active ↔
// standby, or a slave leaving the bond) redraws the diagram on the
// very next poll instead of being masked by the rate bucketing.
const nics = data.nics.map((n) =>
`${n.id}:${n.status || ""}:${bucket(n.rx)}:${bucket(n.tx)}:${sig(n.rx)}:${sig(n.tx)}`
`${n.id}:${n.status || ""}:${n.bond || ""}:${n.role || ""}:${bucket(n.rx)}:${bucket(n.tx)}:${sig(n.rx)}:${sig(n.tx)}`
).join("|")
const bonds = (data.bonds || []).map((b) =>
`${b.id}:${b.mode || ""}:${b.status || ""}:${bucket(b.rx)}:${bucket(b.tx)}:${sig(b.rx)}:${sig(b.tx)}`
).join("|")
const guests = data.consumers.map((c) =>
`${c.id}:${c.bridge}:${c.kind}:${c.offline ? 1 : 0}:${bucket(c.rx)}:${bucket(c.tx)}:${sig(c.rx)}:${sig(c.tx)}`
).join("|")
const bridges = data.bridges.map((b) => `${b.id}:${b.parent || ""}`).join("|")
return `${mode}|${width}|${nics}||${guests}||${bridges}`
return `${mode}|${width}|${nics}||${bonds}||${guests}||${bridges}`
}, [data, mode, width])
const { svgContent, pulses, viewBox, height } = useMemo(() => {
@@ -896,7 +1084,7 @@ export function NetworkFlow({
if (!hit) return
const id = hit.getAttribute("data-node-id") || ""
const kind = (hit.getAttribute("data-node-kind") || "") as
"nic" | "host" | "bridge" | "lxc" | "vm"
"nic" | "host" | "bond" | "bridge" | "lxc" | "vm"
if (id && kind) onNodeClick(id, kind)
}}
>

View File

@@ -18,6 +18,9 @@ interface NetworkData {
interfaces: NetworkInterface[]
physical_interfaces?: NetworkInterface[]
bridge_interfaces?: NetworkInterface[]
// Bond masters. Also present inside `interfaces` for backward
// compatibility; this list is what the topology diagram consumes.
bond_interfaces?: NetworkInterface[]
vm_lxc_interfaces?: NetworkInterface[]
traffic: {
bytes_sent: number
@@ -79,11 +82,26 @@ interface NetworkInterface {
// operator can see "this NIC → vmbr0" at a glance.
used_by_bridges?: string[]
bond_mode?: string
// Kernel's human-readable mode, e.g. "fault-tolerance (active-backup)".
// bond_mode holds the short form ("active-backup") that matches
// /etc/network/interfaces and the Proxmox UI.
bond_mode_detail?: string | null
bond_slaves?: string[]
bond_active_slave?: string | null
// True only for modes where a slave really sits idle (active-backup).
bond_supports_failover?: boolean
bond_slave_status?: Record<string, string>
// Set on a physical NIC that is enslaved to a bond.
bond_master?: string
bond_role?: "active" | "standby" | "member"
bond_link?: string
// Master device resolved from /sys/class/net/<iface>/master — the
// bridge for a guest tap, the bond for a slave NIC.
bridge_owner?: string
bridge_members?: string[]
bridge_physical_interface?: string
bridge_bond_slaves?: string[]
bridge_vlan_interface?: string | null
packet_loss_in?: number
packet_loss_out?: number
vmid?: number
@@ -509,6 +527,7 @@ export function NetworkMetrics() {
const allInterfaces = [
...(networkData.physical_interfaces || []),
...(networkData.bond_interfaces || []),
...(networkData.bridge_interfaces || []),
...(networkData.vm_lxc_interfaces || []),
]
@@ -788,6 +807,7 @@ export function NetworkMetrics() {
const toMBps = (bps?: number) => (bps || 0) / (1024 * 1024)
const allIfaces = [
...(networkData.physical_interfaces || []),
...(networkData.bond_interfaces || []),
...(networkData.bridge_interfaces || []),
...(networkData.vm_lxc_interfaces || []),
]
@@ -797,7 +817,24 @@ export function NetworkMetrics() {
link: formatSpeed(p.speed),
rx: toMBps(p.rx_Bps),
tx: toMBps(p.tx_Bps),
status: (p.status || "").toLowerCase() === "up" ? "up" : "down",
// A slave whose MII status is down has no carrier even though
// the interface itself stays administratively up — the bond
// driver's view is the accurate one here.
status:
p.bond_link === "down"
? "down"
: (p.status || "").toLowerCase() === "up"
? "up"
: "down",
bond: p.bond_master,
role: p.bond_role,
})),
bonds: (networkData.bond_interfaces || []).map((b) => ({
id: b.name,
mode: b.bond_mode && b.bond_mode !== "unknown" ? b.bond_mode : undefined,
rx: toMBps(b.rx_Bps),
tx: toMBps(b.tx_Bps),
status: (b.status || "").toLowerCase() === "up" ? "up" : "down",
})),
bridges: (networkData.bridge_interfaces || []).map((b) => ({
id: b.name,
@@ -918,24 +955,6 @@ export function NetworkMetrics() {
{interface_.bridge_physical_interface && (
<div className="text-sm text-blue-500 font-medium flex items-center gap-1 flex-wrap break-all">
{interface_.bridge_physical_interface}
{interface_.bridge_physical_interface.startsWith("bond") &&
networkData.physical_interfaces && (
<>
{(() => {
const bondInterface = networkData.physical_interfaces.find(
(iface) => iface.name === interface_.bridge_physical_interface,
)
if (bondInterface?.bond_slaves && bondInterface.bond_slaves.length > 0) {
return (
<span className="text-muted-foreground text-xs break-all">
({bondInterface.bond_slaves.join(", ")})
</span>
)
}
return null
})()}
</>
)}
{interface_.bridge_bond_slaves && interface_.bridge_bond_slaves.length > 0 && (
<span className="text-muted-foreground text-xs break-all">
({interface_.bridge_bond_slaves.join(", ")})
@@ -1125,6 +1144,7 @@ export function NetworkMetrics() {
const currentInterfaceData = modalNetworkData
? [
...(modalNetworkData.physical_interfaces || []),
...(modalNetworkData.bond_interfaces || []),
...(modalNetworkData.bridge_interfaces || []),
...(modalNetworkData.vm_lxc_interfaces || []),
].find((iface) => iface.name === selectedInterface.name)
@@ -1154,35 +1174,10 @@ export function NetworkMetrics() {
<div className="font-medium text-blue-500 text-lg break-all">
{displayInterface.bridge_physical_interface}
</div>
{displayInterface.bridge_physical_interface.startsWith("bond") &&
modalNetworkData?.physical_interfaces && (
<>
{(() => {
const bondInterface = modalNetworkData.physical_interfaces.find(
(iface) => iface.name === displayInterface.bridge_physical_interface,
)
if (bondInterface?.bond_slaves && bondInterface.bond_slaves.length > 0) {
return (
<div className="mt-2">
<div className="text-sm text-muted-foreground mb-2">Bond Members</div>
<div className="flex flex-wrap gap-2">
{bondInterface.bond_slaves.map((slave, idx) => (
<Badge
key={idx}
variant="outline"
className="bg-purple-500/10 text-purple-500 border-purple-500/20"
>
{slave}
</Badge>
))}
</div>
</div>
)
}
return null
})()}
</>
)}
{/* Slaves come from the bridge's own payload
(bridge_bond_slaves); the bond master is not
part of physical_interfaces, so looking it up
there never matched. */}
{displayInterface.bridge_bond_slaves && displayInterface.bridge_bond_slaves.length > 0 && (
<div className="mt-2">
<div className="text-sm text-muted-foreground mb-2">Bond Members</div>
@@ -1418,26 +1413,53 @@ export function NetworkMetrics() {
<div className="space-y-3">
<div>
<div className="text-sm text-muted-foreground">Bonding Mode</div>
<div className="font-medium">{displayInterface.bond_mode || "Unknown"}</div>
<div className="font-medium">
{displayInterface.bond_mode || "Unknown"}
{displayInterface.bond_mode_detail &&
displayInterface.bond_mode_detail !== displayInterface.bond_mode && (
<span className="text-muted-foreground font-normal">
{" "}
({displayInterface.bond_mode_detail})
</span>
)}
</div>
</div>
{displayInterface.bond_active_slave && (
<div>
<div className="text-sm text-muted-foreground">Active Slave</div>
<div className="text-sm text-muted-foreground">
{displayInterface.bond_supports_failover ? "Active Slave" : "Primary Slave"}
</div>
<div className="font-medium">{displayInterface.bond_active_slave}</div>
</div>
)}
<div>
<div className="text-sm text-muted-foreground mb-2">Slave Interfaces</div>
<div className="flex flex-wrap gap-2">
{displayInterface.bond_slaves.map((slave, idx) => (
<Badge
key={idx}
variant="outline"
className="bg-purple-500/10 text-purple-500 border-purple-500/20"
>
{slave}
</Badge>
))}
{displayInterface.bond_slaves.map((slave, idx) => {
// Only active-backup has a real standby. In every
// other mode all slaves transmit, so we just show
// the link state.
const link = displayInterface.bond_slave_status?.[slave]
const isDown = link === "down"
const role = isDown
? "down"
: displayInterface.bond_supports_failover
? slave === displayInterface.bond_active_slave
? "active"
: "standby"
: null
const tone = isDown
? "bg-red-500/10 text-red-500 border-red-500/20"
: role === "standby"
? "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
: "bg-purple-500/10 text-purple-500 border-purple-500/20"
return (
<Badge key={idx} variant="outline" className={tone}>
{slave}
{role && <span className="ml-1 opacity-70">· {role}</span>}
</Badge>
)
})}
</div>
</div>
</div>

View File

@@ -2903,9 +2903,10 @@ def get_system_disks():
pass
try:
# 4. Check LVM physical volumes
# 4. Check LVM physical volumes. `--devices` (added when a disk
# is parked) keeps pvs from scanning — and waking — standby HDDs.
result = subprocess.run(
['pvs', '--noheadings', '-o', 'pv_name,vg_name'],
['pvs', *_lvm_device_args(), '--noheadings', '-o', 'pv_name,vg_name'],
capture_output=True, text=True, timeout=5
)
if result.returncode == 0:
@@ -3653,11 +3654,98 @@ def _smart_data_useful(d: dict) -> bool:
)
_standby_cache: dict[str, tuple] = {}
_STANDBY_CACHE_TTL = 15
def _hdd_in_standby(disk_name: str) -> bool:
"""True if `disk_name` is a spinning disk currently parked.
The check is smartctl's CHECK POWER MODE probe (`-n standby`), which
the drive answers without spinning up. Restricted to rotational,
non-NVMe devices so SSDs and NVMe — which have no standby state —
skip the extra probe. Any error falls through to False so the caller
behaves exactly as before. Result cached briefly so one /api/storage
poll doesn't re-probe the same disk from several call sites. See
issue #232.
"""
if disk_name.startswith('nvme'):
return False
now = time.time()
hit = _standby_cache.get(disk_name)
if hit and now - hit[0] < _STANDBY_CACHE_TTL:
return hit[1]
try:
with open(f'/sys/block/{disk_name}/queue/rotational') as f:
if f.read().strip() != '1':
_standby_cache[disk_name] = (now, False)
return False
except OSError:
return False
parked = False
try:
r = subprocess.run(
['smartctl', '-n', 'standby', '-i', f'/dev/{disk_name}'],
capture_output=True, text=True, timeout=_SMART_TIMEOUT,
)
parked = r.returncode == 2
except (subprocess.SubprocessError, OSError):
parked = False
_standby_cache[disk_name] = (now, parked)
return parked
def _lvm_device_args() -> list:
"""Build the `--devices` argument that keeps an LVM tool (pvs/lvs/
vgs) off parked disks.
LVM tools scan every block device by default, which spins up a
standby HDD (issue #232). When any rotational disk is asleep, hand
LVM an explicit list of every block node EXCEPT those belonging to a
parked disk, so it never opens the sleeping drives. Each partition
must be listed too — `--devices /dev/sda` alone does not cover a PV
on /dev/sda3. Returns [] when nothing is parked, so hosts without
standby disks keep the plain pvs/lvs/vgs call unchanged.
"""
try:
r = subprocess.run(
['lsblk', '-rno', 'NAME,TYPE,PKNAME'],
capture_output=True, text=True, timeout=5)
if r.returncode != 0:
return []
nodes = []
for line in r.stdout.strip().split('\n'):
parts = line.split()
if len(parts) < 2 or parts[1] not in ('disk', 'part'):
continue
name = parts[0]
base = name if parts[1] == 'disk' else (parts[2] if len(parts) > 2 else name)
nodes.append((name, base))
standby_bases = {
name for name, base in nodes
if name == base and _hdd_in_standby(name)
}
if not standby_bases:
return []
devices = [f'/dev/{name}' for name, base in nodes if base not in standby_bases]
return ['--devices', ','.join(devices)] if devices else []
except (subprocess.SubprocessError, OSError):
return []
def get_smart_data(disk_name):
"""Cached wrapper around the underlying smartctl probe.
Three short-circuits stack on top of the original 14-variant
Four short-circuits stack on top of the original 14-variant
fallback chain:
0. Standby guard — a parked HDD is left alone. The full `smartctl
-a` probe is a media read that spins the disk back up, so on a
cache miss we first check the power mode (no spin-up) and, if
the drive is asleep, return the last good payload instead of
waking it. This is the fix for issue #232: with a browser on
the dashboard, /api/storage polls often enough that the 30 s
cache kept expiring and re-waking disks that hdparm / hd-idle
had just parked.
1. Result cache — within 30 s of a successful probe, return the
memoised payload immediately. This is the by-far hottest path
because the dashboard polls /api/storage every few seconds.
@@ -3673,6 +3761,17 @@ def get_smart_data(disk_name):
if cached and now - cached[0] < _SMART_RESULT_TTL:
return dict(cached[1])
if _hdd_in_standby(disk_name):
# Keep serving the last known values (temperature blanked, since
# we don't have a fresh one) so the card stays populated while
# the disk sleeps. The Storage view paints its own Standby badge
# from the temperature poller's flag. Don't refresh the cache
# timestamp: the moment the disk is active again we re-probe.
base = dict(cached[1]) if cached else _smart_default_payload()
base['standby'] = True
base['temperature'] = 0
return base
retry_at = _smart_fail_backoff.get(disk_name, 0)
if retry_at > now:
return dict(cached[1]) if cached else _smart_default_payload()
@@ -4712,22 +4811,43 @@ def _ethtool_max_speed(iface_name):
return 0
# A bond master always exposes /sys/class/net/<x>/bonding/. This is the
# kernel's own marker, so it catches bonds whose name isn't "bondN"
# (Proxmox lets you name them freely in /etc/network/interfaces).
def _is_bond_master(iface_name):
try:
return os.path.isdir(f'/sys/class/net/{iface_name}/bonding')
except Exception:
return False
# A physical NIC has a backing device symlink. Covers PCI, USB and any
# renamed NIC (e.g. a udev-renamed "nic0"), which a name-prefix check
# would miss.
def _is_physical_dev(iface_name):
try:
return os.path.exists(f'/sys/class/net/{iface_name}/device')
except Exception:
return False
def get_interface_type(interface_name):
"""Detect the type of network interface"""
try:
# Skip loopback
if interface_name == 'lo':
return 'skip'
if interface_name.startswith(('veth', 'tap')):
return 'vm_lxc'
# Skip other virtual interfaces
if interface_name.startswith(('tun', 'vnet', 'docker', 'virbr')):
return 'skip'
# Check if it's a bond
if interface_name.startswith('bond'):
# Check if it's a bond — sysfs first (name-agnostic), then the
# "bondN" convention as fallback when /sys isn't readable.
if _is_bond_master(interface_name) or interface_name.startswith('bond'):
return 'bond'
# Check if it's a bridge (but not virbr which we skip above)
@@ -4756,36 +4876,75 @@ def get_interface_type(interface_name):
pass
return 'skip'
# Only active-backup (and the *-alb/tlb primary) have a real
# active/standby distinction. In 802.3ad and balance-rr/xor EVERY slave
# transmits, so labelling the others "standby" would be wrong — hence
# the role is only assigned for active-backup.
_BOND_FAILOVER_MODES = ('active-backup',)
def get_bond_info(bond_name):
"""Get detailed information about a bonding interface"""
"""Get detailed information about a bonding interface.
sysfs is the primary source (short mode name, live active_slave);
/proc/net/bonding/<bond> is parsed too because it's the only place
that reports per-slave MII status, which tells a genuinely dead
link apart from a healthy standby.
"""
bond_info = {
'mode': 'unknown',
'mode': 'unknown', # short kernel name, e.g. "active-backup"
'mode_detail': None, # human name, e.g. "fault-tolerance (active-backup)"
'slaves': [],
'active_slave': None
'active_slave': None,
'slave_status': {}, # {slave: 'up'|'down'}
'supports_failover': False,
}
try:
# ── sysfs (authoritative, name-agnostic) ──────────────
mode = _read_sysfs(f'/sys/class/net/{bond_name}/bonding/mode')
if mode:
# "balance-alb 6" → "balance-alb"
bond_info['mode'] = mode.split()[0]
slaves = _read_sysfs(f'/sys/class/net/{bond_name}/bonding/slaves')
if slaves:
bond_info['slaves'] = slaves.split()
active = _read_sysfs(f'/sys/class/net/{bond_name}/bonding/active_slave')
if active:
bond_info['active_slave'] = active
# ── /proc/net/bonding (per-slave MII + pretty mode) ───
bond_file = f'/proc/net/bonding/{bond_name}'
if os.path.exists(bond_file):
with open(bond_file, 'r') as f:
content = f.read()
# Parse bonding mode
for line in content.split('\n'):
if 'Bonding Mode:' in line:
bond_info['mode'] = line.split(':', 1)[1].strip()
elif 'Slave Interface:' in line:
slave_name = line.split(':', 1)[1].strip()
bond_info['slaves'].append(slave_name)
elif 'Currently Active Slave:' in line:
bond_info['active_slave'] = line.split(':', 1)[1].strip()
# print(f"[v0] Bond {bond_name} info: mode={bond_info['mode']}, slaves={bond_info['slaves']}")
pass
current_slave = None
proc_slaves = []
for line in content.split('\n'):
if 'Bonding Mode:' in line:
bond_info['mode_detail'] = line.split(':', 1)[1].strip()
elif 'Slave Interface:' in line:
current_slave = line.split(':', 1)[1].strip()
proc_slaves.append(current_slave)
elif 'Currently Active Slave:' in line:
val = line.split(':', 1)[1].strip()
if val and val.lower() != 'none' and not bond_info['active_slave']:
bond_info['active_slave'] = val
elif 'MII Status:' in line and current_slave:
bond_info['slave_status'][current_slave] = line.split(':', 1)[1].strip().lower()
# Fallbacks when sysfs was unreadable.
if not bond_info['slaves']:
bond_info['slaves'] = proc_slaves
if bond_info['mode'] == 'unknown' and bond_info['mode_detail']:
bond_info['mode'] = bond_info['mode_detail']
bond_info['supports_failover'] = bond_info['mode'] in _BOND_FAILOVER_MODES
except Exception as e:
# print(f"[v0] Error reading bond info for {bond_name}: {e}")
pass
return bond_info
def get_bridge_info(bridge_name):
@@ -4795,7 +4954,10 @@ def get_bridge_info(bridge_name):
'physical_interface': None,
'physical_duplex': 'unknown', # Added physical_duplex field
# Added bond_slaves to show physical interfaces
'bond_slaves': []
'bond_slaves': [],
# Set when the bridge port is a VLAN sub-interface (bond0.10);
# physical_interface then holds the base device (bond0).
'vlan_interface': None
}
try:
@@ -4805,19 +4967,32 @@ def get_bridge_info(bridge_name):
members = os.listdir(brif_path)
bridge_info['members'] = members
for member in members:
# Guest taps and firewall veth pairs are never the bridge's
# uplink — skipping them up front means the uplink is found
# regardless of the (arbitrary) order listdir returns.
uplinks = [m for m in members
if not m.startswith(('veth', 'tap', 'fwpr', 'fwln'))]
for member in uplinks:
# A VLAN sub-interface (bond0.10 / eno1.10) is just a tag
# on top of the real uplink — resolve to the base device
# so the topology still shows the bond or NIC underneath.
base = member.split('.', 1)[0] if '.' in member else member
if base != member:
bridge_info['vlan_interface'] = member
# Check if member is a bond first
if member.startswith('bond'):
bridge_info['physical_interface'] = member
# print(f"[v0] Bridge {bridge_name} connected to bond: {member}")
if _is_bond_master(base) or base.startswith('bond'):
bridge_info['physical_interface'] = base
# print(f"[v0] Bridge {bridge_name} connected to bond: {base}")
pass
bond_info = get_bond_info(member)
bond_info = get_bond_info(base)
if bond_info['slaves']:
bridge_info['bond_slaves'] = bond_info['slaves']
# print(f"[v0] Bond {member} slaves: {bond_info['slaves']}")
# print(f"[v0] Bond {base} slaves: {bond_info['slaves']}")
pass
# Get duplex from bond's active slave
if bond_info['active_slave']:
try:
@@ -4831,17 +5006,19 @@ def get_bridge_info(bridge_name):
# print(f"[v0] Error getting duplex for bond slave {bond_info['active_slave']}: {e}")
pass
break
# Check if member is a physical interface
elif member.startswith(('enp', 'eth', 'eno', 'ens', 'wlan', 'wlp')):
bridge_info['physical_interface'] = member
# print(f"[v0] Bridge {bridge_name} physical interface: {member}")
# Check if member is a physical interface. The device
# symlink is the reliable marker — a NIC renamed to
# "nic0" has no recognisable prefix but still has it.
elif _is_physical_dev(base) or base.startswith(('enp', 'eth', 'eno', 'ens', 'wlan', 'wlp')):
bridge_info['physical_interface'] = base
# print(f"[v0] Bridge {bridge_name} physical interface: {base}")
pass
# Get duplex from physical interface
try:
net_if_stats = psutil.net_if_stats()
if member in net_if_stats:
stats = net_if_stats[member]
if base in net_if_stats:
stats = net_if_stats[base]
bridge_info['physical_duplex'] = 'full' if stats.duplex == 2 else 'half' if stats.duplex == 1 else 'unknown'
# print(f"[v0] Physical interface {member} duplex: {bridge_info['physical_duplex']}")
pass
@@ -4866,6 +5043,10 @@ def get_network_info():
'interfaces': [],
'physical_interfaces': [], # Added separate list for physical interfaces
'bridge_interfaces': [], # Added separate list for bridge interfaces
# Bond masters, so the Network Flow diagram can draw them as
# the intermediate node between their slaves and the host.
# They stay in `interfaces` too, for backward compatibility.
'bond_interfaces': [],
'vm_lxc_interfaces': [],
'traffic': {'bytes_sent': 0, 'bytes_recv': 0, 'packets_sent': 0, 'packets_recv': 0},
# 'hostname': socket.gethostname(),
@@ -5043,9 +5224,12 @@ def get_network_info():
if interface_type == 'bond':
bond_info = get_bond_info(interface_name)
interface_info['bond_mode'] = bond_info['mode']
interface_info['bond_mode_detail'] = bond_info['mode_detail']
interface_info['bond_slaves'] = bond_info['slaves']
interface_info['bond_active_slave'] = bond_info['active_slave']
interface_info['bond_supports_failover'] = bond_info['supports_failover']
interface_info['bond_slave_status'] = bond_info['slave_status']
if interface_type == 'bridge':
bridge_info = get_bridge_info(interface_name)
interface_info['bridge_members'] = bridge_info['members']
@@ -5065,7 +5249,9 @@ def get_network_info():
else:
# Keep other types in the general interfaces list for backward compatibility
network_data['interfaces'].append(interface_info)
if interface_type == 'bond':
network_data['bond_interfaces'].append(interface_info)
network_data['physical_active_count'] = physical_active_count
network_data['physical_total_count'] = physical_total_count
network_data['bridge_active_count'] = bridge_active_count
@@ -5073,6 +5259,35 @@ def get_network_info():
network_data['vm_lxc_active_count'] = vm_lxc_active_count
network_data['vm_lxc_total_count'] = vm_lxc_total_count
# Tag every bond slave with its master and its CURRENT role. The
# role is read fresh on each poll (never pinned to a NIC), so a
# failover flips active/standby on the next refresh.
# active/standby → only for active-backup, the one mode where
# a slave really sits idle.
# member → LACP, balance-rr/xor/tlb/alb: every slave
# transmits, so no standby label.
# `bond_link` comes from /proc/net/bonding's per-slave MII status
# and distinguishes a dead link from a healthy standby.
slave_meta = {}
for bond in network_data['bond_interfaces']:
failover = bond.get('bond_supports_failover')
active = bond.get('bond_active_slave')
statuses = bond.get('bond_slave_status') or {}
for slave in bond.get('bond_slaves') or []:
if failover:
role = 'active' if slave == active else 'standby'
else:
role = 'member'
slave_meta[slave] = {
'bond_master': bond['name'],
'bond_role': role,
'bond_link': statuses.get(slave, 'unknown'),
}
for phy in network_data['physical_interfaces']:
meta = slave_meta.get(phy['name'])
if meta:
phy.update(meta)
# Map physical NICs ↔ bridges using them. For a bridge with a
# direct NIC parent, that NIC gets the bridge in its list. For
# bridges sitting on top of a bond, every bond slave gets the
@@ -5135,6 +5350,7 @@ def get_network_info():
'interfaces': [],
'physical_interfaces': [],
'bridge_interfaces': [],
'bond_interfaces': [],
'vm_lxc_interfaces': [],
'traffic': {'bytes_sent': 0, 'bytes_recv': 0, 'packets_sent': 0, 'packets_recv': 0},
'active_count': 0,
@@ -7598,29 +7814,23 @@ def _get_hardware_info_uncached():
except:
pass
# Parse SATA version from smartctl output
# SATA version + form factor from one `smartctl -i`.
# `-n standby` so a parked HDD isn't handed even an
# IDENTIFY command (issue #232) — exit code 2 leaves
# both fields None and the disk asleep. Merged from
# two separate calls that parsed the same output.
sata_version = None
form_factor = None
try:
result_smart = subprocess.run(['smartctl', '-i', f'/dev/{disk_name}'],
capture_output=True, text=True, timeout=5)
result_smart = subprocess.run(
['smartctl', '-n', 'standby', '-i', f'/dev/{disk_name}'],
capture_output=True, text=True, timeout=5)
if result_smart.returncode == 0:
for line in result_smart.stdout.split('\n'):
if 'SATA Version is:' in line:
sata_version = line.split(':', 1)[1].strip()
break
except:
pass
# Parse form factor from smartctl output
form_factor = None
try:
result_smart = subprocess.run(['smartctl', '-i', f'/dev/{disk_name}'],
capture_output=True, text=True, timeout=5)
if result_smart.returncode == 0:
for line in result_smart.stdout.split('\n'):
if 'Form Factor:' in line:
elif 'Form Factor:' in line:
form_factor = line.split(':', 1)[1].strip()
break
except:
pass
@@ -12702,6 +12912,21 @@ _BACKUP_DEFAULT_DUMP_DIRS = ('/var/lib/vz/dump',)
_BACKUP_FILENAME_RE = re.compile(r'^([A-Za-z0-9._-]+)-(\d{8}_\d{6})\.tar(\.zst|\.gz)?$')
def _shell_env_line(line: str) -> str:
"""Turn a raw ``KEY=value`` pair into a shell-safe assignment.
The job .env is `source`d by run_scheduled_backup.sh as root, so an
unquoted value is not just cosmetic: globs and spaces split the
assignment into a command, and `$(...)` or backticks in a password
run at source time. backup_scheduler.sh quotes with `printf %q`;
this is the same guarantee for the lines written from the API.
"""
if not line or line.startswith('#') or '=' not in line:
return line
key, val = line.split('=', 1)
return f'{key}={shlex.quote(val)}'
def _parse_job_env(file_path: str) -> dict:
"""Parse a /var/lib/proxmenux/backup-jobs/*.env file (shell KEY=value
format with optional quoting) into a Python dict. Returns {} on any
@@ -12716,9 +12941,22 @@ def _parse_job_env(file_path: str) -> dict:
key, val = line.split('=', 1)
key = key.strip()
val = val.strip()
# Strip shell quoting if balanced
if len(val) >= 2 and val[0] == val[-1] and val[0] in ('"', "'"):
val = val[1:-1]
# Unquote the way the shell would — `shlex` handles both
# writers, `printf %q` from the TUI and shlex.quote()
# from the API. Exactly one token means the value was
# properly quoted; anything else is a legacy unquoted
# multi-word value already on disk, kept verbatim rather
# than truncated to its first word.
try:
parts = shlex.split(val)
if len(parts) == 1:
val = parts[0]
elif not parts:
val = ''
except ValueError:
# Unbalanced quoting — fall back to a naive strip.
if len(val) >= 2 and val[0] == val[-1] and val[0] in ('"', "'"):
val = val[1:-1]
out[key] = val
except OSError:
pass
@@ -13412,7 +13650,10 @@ def _list_pbs_destinations() -> list:
repos.append({
'name': name,
'repository': repo,
'fingerprint': None,
# A manual destination never reaches
# storage.cfg, so the sidecar is the only
# place its fingerprint exists.
'fingerprint': _resolve_pbs_fingerprint(name) or None,
'source': 'manual',
})
seen.add((name, repo))
@@ -14947,6 +15188,29 @@ def _resolve_pbs_password(repo_name: str) -> str:
return ''
def _fingerprint_for_repository(repo: str) -> str:
"""Resolve the fingerprint from a `<user>@<server>:<datastore>`
repository string rather than a destination *name*.
Job payloads carry the repository, so this maps it back to the
destination before delegating to `_resolve_pbs_fingerprint`. A job
written without a `PBS_FINGERPRINT=` line fails on every run against
a self-signed PBS — the client refuses the connection instead of
trusting the certificate.
"""
if not repo:
return ''
try:
for dest in _list_pbs_destinations():
if dest.get('repository') == repo:
fp = dest.get('fingerprint') or _resolve_pbs_fingerprint(dest.get('name') or '')
if fp:
return fp
except Exception:
pass
return ''
def _resolve_pbs_fingerprint(repo_name: str) -> str:
"""Return the PBS fingerprint for `repo_name`. Symmetric to
_resolve_pbs_password: first the proxmenux manual sidecar
@@ -15343,6 +15607,8 @@ def api_host_backups_job_create():
import socket
pbs_backup_id = f'hostcfg-{socket.gethostname()}'
pbs_fingerprint = (payload.get('pbs_fingerprint') or '').strip()
if not pbs_fingerprint:
pbs_fingerprint = _fingerprint_for_repository(pbs_repo)
# Client-side encryption. Three modes:
# - "none" — no --keyfile flag, plain backup
# - "new" — generate a fresh per-host keyfile (default,
@@ -15448,7 +15714,7 @@ def api_host_backups_job_create():
os.makedirs(_BACKUP_JOBS_DIR, exist_ok=True)
with open(env_file, 'w') as f:
for ln in lines:
f.write(ln + '\n')
f.write(_shell_env_line(ln) + '\n')
os.chmod(env_file, 0o600)
with open(f'{_BACKUP_JOBS_DIR}/{job_id}.paths', 'w') as f:
for p in resolved_paths:
@@ -15832,6 +16098,8 @@ def api_host_backups_job_update(job_id):
import socket
pbs_backup_id = f'hostcfg-{socket.gethostname()}'
pbs_fingerprint = (payload.get('pbs_fingerprint') or existing.get('PBS_FINGERPRINT') or '').strip()
if not pbs_fingerprint:
pbs_fingerprint = _fingerprint_for_repository(pbs_repo)
# Encryption: matches job-create + manual-run. The Web sends
# `pbs_encrypt_mode` ("none" / "new" / "existing"); older
# clients may still send the legacy `pbs_encrypt` bool.
@@ -15909,7 +16177,7 @@ def api_host_backups_job_update(job_id):
tmp_env = f'{env_file}.tmp.{os.getpid()}'
with open(tmp_env, 'w') as f:
for ln in lines:
f.write(ln + '\n')
f.write(_shell_env_line(ln) + '\n')
os.chmod(tmp_env, 0o600)
os.replace(tmp_env, env_file)
paths_file = f'{_BACKUP_JOBS_DIR}/{job_id}.paths'
@@ -16044,6 +16312,8 @@ def api_host_backups_manual_run():
# form when they need a separate retention bucket.
pbs_backup_id = f'hostcfg-{socket.gethostname()}'
pbs_fingerprint = (payload.get('pbs_fingerprint') or '').strip()
if not pbs_fingerprint:
pbs_fingerprint = _fingerprint_for_repository(pbs_repo)
# Encryption mode: matches the job-create endpoint exactly.
# Reads the modern `pbs_encrypt_mode` string ("none"/"new"/
# "existing") first, falls back to legacy `pbs_encrypt` bool
@@ -16111,7 +16381,7 @@ def api_host_backups_manual_run():
os.makedirs(_BACKUP_JOBS_DIR, exist_ok=True)
with open(env_file, 'w') as f:
for ln in lines:
f.write(ln + '\n')
f.write(_shell_env_line(ln) + '\n')
os.chmod(env_file, 0o600)
with open(paths_file, 'w') as f:
for p in resolved_paths:

View File

@@ -115,6 +115,75 @@ def _disk_base_for_sysfs(name: str) -> str:
return name
_standby_cache: dict = {}
_STANDBY_CACHE_TTL = 15
def _hdd_in_standby(disk_name: str) -> bool:
"""True if `disk_name` is a spinning disk currently parked.
CHECK POWER MODE probe (`smartctl -n standby`) — answered without
spinning the drive up. Rotational, non-NVMe only. Mirrors the helper
in flask_server.py; see issue #232.
"""
base = _disk_base_for_sysfs(disk_name)
if base.startswith('nvme'):
return False
now = time.time()
hit = _standby_cache.get(base)
if hit and now - hit[0] < _STANDBY_CACHE_TTL:
return hit[1]
try:
with open(f'/sys/block/{base}/queue/rotational') as f:
if f.read().strip() != '1':
_standby_cache[base] = (now, False)
return False
except OSError:
return False
parked = False
try:
r = subprocess.run(
['smartctl', '-n', 'standby', '-i', f'/dev/{base}'],
capture_output=True, text=True, timeout=5)
parked = r.returncode == 2
except Exception:
parked = False
_standby_cache[base] = (now, parked)
return parked
def _lvm_device_args() -> list:
"""`--devices` list scoping pvs/lvs/vgs off parked disks so they
aren't spun up by the LVM scan (issue #232). Empty when nothing is
parked, so the plain command runs unchanged. Each partition is
listed too — a whole-disk path alone doesn't cover a PV on a
partition."""
try:
r = subprocess.run(
['lsblk', '-rno', 'NAME,TYPE,PKNAME'],
capture_output=True, text=True, timeout=5)
if r.returncode != 0:
return []
nodes = []
for line in r.stdout.strip().split('\n'):
parts = line.split()
if len(parts) < 2 or parts[1] not in ('disk', 'part'):
continue
name = parts[0]
base = name if parts[1] == 'disk' else (parts[2] if len(parts) > 2 else name)
nodes.append((name, base))
standby_bases = {
name for name, base in nodes
if name == base and _hdd_in_standby(name)
}
if not standby_bases:
return []
devices = [f'/dev/{name}' for name, base in nodes if base not in standby_bases]
return ['--devices', ','.join(devices)] if devices else []
except Exception:
return []
def _is_disk_removable(disk_name: str) -> bool:
"""True if `/sys/block/<disk>/removable` reads 1. USB-attached storage."""
try:
@@ -2199,8 +2268,10 @@ class HealthMonitor:
if result_which.returncode != 0:
return {'status': 'OK'} # LVM not installed
# `--devices` keeps lvs from scanning parked HDDs (issue #232).
dev_args = _lvm_device_args()
result = subprocess.run(
['lvs', '--noheadings', '--options', 'lv_name,vg_name,lv_attr'],
['lvs', *dev_args, '--noheadings', '--options', 'lv_name,vg_name,lv_attr'],
capture_output=True,
text=True,
timeout=3
@@ -2224,7 +2295,7 @@ class HealthMonitor:
if not volumes:
# Check if any VGs exist to determine if LVM is truly unconfigured or just inactive
vg_result = subprocess.run(
['vgs', '--noheadings', '--options', 'vg_name'],
['vgs', *dev_args, '--noheadings', '--options', 'vg_name'],
capture_output=True,
text=True,
timeout=3

View File

@@ -48,6 +48,9 @@ _FN_DEF_RE = re.compile(r"^(?P<name>[a-zA-Z_][a-zA-Z0-9_]*)\s*\(\)\s*\{\s*$")
_VERSION_RE = re.compile(r'local\s+FUNC_VERSION\s*=\s*"([0-9]+(?:\.[0-9]+)+)"')
_DESC_RE = re.compile(r"#\s*description\s*:\s*([^\n]+)")
_REGISTER_RE = re.compile(r'\bregister_tool\s+"([^"]+)"\s+true\b')
# Matches a heredoc opener and captures its terminator word. Handles
# `<<EOF`, `<<-EOF`, `<< EOF`, `<<'EOF'` and `<<"EOF"`.
_HEREDOC_RE = re.compile(r'<<[-~]?\s*["\']?([A-Za-z_][A-Za-z0-9_]*)["\']?')
# In-memory cache of the last scan. Sprint 12A uses a single startup scan
# plus on-demand re-scan via the API; no automatic refresh.
@@ -122,12 +125,31 @@ def parse_post_install_script(path: Path) -> dict[str, dict[str, str]]:
continue
func_name = match.group("name")
# Find the matching closing brace at column 0. Bash post-install
# scripts use the convention `}` on its own line at the start of
# the line to close top-level functions, so we scan until that.
# Find the matching closing brace at column 0. Top-level post-install
# functions close with `}` alone on a line. A bare scan for that line
# breaks when the body embeds a heredoc whose content also has `}` at
# column 0 — e.g. a logrotate stanza written via `cat >... <<EOF`.
# That truncated the body before its `register_tool` call, so tools
# like log2ram / network_optimization were never registered and never
# flagged as updatable. Track heredoc state and ignore `}` inside one.
body_start = i + 1
body_end = body_start
while body_end < len(lines) and not lines[body_end].rstrip() == "}":
heredoc_term = None
while body_end < len(lines):
current = lines[body_end]
if heredoc_term is not None:
# Inside a heredoc — only its terminator line ends it.
if current.strip() == heredoc_term:
heredoc_term = None
body_end += 1
continue
if current.rstrip() == "}":
break
# A line may open a heredoc (use the last opener on the line, so
# `cmd <<A | cmd <<B` picks B, matching shell behaviour).
openers = _HEREDOC_RE.findall(current)
if openers:
heredoc_term = openers[-1]
body_end += 1
body = "\n".join(lines[body_start:body_end])

208
README.md Normal file
View File

@@ -0,0 +1,208 @@
<div align="center">
<img src="https://github.com/MacRimi/ProxMenux/blob/main/images/main.png"
alt="ProxMenux Logo"
style="max-width: 100%; height: auto;" >
</div>
<br />
<div align="center">
<a href="https://proxmenux.com/en" target="_blank">
<img src="https://img.shields.io/badge/Website-%23E64804?style=for-the-badge&logo=World-Wide-Web&logoColor=white" alt="Website" />
</a>
<a href="https://proxmenux.com/en/docs/introduction" target="_blank">
<img src="https://img.shields.io/badge/Docs-%232A3A5D?style=for-the-badge&logo=read-the-docs&logoColor=white" alt="Docs" />
</a>
<a href="https://proxmenux.com/en/changelog" target="_blank">
<img src="https://img.shields.io/badge/Changelog-%232A3A5D?style=for-the-badge&logo=git&logoColor=white" alt="Changelog" />
</a>
<a href="https://proxmenux.com/en/guides" target="_blank">
<img src="https://img.shields.io/badge/Guides-%232A3A5D?style=for-the-badge&logo=bookstack&logoColor=white" alt="Guides" />
</a>
</div>
<div align="center" style="margin-top: 14px;">
<a href="https://github.com/MacRimi/ProxMenux/releases/latest"><img src="https://img.shields.io/github/v/release/MacRimi/ProxMenux?display_name=tag&label=latest&color=2A3A5D&style=flat-square" alt="Latest release" /></a>
<a href="https://github.com/MacRimi/ProxMenux/releases?q=prerelease%3Atrue"><img src="https://img.shields.io/github/v/release/MacRimi/ProxMenux?include_prereleases&label=beta&color=E64804&style=flat-square" alt="Latest beta" /></a>
<a href="https://github.com/MacRimi/ProxMenux/blob/main/LICENSE"><img src="https://img.shields.io/github/license/MacRimi/ProxMenux?color=2A3A5D&style=flat-square&cacheSeconds=300" alt="License" /></a>
<a href="https://github.com/MacRimi/ProxMenux/stargazers"><img src="https://img.shields.io/github/stars/MacRimi/ProxMenux?style=flat-square" alt="GitHub stars" /></a>
<a href="https://github.com/MacRimi/ProxMenux/issues"><img src="https://img.shields.io/github/issues/MacRimi/ProxMenux?color=2A3A5D&style=flat-square" alt="Open issues" /></a>
</div>
<br />
<p align="center">
<strong>ProxMenux</strong> is an interactive toolkit for <strong>Proxmox VE</strong> — a menu-driven CLI plus a web dashboard for post-install, backup/restore and continuous health monitoring of your homelab.
</p>
---
## 📌 Installation
To install ProxMenux, simply run the following command in your Proxmox server terminal:
```bash
bash -c "$(wget -qLO - https://raw.githubusercontent.com/MacRimi/ProxMenux/main/install_proxmenux.sh)"
```
> ⚠️ Be careful when copying scripts from the internet. Always remember to check the source!
>
> 📄 You can [review the source code](https://github.com/MacRimi/ProxMenux/blob/main/install_proxmenux.sh) before execution.
>
> 🛡️ All executable links follow our [Code of Conduct](https://github.com/MacRimi/ProxMenux?tab=coc-ov-file#-2-security--code-responsibility).
---
## 📌 How to Use
Once installed, launch **ProxMenux** by running:
```bash
menu
```
Then, follow the on-screen options to manage your Proxmox server efficiently.
---
## 🖥️ ProxMenux Monitor
ProxMenux Monitor is an integrated web dashboard that provides real-time visibility into your Proxmox infrastructure — accessible from any browser on your network, without needing a terminal.
**What it offers:**
- Real-time monitoring of CPU, RAM, disk usage and network traffic
- Overview of running VMs and LXC containers with status indicators
- Login authentication to protect access
- Two-Factor Authentication (2FA) with TOTP support
- Reverse proxy support (Nginx / Traefik)
- Designed to work across desktop and mobile devices
**Access:**
Once installed, the dashboard is available at:
```
http://<your-proxmox-ip>:8008
```
The Monitor is installed automatically as part of the standard ProxMenux installation and runs as a systemd service (`proxmenux-monitor.service`) that starts automatically on boot.
**Useful commands:**
```bash
# Check service status
systemctl status proxmenux-monitor
# View logs
journalctl -u proxmenux-monitor -n 50
# Restart the service
systemctl restart proxmenux-monitor
```
---
## 🧪 Beta Program
Want to try the latest features before the official release and help shape the final version?
The **ProxMenux Beta Program** gives early access to new functionality — including the newest builds of ProxMenux Monitor — directly from the `develop` branch. Beta builds may contain bugs or incomplete features. Your feedback is what helps fix them before the stable release.
**Install the beta version:**
```bash
bash -c "$(wget -qLO - https://raw.githubusercontent.com/MacRimi/ProxMenux/develop/install_proxmenux_beta.sh)"
```
**What to expect:**
- You'll get new features and Monitor builds before anyone else
- Some things may not work perfectly — that's expected and normal
- When a stable release is published, ProxMenux will notify you on the next `menu` launch and offer to switch automatically
**How to report issues:**
Open a [GitHub Issue](https://github.com/MacRimi/ProxMenux/issues) and include:
- What you did and what you expected to happen
- Any error messages shown on screen
- Logs from the Monitor if relevant:
```bash
journalctl -u proxmenux-monitor -n 50
```
> 💙 Thank you for being part of the beta program. Your help makes ProxMenux better for everyone.
---
## 🔧 Dependencies
The following Debian packages are installed automatically during setup:
| Package | Purpose |
|---|---|
| `dialog` | Interactive terminal menus |
| `curl` | Downloads and connectivity checks |
| `jq` | JSON processing |
| `git` | Repository cloning and updates |
| `python3` + `python3-pip` | ProxMenux Monitor (Flask web dashboard) |
UI translations ship as pre-built JSON files per language (English, Spanish, French, German, Italian, Portuguese). There is no runtime translation dependency.
---
## 🤝 Contributing
ProxMenux is an open, collaborative project — contributions of every shape are very welcome, no matter your background. Every PR, bug report, idea, translation or kind word helps move the project forward.
> 📖 **Before sending code**, please read the [**Contributing Guide**](CONTRIBUTING.md). It covers the project structure, the UI design policy (the two-phase `dialog` / `whiptail` flow), message helpers, translation policy and submission conventions — what reviewers will look for in your PR.
**Ways to help:**
- 💻 **Code** — fix a bug, polish a script, add a feature. Read the [Contributing Guide](CONTRIBUTING.md) first, then [open a pull request](https://github.com/MacRimi/ProxMenux/pulls).
- 🐛 **Bug reports** — found something broken? [Open an issue](https://github.com/MacRimi/ProxMenux/issues/new) with steps to reproduce, and the Monitor logs if relevant (`journalctl -u proxmenux-monitor -n 50`).
- 💡 **Ideas & feedback** — share suggestions in [GitHub Discussions](https://github.com/MacRimi/ProxMenux/discussions). Every idea is welcome.
- 🌍 **Translations** — the documentation site already supports English and Spanish; help expand it to more languages following the [translation guide](web/CONTRIBUTING-TRANSLATIONS.md) (one page per PR).
- 🧪 **Beta testing** — run the [beta build](#-beta-program) and let us know what you find.
-**Spread the word** — a GitHub star or a mention in your homelab community helps others discover the project.
Before contributing, please take a moment to read our [Code of Conduct](https://github.com/MacRimi/ProxMenux?tab=coc-ov-file).
### Contributors
Thanks to everyone who has helped make ProxMenux what it is today.
<a href="https://github.com/MacRimi/ProxMenux/graphs/contributors">
<img src="https://contrib.rocks/image?repo=MacRimi/ProxMenux&v=123" alt="ProxMenux contributors" />
</a>
Made with [contrib.rocks](https://contrib.rocks).
---
## ⭐ Support the Project
If **ProxMenux** is useful to you, the simplest way to support it is a ⭐ on GitHub — it really helps others discover the project.
If you want to go a step further, a coffee on Ko-fi keeps development going:
<p>
<a href="https://ko-fi.com/G2G313ECAN" target="_blank">
<img src="https://img.shields.io/badge/Support%20on-Ko--fi-FF5E5B?style=for-the-badge&logo=ko-fi&logoColor=white" alt="Support on Ko-fi" />
</a>
</p>
---
## 📈 Project Growth
<p align="center">
<a href="https://github.com/MacRimi/repo-growth">
<img src="images/project-growth.svg" alt="ProxMenux project growth" width="900">
</a>
</p>
<p align="center">
<sub>Stars and forks tracked with <a href="https://github.com/MacRimi/repo-growth">Repo Growth</a>.</sub>
</p>

View File

@@ -764,7 +764,7 @@ install_normal_version() {
((current_step++))
show_progress $current_step $total_steps "Install ProxMenux repository"
msg_info "Cloning ProxMenux repositoryy."
msg_info "Cloning ProxMenux repository."
if ! git clone --depth 1 "$REPO_URL" "$TEMP_DIR" 2>/dev/null; then
msg_error "Failed to clone repository from $REPO_URL"
exit 1
@@ -848,7 +848,7 @@ install_normal_version() {
create_monitor_service
fi
msg_ok "ProxMenux Normal Version installation completed successfully."
msg_ok "ProxMenux installation completed successfully."
}
show_installation_options() {

View File

@@ -480,6 +480,7 @@
"Ceph repository configured for PVE 8": "Ceph-Repository für PVE 8 konfiguriert",
"Ceph repository configured for PVE 9": "Ceph-Repository für PVE 9 konfiguriert",
"Ceph version OK:": "Ceph-Version OK:",
"Certificate fingerprint of the PBS server:": "Zertifikatsfingerabdruck des PBS-Servers:",
"Change Language": "Sprache ändern",
"Change Release Channel": "Veröffentlichungskanal ändern",
"Changes applied. A system reboot is recommended for them to take full effect.": "Änderungen übernommen. Damit sie ihre volle Wirkung entfalten, wird ein Neustart des Systems empfohlen.",
@@ -3096,6 +3097,7 @@
"PBS API log rotation configured (hourly, size-based)": "PBS-API-Protokollrotation konfiguriert (stündlich, größenbasiert)",
"PBS backup error log": "PBS-Backup-Fehlerprotokoll",
"PBS backup failed.": "PBS-Sicherung fehlgeschlagen.",
"PBS certificate": "PBS-Zertifikat",
"PBS encryption keyfile (show / replace / remove)": "PBS-Verschlüsselungsschlüsseldatei (anzeigen/ersetzen/entfernen)",
"PBS encryption keyfile — detailed info": "PBS-Verschlüsselungsschlüsseldatei detaillierte Informationen",
"PBS extraction failed": "Die PBS-Extraktion ist fehlgeschlagen",
@@ -4476,6 +4478,7 @@
"Total size:": "Gesamtgröße:",
"Translation files:": "Übersetzungsdateien:",
"Tried pvesm path and manual detection methods": "Versuchte den Pvesm-Pfad und manuelle Erkennungsmethoden",
"Trust this certificate and save it for scheduled backups?": "Diesem Zertifikat vertrauen und es für geplante Sicherungen speichern?",
"Try Again": "Versuchen Sie es erneut",
"Try a different search term.": "Versuchen Sie es mit einem anderen Suchbegriff.",
"Try accessing": "Versuchen Sie, darauf zuzugreifen",

View File

@@ -480,6 +480,7 @@
"Ceph repository configured for PVE 8": "Repositorio Ceph configurado para PVE 8",
"Ceph repository configured for PVE 9": "Repositorio Ceph configurado para PVE 9",
"Ceph version OK:": "Versión Ceph correcta:",
"Certificate fingerprint of the PBS server:": "Huella digital del certificado del servidor PBS:",
"Change Language": "Cambiar idioma",
"Change Release Channel": "Cambiar canal de lanzamiento",
"Changes applied. A system reboot is recommended for them to take full effect.": "Se aplicaron cambios. Se recomienda reiniciar el sistema para que surtan efecto completo.",
@@ -3096,6 +3097,7 @@
"PBS API log rotation configured (hourly, size-based)": "Rotación de registros de API de PBS configurada (por horas, según el tamaño)",
"PBS backup error log": "Registro de errores de copia de seguridad de PBS",
"PBS backup failed.": "La copia de seguridad de PBS falló.",
"PBS certificate": "certificado PBS",
"PBS encryption keyfile (show / replace / remove)": "archivo de claves de cifrado PBS (mostrar/reemplazar/eliminar)",
"PBS encryption keyfile — detailed info": "archivo de claves de cifrado PBS: información detallada",
"PBS extraction failed": "Error en la extracción de PBS",
@@ -4476,6 +4478,7 @@
"Total size:": "Tamaño total:",
"Translation files:": "Archivos de traducción:",
"Tried pvesm path and manual detection methods": "Ruta pvesm probada y métodos de detección manual",
"Trust this certificate and save it for scheduled backups?": "¿Confiar en este certificado y guardarlo para copias de seguridad programadas?",
"Try Again": "Intentar otra vez",
"Try a different search term.": "Pruebe con un término de búsqueda diferente.",
"Try accessing": "Intenta acceder",

View File

@@ -480,6 +480,7 @@
"Ceph repository configured for PVE 8": "Dépôt Ceph configuré pour PVE 8",
"Ceph repository configured for PVE 9": "Dépôt Ceph configuré pour PVE 9",
"Ceph version OK:": "Version Ceph OK :",
"Certificate fingerprint of the PBS server:": "Empreinte digitale du certificat du serveur PBS :",
"Change Language": "Changer de langue",
"Change Release Channel": "Changer le canal de publication",
"Changes applied. A system reboot is recommended for them to take full effect.": "Modifications appliquées. Un redémarrage du système est recommandé pour qu'ils prennent pleinement effet.",
@@ -3096,6 +3097,7 @@
"PBS API log rotation configured (hourly, size-based)": "rotation des journaux de l'API PBS configurée (horaire, basée sur la taille)",
"PBS backup error log": "Journal des erreurs de sauvegarde PBS",
"PBS backup failed.": "La sauvegarde PBS a échoué.",
"PBS certificate": "certificat PBS",
"PBS encryption keyfile (show / replace / remove)": "fichier de clé de chiffrement PBS (afficher/remplacer/supprimer)",
"PBS encryption keyfile — detailed info": "Fichier de clé de chiffrement PBS  informations détaillées",
"PBS extraction failed": "L'extraction PBS a échoué",
@@ -4476,6 +4478,7 @@
"Total size:": "Taille totale :",
"Translation files:": "Fichiers de traduction :",
"Tried pvesm path and manual detection methods": "Chemin pvesm essayé et méthodes de détection manuelle",
"Trust this certificate and save it for scheduled backups?": "Faire confiance à ce certificat et l'enregistrer pour les sauvegardes planifiées ?",
"Try Again": "Essayer à nouveau",
"Try a different search term.": "Essayez un autre terme de recherche.",
"Try accessing": "Essayez d'accéder",

View File

@@ -480,6 +480,7 @@
"Ceph repository configured for PVE 8": "Repository Ceph configurato per PVE 8",
"Ceph repository configured for PVE 9": "Repository Ceph configurato per PVE 9",
"Ceph version OK:": "Versione Ceph OK:",
"Certificate fingerprint of the PBS server:": "impronta digitale del certificato del server PBS:",
"Change Language": "Cambia lingua",
"Change Release Channel": "Cambia canale di rilascio",
"Changes applied. A system reboot is recommended for them to take full effect.": "Modifiche applicate. Si consiglia di riavviare il sistema affinché abbiano pieno effetto.",
@@ -3096,6 +3097,7 @@
"PBS API log rotation configured (hourly, size-based)": "rotazione del log API PBS configurata (oraria, in base alle dimensioni)",
"PBS backup error log": "Registro degli errori del backup PBS",
"PBS backup failed.": "Il backup PBS non è riuscito.",
"PBS certificate": "certificato PBS",
"PBS encryption keyfile (show / replace / remove)": "file chiave di crittografia PBS (mostra/sostituisci/rimuovi)",
"PBS encryption keyfile — detailed info": "file chiave di crittografia PBS: informazioni dettagliate",
"PBS extraction failed": "L'estrazione PBS non è riuscita",
@@ -4476,6 +4478,7 @@
"Total size:": "Dimensione totale:",
"Translation files:": "File di traduzione:",
"Tried pvesm path and manual detection methods": "Ho provato il percorso pvesm e i metodi di rilevamento manuale",
"Trust this certificate and save it for scheduled backups?": "considerare attendibile questo certificato e salvarlo per i backup pianificati?",
"Try Again": "Riprova",
"Try a different search term.": "Prova un termine di ricerca diverso.",
"Try accessing": "Prova ad accedere",

View File

@@ -480,6 +480,7 @@
"Ceph repository configured for PVE 8": "Repositório Ceph configurado para PVE 8",
"Ceph repository configured for PVE 9": "Repositório Ceph configurado para PVE 9",
"Ceph version OK:": "Versão do Ceph OK:",
"Certificate fingerprint of the PBS server:": "Impressão digital do certificado do servidor PBS:",
"Change Language": "Alterar idioma",
"Change Release Channel": "Alterar canal de lançamento",
"Changes applied. A system reboot is recommended for them to take full effect.": "Alterações aplicadas. Recomenda-se uma reinicialização do sistema para que tenham efeito total.",
@@ -3096,6 +3097,7 @@
"PBS API log rotation configured (hourly, size-based)": "rotação de log da API PBS configurada (por hora, com base no tamanho)",
"PBS backup error log": "Log de erros de backup do PBS",
"PBS backup failed.": "Falha no backup do PBS.",
"PBS certificate": "certificado PBS",
"PBS encryption keyfile (show / replace / remove)": "arquivo-chave de criptografia PBS (mostrar/substituir/remover)",
"PBS encryption keyfile — detailed info": "arquivo-chave de criptografia PBS informações detalhadas",
"PBS extraction failed": "Falha na extração de PBS",
@@ -4476,6 +4478,7 @@
"Total size:": "Tamanho total:",
"Translation files:": "Arquivos de tradução:",
"Tried pvesm path and manual detection methods": "Tentei caminho pvesm e métodos de detecção manual",
"Trust this certificate and save it for scheduled backups?": "Confiar neste certificado e salvá-lo para backups agendados?",
"Try Again": "Tente novamente",
"Try a different search term.": "Experimente um termo de pesquisa diferente.",
"Try accessing": "Tente acessar",

View File

@@ -296,6 +296,10 @@ _create_job_attached() {
"PBS_BACKUP_ID=${bid}"
"PBS_KEYFILE=${pbs_kf_val}"
"PBS_ENCRYPTION_PASSWORD=${HB_PBS_ENC_PASS:-}"
# Resolved by hb_select_pbs_repository. Persist it so the runner
# doesn't have to re-derive it — it can only do that for
# Datacenter-managed storages.
"PBS_FINGERPRINT=${HB_PBS_FINGERPRINT:-}"
)
;;
local)
@@ -463,6 +467,10 @@ _create_job() {
"PBS_BACKUP_ID=${bid}"
"PBS_KEYFILE=${pbs_kf_val}"
"PBS_ENCRYPTION_PASSWORD=${HB_PBS_ENC_PASS:-}"
# Resolved by hb_select_pbs_repository. Persist it so the runner
# doesn't have to re-derive it — it can only do that for
# Datacenter-managed storages.
"PBS_FINGERPRINT=${HB_PBS_FINGERPRINT:-}"
)
;;
esac

View File

@@ -840,14 +840,44 @@ hb_configure_pbs_manual() {
repo="${user}@${host}:${datastore}"
mkdir -p "$HB_STATE_DIR"
# Capture the PBS certificate fingerprint. proxmox-backup-client
# refuses a self-signed PBS without one, and a scheduled job has no
# TTY to answer the interactive prompt — so a destination added here
# with no fingerprint would fail every timed run. openssl reads the
# cert without authenticating; show it for the operator to confirm
# (trust-on-first-use) before saving. Skipped silently if the host is
# unreachable or openssl is absent — a CA-signed PBS needs no pin.
local fingerprint="" fp_host fp_port
fp_host="${host%%:*}"
fp_port="${host##*:}"
[[ "$fp_port" == "$host" || -z "$fp_port" ]] && fp_port=8007
if command -v openssl >/dev/null 2>&1; then
fingerprint=$(echo | timeout 8 openssl s_client -connect "${fp_host}:${fp_port}" 2>/dev/null \
| openssl x509 -fingerprint -sha256 -noout 2>/dev/null \
| sed 's/.*=//' | tr 'A-Z' 'a-z')
fi
if [[ -n "$fingerprint" ]]; then
if ! dialog --backtitle "ProxMenux" --title "$(hb_translate "PBS certificate")" \
--yesno "$(hb_translate "Certificate fingerprint of the PBS server:")\n\n${fingerprint}\n\n$(hb_translate "Trust this certificate and save it for scheduled backups?")" \
14 76; then
fingerprint=""
fi
fi
local cfg_line="${name}|${repo}"
local manual_cfg="$HB_STATE_DIR/pbs-manual-configs.txt"
touch "$manual_cfg"
grep -Fxq "$cfg_line" "$manual_cfg" || echo "$cfg_line" >> "$manual_cfg"
printf '%s' "$secret" > "$HB_STATE_DIR/pbs-pass-${name}.txt"
chmod 600 "$HB_STATE_DIR/pbs-pass-${name}.txt"
if [[ -n "$fingerprint" ]]; then
printf '%s' "$fingerprint" > "$HB_STATE_DIR/pbs-fingerprint-${name}.txt"
chmod 600 "$HB_STATE_DIR/pbs-fingerprint-${name}.txt"
fi
HB_PBS_NAME="$name"; HB_PBS_REPOSITORY="$repo"; HB_PBS_SECRET="$secret"
HB_PBS_FINGERPRINT="$fingerprint"
}
hb_select_pbs_repository() {
@@ -875,13 +905,12 @@ hb_select_pbs_repository() {
HB_PBS_NAME="${HB_PBS_NAMES[$sel]}"
export HB_PBS_REPOSITORY="${HB_PBS_REPOS[$sel]}"
HB_PBS_SECRET="${HB_PBS_SECRETS[$sel]}"
# Export the fingerprint so _bk_pbs / _rs_extract_pbs can
# pass it to proxmox-backup-client via PBS_FINGERPRINT. The
# binary otherwise prompts "Are you sure you want to
# continue connecting? (y/n):" — twice in some flows
# (backup + catalog upload) — and silently auto-accepts on
# stdin closure, which is both noisy and an MITM risk on a
# cross-host restore.
# Export the fingerprint so _bk_pbs / _rs_extract_pbs can pass it
# to proxmox-backup-client via PBS_FINGERPRINT. Against a
# self-signed PBS the client only offers its "continue
# connecting? (y/n)" prompt on a real TTY; anywhere else it
# refuses the connection outright, so the fingerprint is what
# makes the transfer work at all.
export HB_PBS_FINGERPRINT="${HB_PBS_FINGERPRINTS[$sel]:-}"
if [[ -z "$HB_PBS_SECRET" ]]; then
while true; do

View File

@@ -339,6 +339,35 @@ _sb_pbs_resolve_fingerprint() {
printf '%s' "$found_fp"
}
# Same lookup for a PBS destination added through ProxMenux instead of
# Datacenter → Storage. Those never reach storage.cfg, so the walkers
# above find nothing and their credentials live only in our state dir:
# `pbs-manual-configs.txt` holds `<name>|<user>@<server>:<datastore>`
# lines, and the secret itself is in the `pbs-<kind>-<name>.txt` sidecar
# written when the destination was added.
_sb_pbs_state_lookup() {
local repo="$1" kind="$2" # kind: fingerprint | pass
local state_dir="${HB_STATE_DIR:-/usr/local/share/proxmenux}"
local manual="$state_dir/pbs-manual-configs.txt"
[[ -f "$manual" && -n "$repo" ]] || return 1
local line name entry_repo
while IFS= read -r line; do
line="${line%%#*}"
line="${line#"${line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
[[ -z "$line" || "$line" != *"|"* ]] && continue
name="${line%%|*}"
entry_repo="${line#*|}"
[[ "$entry_repo" == "$repo" ]] || continue
local f="$state_dir/pbs-${kind}-${name}.txt"
[[ -r "$f" ]] || return 1
tr -d '\r\n' < "$f"
return 0
done < "$manual"
return 1
}
_sb_run_pbs() {
local stage_root="$1"
local backup_id="$2"
@@ -363,17 +392,21 @@ _sb_run_pbs() {
# this function can run unchanged.
if [[ -z "${PBS_PASSWORD:-}" && -n "${PBS_REPOSITORY:-}" ]]; then
PBS_PASSWORD=$(_sb_pbs_resolve_password "$PBS_REPOSITORY" 2>/dev/null || true)
# ProxMenux-managed destinations aren't in /etc/pve/priv/storage.
[[ -z "$PBS_PASSWORD" ]] && \
PBS_PASSWORD=$(_sb_pbs_state_lookup "$PBS_REPOSITORY" pass 2>/dev/null || true)
fi
# Same treatment for PBS_FINGERPRINT. Attached scheduled jobs are
# created without one (the wizard delegates trust to PVE's storage
# config), and running proxmox-backup-client with an empty fingerprint
# against a self-signed PBS drops into an interactive `(y/n)` prompt —
# which under systemd never gets an answer and hangs the timer.
# Resolving on-demand from storage.cfg matches the runtime behavior
# PVE itself has for its own vzdump jobs.
# config), and against a self-signed PBS the client refuses to connect
# without it, so the job fails on every run. Two sources, in order:
# PVE's storage.cfg for Datacenter-managed storages, then our own
# state dir for destinations added from the ProxMenux UI.
if [[ -z "${PBS_FINGERPRINT:-}" && -n "${PBS_REPOSITORY:-}" ]]; then
PBS_FINGERPRINT=$(_sb_pbs_resolve_fingerprint "$PBS_REPOSITORY" 2>/dev/null || true)
[[ -z "$PBS_FINGERPRINT" ]] && \
PBS_FINGERPRINT=$(_sb_pbs_state_lookup "$PBS_REPOSITORY" fingerprint 2>/dev/null || true)
fi
# PBS_ENCRYPTION_PASSWORD: the passphrase that unlocks the keyfile

View File

@@ -113,6 +113,14 @@ export default async function TerminalTabPage({
{t.rich("target.body2", { strong, em, code, link: vmsLink })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("menuGuard.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("menuGuard.body1", { strong, em, code })}
</p>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("menuGuard.body2", { strong, em, code })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("fourTerminals.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t("fourTerminals.intro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">

View File

@@ -225,6 +225,14 @@ export default async function HealthMonitorPage({
{t("dashboardView.pillBody")}
</Callout>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("dashboardView.updateNowTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("dashboardView.updateNowBody1", { strong, em, code })}
</p>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("dashboardView.updateNowBody2", { strong, em, code })}
</p>
<h2 id="dismissing-alerts-and-the-suppression-duration" className="text-2xl font-semibold mt-10 mb-4 text-gray-900 scroll-mt-24">{t("dismiss.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("dismiss.intro", { em })}

View File

@@ -96,6 +96,11 @@ export default async function PostInstallNetworkPage({
{t.rich("sysctl.sourceOutro", { code })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("sysctl.fwbrTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("sysctl.fwbrBody", { code })}
</p>
<Callout variant="warning" title={t("sysctl.rpFilterTitle")}>
{t.rich("sysctl.rpFilterBody", { em, code })}
</Callout>

View File

@@ -91,7 +91,6 @@ export default async function PostInstallStoragePage({
<thead className="bg-gray-100">
<tr>
<th className="border border-gray-200 px-3 py-2 text-left text-gray-900">{t("arc.headerRam")}</th>
<th className="border border-gray-200 px-3 py-2 text-left text-gray-900">{t("arc.headerMin")}</th>
<th className="border border-gray-200 px-3 py-2 text-left text-gray-900">{t("arc.headerMax")}</th>
</tr>
</thead>
@@ -99,7 +98,6 @@ export default async function PostInstallStoragePage({
{arcRows.map((row) => (
<tr key={row.ram}>
<td className="border border-gray-200 px-3 py-2">{row.ram}</td>
<td className="border border-gray-200 px-3 py-2">{row.min}</td>
<td className="border border-gray-200 px-3 py-2">{row.max}</td>
</tr>
))}

View File

@@ -1,7 +1,7 @@
{
"meta": {
"title": "Creating backups — interactive backup flow | ProxMenux",
"description": "The interactive backup flow in ProxMenux. Two entry points (Scripts TUI menu and Monitor Web UI), three destinations, two profile modes, one staging step, and one confirmation dialog. Documents the six-option matrix, the default and custom profiles, and what the operator sees between selecting a backup and the archive landing on the destination.",
"description": "The interactive backup flow in ProxMenux. Two entry points (Scripts TUI menu and Monitor Web UI), three destinations, two profile modes, one staging step, and one confirmation dialog. Documents the six-option matrix, the default and custom profiles, and what the user sees between selecting a backup and the archive landing on the destination.",
"ogTitle": "ProxMenux Backup — creating backups",
"ogDescription": "The interactive backup flow with three destinations, two profiles and a common staging step.",
"twitterTitle": "Creating backups | ProxMenux",
@@ -25,7 +25,7 @@
},
"modes": {
"heading": "Manual vs scheduled backups",
"body": "Backups can be produced in two modes: <strong>manual</strong> (the interactive flow this page documents — the operator picks a destination and a profile from a menu and watches the archive land) or <strong>scheduled</strong> (an unattended job that runs on a cron-style timer and applies the retention configured on the job). Both modes support the same three destinations and the same two profiles, and both are available from both entry points — the Scripts TUI menu and the Monitor Backups tab. Scheduled jobs use the same backend functions as the manual flow through <code>run_scheduled_backup.sh</code>; the archives produced are indistinguishable.",
"body": "Backups can be produced in two modes: <strong>manual</strong> (the interactive flow this page documents — the user picks a destination and a profile from a menu and watches the archive land) or <strong>scheduled</strong> (an unattended job that runs on a cron-style timer and applies the retention configured on the job). Both modes support the same three destinations and the same two profiles, and both are available from both entry points — the Scripts TUI menu and the Monitor Backups tab. Scheduled jobs use the same backend functions as the manual flow through <code>run_scheduled_backup.sh</code>; the archives produced are indistinguishable.",
"seeAlso": "The scheduled-jobs page covers the full flow, including how to create a job, attach it to an existing PVE vzdump timer, and configure the retention values.",
"monitorAlt": "ProxMenux Monitor Backups tab showing the New scheduled backup dialog with destination, profile, schedule and retention fields.",
"monitorCaption": "Scheduled backup — ProxMenux Monitor. The same wizard-style dialog that creates a manual backup carries the schedule and retention fields at the bottom for the unattended path."
@@ -37,7 +37,7 @@
{ "combo": "1", "destination": "PBS", "profile": "Default", "action": "Uploads the default profile plus persistent extras to a configured PBS repository." },
{ "combo": "2", "destination": "Borg", "profile": "Default", "action": "Creates an archive with the default profile plus persistent extras in the selected Borg repository." },
{ "combo": "3", "destination": "Local", "profile": "Default", "action": "Writes a <code>.tar.zst</code> archive with the default profile plus persistent extras to the configured local target." },
{ "combo": "4", "destination": "PBS", "profile": "Custom", "action": "Opens the path picker before the PBS upload; the operator ticks paths and can add new ones." },
{ "combo": "4", "destination": "PBS", "profile": "Custom", "action": "Opens the path picker before the PBS upload; the user ticks paths and can add new ones." },
{ "combo": "5", "destination": "Borg", "profile": "Custom", "action": "Opens the path picker before the Borg archive create." },
{ "combo": "6", "destination": "Local", "profile": "Custom", "action": "Opens the path picker before writing the local <code>.tar.zst</code>." }
]
@@ -45,11 +45,11 @@
"profiles": {
"heading": "Default vs Custom profile",
"defaultTitle": "Default profile",
"defaultBody": "The default profile is the curated list from <code>hb_default_profile_paths</code> (documented in <em>How it works</em> under <em>Path categories</em>) plus every entry in the persistent extras file <code>/usr/local/share/proxmenux/backup-extra-paths.txt</code>. The operator confirms the destination and encryption options and the backup proceeds without further path selection.",
"defaultBody": "The default profile is the curated list from <code>hb_default_profile_paths</code> (documented in <em>How it works</em> under <em>Path categories</em>) plus every entry in the persistent extras file <code>/usr/local/share/proxmenux/backup-extra-paths.txt</code>. The user confirms the destination and encryption options and the backup proceeds without further path selection.",
"customTitle": "Custom profile",
"customBody": "The custom profile opens a checklist showing every path in the default profile (unchecked) and every persistent extra (pre-checked, prefixed with <code>[+]</code>). The operator ticks the set for this run and can press <em>Add custom path</em> to append a new absolute path. Any path added inline is persisted to <code>backup-extra-paths.txt</code> so future backups pick it up automatically without re-adding it. Removing a persistent extra unticks it for this run but does not delete it from the file — deletion is a separate <em>Manage custom paths</em> action outside the backup flow.",
"customBody": "The custom profile opens a checklist showing every path in the default profile (unchecked) and every persistent extra (pre-checked, prefixed with <code>[+]</code>). The user ticks the set for this run and can press <em>Add custom path</em> to append a new absolute path. Any path added inline is persisted to <code>backup-extra-paths.txt</code> so future backups pick it up automatically without re-adding it. Removing a persistent extra unticks it for this run but does not delete it from the file — deletion is a separate <em>Manage custom paths</em> action outside the backup flow.",
"customPickerAlt": "Custom profile checklist showing the default-profile paths (unchecked) and persistent extras (pre-checked with a [+] prefix), plus buttons to add a new path or confirm the selection.",
"customPickerCaption": "Custom profile — the path picker. Default-profile paths are unchecked; persistent extras appear pre-checked with a [+] prefix. The operator ticks the set for this run.",
"customPickerCaption": "Custom profile — the path picker. Default-profile paths are unchecked; persistent extras appear pre-checked with a [+] prefix. The user ticks the set for this run.",
"manageCustomAlt": "Manage custom paths menu showing the list of persistent extras and options to add, remove or edit them.",
"manageCustomCaption": "Manage custom paths — the entry point where persistent extras are added or removed. Every path listed here is included automatically in Default-mode backups without needing to open the Custom picker."
},
@@ -57,11 +57,11 @@
"heading": "What runs regardless of destination",
"intro": "After the profile is resolved, every backend runs the same staging pipeline before diverging into its own upload path. <code>hb_prepare_staging</code> assembles the archive tree in <code>/tmp/proxmenux-DESTINATION-stage.XXXXXX</code> and populates each of the three payloads.",
"steps": [
{ "step": "1", "name": "rootfs assembly", "detail": "Runs <code>rsync -a</code> for each selected path into <code>staging_root/rootfs/</code>. Excludes volatile subpaths (bash history, caches, trash) from <code>/root/</code>. Paths absent from the source are recorded in <code>metadata/missing_paths.txt</code> without stopping the backup." },
{ "step": "1", "name": "rootfs assembly", "detail": "Runs <code>rsync -a</code> for each selected path into <code>staging_root/rootfs/</code>. The pmxcfs database at <code>/var/lib/pve-cluster/config.db</code> is captured separately via <code>sqlite3 .backup</code> to produce a consistent snapshot without stopping the cluster, and the <code>.db-wal</code> / <code>.db-shm</code> sidecars are excluded from the rsync; when <code>sqlite3</code> is not available a <code>config.db.raw-fallback</code> is dropped instead, and the restore promotes it on import. Volatile subpaths (bash history, caches, trash) are excluded from <code>/root/</code>. Paths absent from the source are recorded in <code>metadata/missing_paths.txt</code> without stopping the backup." },
{ "step": "2", "name": "Manifest generation", "detail": "<code>build_manifest.sh</code> orchestrates the six collectors and writes <code>manifest.json</code> at the top of staging. On collector failure, the affected section falls back to a documented empty default; the manifest is still valid." },
{ "step": "3", "name": "Package inventory", "detail": "<code>apt-mark showmanual</code> is captured verbatim into <code>metadata/packages.manual.list</code>. Component state is already inside the restored rootfs (<code>components_status.json</code>) because <code>/usr/local/share/proxmenux/</code> is part of the default profile." },
{ "step": "4", "name": "Run info", "detail": "<code>metadata/run_info.env</code> records the backup run identity — hostname, timestamp, kernel version — used by the restore's compatibility check to determine the cross-kernel direction." },
{ "step": "5", "name": "Notification (start)", "detail": "<code>hb_notify_lifecycle \"start\"</code> fires. If notifications are configured in the Monitor, an operator-facing <em>Host backup started</em> event is emitted. Silent if no channels are configured." }
{ "step": "5", "name": "Notification (start)", "detail": "<code>hb_notify_lifecycle \"start\"</code> fires. If notifications are configured in the Monitor, an user-facing <em>Host backup started</em> event is emitted. Silent if no channels are configured." }
]
},
"included": {
@@ -75,7 +75,7 @@
"<code>*.log</code> — log files."
],
"rootTitle": "<code>/root/</code> exclusions",
"rootBody": "<code>/root/</code> is part of the default profile so operator scripts and config land in the archive. Volatile subpaths are dropped:",
"rootBody": "<code>/root/</code> is part of the default profile so user scripts and config land in the archive. Volatile subpaths are dropped:",
"rootItems": [
"<code>.bash_history</code>",
"<code>.cache/</code>",
@@ -107,7 +107,7 @@
"archiveStructure": {
"heading": "Archive structure",
"intro": "The staging directory produced by every backend follows the same layout regardless of destination. The tarball, PBS <code>.pxar</code> or Borg archive stores this tree verbatim.",
"tree": "backup-[timestamp]/\n├── manifest.json # structured host state (kernel_params, hardware, storage, guests, components, source_host)\n├── metadata/\n│ ├── packages.manual.list # output of apt-mark showmanual\n│ ├── run_info.env # hostname, timestamp, kernel version\n│ ├── paths_archived.txt # exact list of paths that reached rootfs/\n│ └── missing_paths.txt # paths from the profile absent on source\n└── rootfs/\n ├── etc/ # /etc/pve, /etc/network, /etc/ssh, /etc/apt, ...\n ├── root/ # /root without volatile subpaths\n ├── usr/local/ # /usr/local/bin, /usr/local/sbin, /usr/local/share/proxmenux (state only)\n └── var/ # /var/lib/pve-cluster, /var/spool/cron/crontabs"
"tree": "backup-[timestamp]/\n├── manifest.json # structured host state (kernel_params, hardware, storage, guests, components, source_host)\n├── metadata/\n│ ├── packages.manual.list # output of apt-mark showmanual\n│ ├── run_info.env # hostname, timestamp, kernel version, pmxcfs method\n│ ├── paths_archived.txt # exact list of paths that reached rootfs/\n│ └── missing_paths.txt # paths from the profile absent on source\n└── rootfs/\n ├── etc/ # /etc/pve, /etc/network, /etc/systemd/network, /etc/ssh, /etc/apt, ...\n ├── root/ # /root without volatile subpaths\n ├── usr/local/ # /usr/local/bin, /usr/local/sbin, /usr/local/share/proxmenux (state only)\n └── var/ # /var/lib/pve-cluster (config.db via sqlite3 .backup), /var/spool/cron/crontabs"
},
"confirmation": {
"heading": "Confirmation summary",
@@ -115,7 +115,7 @@
},
"writing": {
"heading": "Writing to the destination",
"intro": "Once the operator confirms, each backend runs its own write step. The mechanics are covered in the destination pages; the shared surface is the log, the sidecar and the completion notification.",
"intro": "Once the user confirms, each backend runs its own write step. The mechanics are covered in the destination pages; the shared surface is the log, the sidecar and the completion notification.",
"rows": [
{ "topic": "Log file", "detail": "Every backend writes its full output to <code>/tmp/proxmenux-DESTINATION-backup-YYYYMMDD_HHMMSS.log</code> and, on failure, offers to open it in a scrollable dialog. The log path is printed in the completion summary only when the file has content." },
{ "topic": "Sidecar (local only)", "detail": "<code>hb_write_archive_sidecar</code> drops a <code>*.proxmenux.json</code> next to the local archive so the Monitor identifies it as a ProxMenux host backup even after moves or renames." },

View File

@@ -46,14 +46,14 @@
"intro": "The safe-subset filter alone would leave the target without the tuning the user had inside those boot-critical files: IOMMU cmdline for GPU passthrough, VFIO device IDs, custom <code>GRUB_TIMEOUT</code>, nvidia blacklists. The hydration pass re-applies those bits kernel-agnostically. Four phases run when the target kernel is newer than the backup's, each additive (never overwrites a value the target already carries) and idempotent (running twice is a no-op).",
"phaseRows": [
{ "phase": "1a — GRUB path", "detail": "For hosts using GRUB (ext4/lvm installs). <code>_rs_hyd_grub</code> merges every token from the backup's <code>manifest.kernel_params.cmdline_extra</code> into the target's live <code>GRUB_CMDLINE_LINUX_DEFAULT</code>, skipping tokens whose key the target already carries. Then merges whitelisted <code>GRUB_*</code> keys (<code>GRUB_TIMEOUT</code>, <code>GRUB_TIMEOUT_STYLE</code>, <code>GRUB_DEFAULT</code>, <code>GRUB_TERMINAL</code>, <code>GRUB_DISABLE_OS_PROBER</code>, <code>GRUB_SERIAL_COMMAND</code>, <code>GRUB_GFXMODE</code>, <code>GRUB_GFXPAYLOAD_LINUX</code>) from the backup's <code>/etc/default/grub</code> if they differ from the target's." },
{ "phase": "1b — systemd-boot / ZFS path", "detail": "For hosts using systemd-boot (typically ZFS-on-root). <code>_rs_hyd_kernel_cmdline</code> merges operator tokens from <code>cmdline_extra</code> into the target's <code>/etc/kernel/cmdline</code>, keeping the target's own <code>root=</code>, <code>boot=</code> and <code>rootflags=</code> boilerplate intact." },
{ "phase": "1b — systemd-boot / ZFS path", "detail": "For hosts using systemd-boot (typically ZFS-on-root). <code>_rs_hyd_kernel_cmdline</code> merges user tokens from <code>cmdline_extra</code> into the target's <code>/etc/kernel/cmdline</code>, keeping the target's own <code>root=</code>, <code>boot=</code> and <code>rootflags=</code> boilerplate intact." },
{ "phase": "2 — /etc/modules merge", "detail": "<code>_rs_hyd_modules</code> appends modules from <code>manifest.kernel_params.modules_loaded_at_boot</code> that are in the whitelist (<code>vfio</code>, <code>vfio_pci</code>, <code>vfio_iommu_type1</code>, <code>vfio_virqfd</code>, <code>kvm</code>, <code>kvm_intel</code>, <code>kvm_amd</code>, <code>nvidia</code>, <code>nvidia_drm</code>, <code>nvidia_modeset</code>, <code>nvidia_uvm</code>, <code>i915</code>, <code>xe</code>) AND not already present in the target's <code>/etc/modules</code>." },
{ "phase": "3 — Whitelisted files copy", "detail": "<code>_rs_hyd_files</code> copies operator-authored files from the staging rootfs to the live target when the content differs. Whitelist covers VFIO/nvidia/blacklist files under <code>/etc/modprobe.d</code>, <code>/etc/modules-load.d</code>, and the ProxMenux VFIO bind rule + nvidia udev rules under <code>/etc/udev/rules.d</code>. Distro-owned files (<code>pve-blacklist.conf</code>, <code>mdadm.conf</code>, <code>nvme.conf</code>) are intentionally excluded — their contents evolve between releases." },
{ "phase": "3 — Whitelisted files copy", "detail": "<code>_rs_hyd_files</code> copies user-authored files from the staging rootfs to the live target when the content differs. Whitelist covers VFIO/nvidia/blacklist files under <code>/etc/modprobe.d</code>, <code>/etc/modules-load.d</code>, and the ProxMenux VFIO bind rule + nvidia udev rules under <code>/etc/udev/rules.d</code>. Distro-owned files (<code>pve-blacklist.conf</code>, <code>mdadm.conf</code>, <code>nvme.conf</code>) are intentionally excluded — their contents evolve between releases." },
{ "phase": "4 — Force post-boot reflows", "detail": "The four phases write directly to the live target OUTSIDE the normal restore pipeline. To make the merged tokens/modules/files take effect on the next boot, <code>HB_HYDRATION_APPLIED=1</code> propagates through <code>plan.env</code> to <code>apply_pending_restore.sh</code>, which forces <code>NEEDS_INITRAMFS=1</code> and <code>NEEDS_GRUB=1</code> regardless of what was in the apply list. The post-boot dispatcher then regenerates initramfs and refreshes the bootloader." }
]
},
"planCommit": {
"heading": "Plan vs commit — the operator sees a preview first",
"heading": "Plan vs commit — the user sees a preview first",
"body": "The hydration runs in two modes. Before the confirmation dialog, ProxMenux runs <code>_rs_apply_bk_older_hydration</code> in <code>plan</code> mode: it computes exactly what would be merged, populates <code>RS_HYDRATION_SUMMARY</code> with a green block listing each action, and returns without writing. The confirmation dialog shows that green block alongside the amber safe-subset skip list, so the user sees UPFRONT what will be re-applied automatically. After the user confirms, ProxMenux re-runs the same helper in <code>commit</code> mode — same phases, same logic, but this time each phase writes to the live target. Cancelling the confirmation dialog leaves the target untouched."
},
"flowDiagram": {

View File

@@ -23,7 +23,7 @@
{
"id": "passphrase-recuperacion",
"term": "Recovery passphrase",
"def": "Text password the operator picks to protect the <a href=\"#sobre-recuperacion\">recovery envelope</a>. It is prompted twice with a match check, and is only used to encrypt the envelope and to decrypt it if the keyfile ever needs to be recovered. <strong>It is not the passphrase that unlocks the keyfile itself</strong>, and it is never sent to PBS. During a normal backup it never leaves the host."
"def": "Text password the user picks to protect the <a href=\"#sobre-recuperacion\">recovery envelope</a>. It is prompted twice with a match check, and is only used to encrypt the envelope and to decrypt it if the keyfile ever needs to be recovered. <strong>It is not the passphrase that unlocks the keyfile itself</strong>, and it is never sent to PBS. During a normal backup it never leaves the host."
},
{
"id": "clave-cifrado",
@@ -130,7 +130,7 @@
{
"id": "backup-id",
"term": "Backup ID",
"def": "Name of the <a href=\"#grupo-copia\">backup group</a>. ProxMenux defaults to <code>hostcfg-HOSTNAME</code> and lets the operator edit it before upload; unsupported characters are stripped automatically."
"def": "Name of the <a href=\"#grupo-copia\">backup group</a>. ProxMenux defaults to <code>hostcfg-HOSTNAME</code> and lets the user edit it before upload; unsupported characters are stripped automatically."
},
{
"id": "pxar",

View File

@@ -20,11 +20,11 @@
"heading": "Archive layout",
"intro": "Every archive follows the same tree layout regardless of destination. The <code>metadata/</code> subdirectory holds the structured payloads; the <code>rootfs/</code> subdirectory holds the filesystem copy.",
"treeCaption": "The staging directory laid out during a backup. Every destination receives the same tree (adapted to its native format: tar for local, PBS chunks for PBS, borg segments for Borg).",
"tree": "backup-[timestamp]/\n├── manifest.json # structured host state\n├── metadata/\n│ ├── packages.manual.list # apt-mark showmanual\n│ ├── run_info.env # backup run identity + kernel version\n│ ├── paths_archived.txt # exact list of paths that reached rootfs/\n│ └── missing_paths.txt # paths from the profile absent on source\n└── rootfs/\n ├── etc/ # /etc/pve, /etc/network, /etc/ssh, …\n ├── root/ # /root (with volatile subdirs excluded)\n ├── usr/local/ # /usr/local/bin, /usr/local/share/proxmenux, …\n └── var/ # /var/lib/pve-cluster, /var/spool/cron/…"
"tree": "backup-[timestamp]/\n├── manifest.json # structured host state\n├── metadata/\n│ ├── packages.manual.list # apt-mark showmanual\n│ ├── run_info.env # backup run identity + kernel version + pmxcfs method\n│ ├── paths_archived.txt # exact list of paths that reached rootfs/\n│ └── missing_paths.txt # paths from the profile absent on source\n└── rootfs/\n ├── etc/ # /etc/pve, /etc/network, /etc/systemd/network, /etc/ssh, …\n ├── root/ # /root (with volatile subdirs excluded)\n ├── usr/local/ # /usr/local/bin, /usr/local/share/proxmenux, …\n └── var/ # /var/lib/pve-cluster (config.db via sqlite3 .backup), /var/spool/cron/…"
},
"rootfs": {
"heading": "The rootfs payload",
"intro": "The <code>rootfs/</code> tree is a <strong>plain filesystem copy</strong> produced by <code>rsync</code> from the source host. It contains a curated <strong>default profile</strong> of paths that matter for a Proxmox restore, plus any <strong>custom paths</strong> added by the operator to the backup job or interactive session. The set is deliberately narrow: only paths that either <em>hold configuration</em> or <em>hold state that Proxmox cannot regenerate on its own</em>.",
"intro": "The <code>rootfs/</code> tree is a <strong>plain filesystem copy</strong> produced by <code>rsync</code> from the source host. It contains a curated <strong>default profile</strong> of paths that matter for a Proxmox restore, plus any <strong>custom paths</strong> added by the user to the backup job or interactive session. The set is deliberately narrow: only paths that either <em>hold configuration</em> or <em>hold state that Proxmox cannot regenerate on its own</em>.",
"defaultProfileTitle": "The default profile",
"defaultProfileBody": "The default profile is defined by <code>hb_default_profile_paths</code> in <code>lib_host_backup_common.sh</code>. It covers eight categories that together describe a working Proxmox host:",
"categoriesTitle": "Path categories",
@@ -32,17 +32,17 @@
{
"category": "PVE core",
"paths": "/etc/pve, /var/lib/pve-cluster, /etc/vzdump.conf",
"why": "Cluster filesystem contents, cluster live data, vzdump defaults."
"why": "Cluster filesystem contents and vzdump defaults. The pmxcfs database (<code>config.db</code>) is captured as a consistent snapshot via <code>sqlite3 .backup</code> without stopping the cluster, and the <code>.db-wal</code> / <code>.db-shm</code> sidecars are excluded from the rsync. When <code>sqlite3</code> is not available, a <code>config.db.raw-fallback</code> is dropped instead, which the restore promotes automatically."
},
{
"category": "Host identity & network",
"paths": "/etc/hostname, /etc/hosts, /etc/timezone, /etc/resolv.conf, /etc/network",
"why": "Everything the host needs to come up on the network with the same identity."
"paths": "/etc/hostname, /etc/hosts, /etc/timezone, /etc/resolv.conf, /etc/network, /etc/systemd/network",
"why": "Everything the host needs to come up on the network with the same identity. The <code>.link</code> files under <code>/etc/systemd/network</code> pin NIC names to their MAC, so reinstalls and hardware changes do not rename interfaces."
},
{
"category": "Access & auth",
"paths": "/etc/ssh, /etc/sudoers, /etc/sudoers.d, /etc/pam.d, /etc/security",
"why": "SSH keys, sudo rules and PAM configuration. Losing these locks the operator out of the restored host."
"why": "SSH keys, sudo rules and PAM configuration. Losing these locks the user out of the restored host."
},
{
"category": "Kernel & boot",
@@ -57,7 +57,7 @@
{
"category": "Packaging & cron",
"paths": "/etc/apt, /etc/cron.d, /etc/cron.{daily,hourly,weekly,monthly}, /etc/cron.allow, /etc/cron.deny, /var/spool/cron/crontabs",
"why": "APT sources for consistent package resolution, scheduled tasks defined by the operator."
"why": "APT sources for consistent package resolution, scheduled tasks defined by the user."
},
{
"category": "ProxMenux state & tools",
@@ -78,11 +78,11 @@
"customTitle": "Extending the profile with custom paths",
"customBody": "Beyond the default profile, ProxMenux offers two ways to include additional paths in a backup. They compose without conflict and both apply to interactive backups and scheduled jobs.",
"customExtrasTitle": "1. Persistent extras (per-host file)",
"customExtrasBody": "A text file at <code>/usr/local/share/proxmenux/backup-extra-paths.txt</code> holds a list of absolute paths the operator has marked as \"always include\" on this host. When a backup runs in <strong>Default</strong> mode, ProxMenux appends these paths to the default profile automatically without asking. The file is edited from the interface — no manual editing needed — and persists across reboots and updates. One absolute path per line; <code>#</code> comments are allowed.",
"customExtrasBody": "A text file at <code>/usr/local/share/proxmenux/backup-extra-paths.txt</code> holds a list of absolute paths the user has marked as \"always include\" on this host. When a backup runs in <strong>Default</strong> mode, ProxMenux appends these paths to the default profile automatically without asking. The file is edited from the interface — no manual editing needed — and persists across reboots and updates. One absolute path per line; <code>#</code> comments are allowed.",
"customModeTitle": "2. Custom mode (per-run)",
"customModeBody": "Launching a backup in <strong>Custom</strong> mode replaces the automatic application of the default profile with a checklist showing every path: the default-profile entries and the persistent extras (prefixed with <code>[+]</code> and pre-checked). The operator ticks or unticks entries for that specific run, and can also press <em>Add custom path</em> to enter a new path — which is then persisted to the extras file for future backups.",
"customModeBody": "Launching a backup in <strong>Custom</strong> mode replaces the automatic application of the default profile with a checklist showing every path: the default-profile entries and the persistent extras (prefixed with <code>[+]</code> and pre-checked). The user ticks or unticks entries for that specific run, and can also press <em>Add custom path</em> to enter a new path — which is then persisted to the extras file for future backups.",
"customMissingTitle": "Paths absent from the source",
"customMissingBody": "Any path from the profile (default or added) that does not exist on the source host is recorded in <code>metadata/missing_paths.txt</code> inside the archive. The backup neither fails nor stops — the operator sees a summary of archived paths and missing paths at the end. In practice this happens for optional tooling paths like <code>/etc/wireguard</code> or <code>/etc/prometheus</code> when those tools are not installed."
"customMissingBody": "Any path from the profile (default or added) that does not exist on the source host is recorded in <code>metadata/missing_paths.txt</code> inside the archive. The backup neither fails nor stops — the user sees a summary of archived paths and missing paths at the end. In practice this happens for optional tooling paths like <code>/etc/wireguard</code> or <code>/etc/prometheus</code> when those tools are not installed."
},
"manifest": {
"heading": "The manifest payload",
@@ -102,7 +102,7 @@
{
"collector": "collect_storage.sh",
"produces": "storage_inventory",
"content": "ZFS pools (with pool type + member disks resolved to <code>/dev/disk/by-id/*</code> for portability), LVM volume groups + thin pools, physical disks with SMART capability, PVE <code>storage.cfg</code> entries, external mounts."
"content": "ZFS pools (with pool type + member disks resolved to <code>/dev/disk/by-id/*</code>, <code>/dev/disk/by-partuuid/*</code> or raw <code>/dev/sdX</code> paths depending on how each pool was originally created), LVM volume groups + thin pools, physical disks with SMART capability, PVE <code>storage.cfg</code> entries, external mounts."
},
{
"collector": "collect_kernel.sh",
@@ -157,8 +157,8 @@
},
"restoreFlow": {
"heading": "How the restore consumes the three payloads",
"intro": "The restore is a five-stage pipeline. Each stage reads a specific subset of the archive and updates the target host. No stage requires the source host to be reachable — the archive is fully self-contained.",
"stagesCaption": "Stage 1 uses the manifest to decide what to touch. Stage 2 copies rootfs paths that are safe to apply on a running system. Stage 3 stages the risky paths for the next boot. Stage 4 handles packages. Stage 5 runs after the reboot and reinstalls components against the target's kernel.",
"intro": "The restore is a six-stage pipeline. Each stage reads a specific subset of the archive and updates the target host. No stage requires the source host to be reachable — the archive is fully self-contained.",
"stagesCaption": "Stage 1 uses the manifest to decide what to touch. Stage 2 copies rootfs paths that are safe to apply on a running system. Stage 3 stages the risky paths for the next boot. Stage 3b imports non-root ZFS data pools whose disks are all present. Stage 4 handles packages. Stage 5 runs after the reboot and reinstalls components against the target's kernel.",
"stageRows": [
{
"stage": "1",
@@ -178,6 +178,12 @@
"reads": "rootfs/ (reboot + dangerous paths)",
"action": "<code>_rs_prepare_pending_restore</code> stages risky paths under <code>/var/lib/proxmenux/pending-restore/</code>, writes <code>plan.env</code>, <code>apply-on-boot.list</code> and <code>rs-skip-paths.txt</code>, and enables <code>proxmenux-restore-onboot.service</code> to fire on the next boot."
},
{
"stage": "3b",
"name": "Data-pool import",
"reads": "manifest.storage_inventory + target ZFS state",
"action": "<code>_rs_import_data_pools</code> imports every non-root ZFS pool whose full set of disks is present on the target. Pools carrying a foreign hostid are retried with <code>zpool import -f</code>. Pools missing any disk are skipped with a warning. The per-pool outcome is recorded in <code>/var/log/proxmenux/restore-datapools-&lt;timestamp&gt;.log</code>."
},
{
"stage": "4",
"name": "Package install",

View File

@@ -36,7 +36,7 @@
"intro": "A ProxMenux archive is structured around three self-contained payloads. The restore uses all three together to reproduce the source host on a target that may not even have the same kernel installed.",
"diagramCaption": "Every backup ships the same three payloads regardless of destination. The restore consumes all three: rootfs to lay down files, manifest to detect drift and cross-kernel differences, and application inventory to re-install packages and components against the target's own kernel.",
"pillar1Label": "Filesystem",
"pillar1Detail": "rootfs/\n(rsync of\n/etc, /root,\n/var/lib/pve-cluster,\n+ optional paths)",
"pillar1Detail": "rootfs/\n(rsync of /etc,\n/etc/systemd/network,\n/root, plus a\nsqlite3 snapshot\nof /var/lib/pve-cluster,\n+ optional paths)",
"pillar2Label": "Manifest",
"pillar2Detail": "manifest.json\n(hardware, kernel\nparams, network,\nZFS, users, cron,\nZFS pools, storage)",
"pillar3Label": "Applications",
@@ -44,7 +44,7 @@
},
"restoreIsUniversal": {
"heading": "The restore reproduces the source host, not the archive",
"body": "Restoring a ProxMenux backup does not just extract the filesystem. The restore flow reads the manifest to detect drift between the source and the target (hardware differences, NIC renames, kernel version, ZFS pool identity), replays the filesystem, then triggers the correct component installer for every service that was installed on the source (NVIDIA driver, Coral TPU, AMD GPU tools, Intel GPU tools). Each installer runs against the <strong>target's current kernel</strong>, so the restored host does not depend on the source's kernel being present. When the target's kernel is newer than the backup's, a kernel-agnostic <strong>hydration</strong> pass merges the operator's own tuning (IOMMU tokens, VFIO device IDs, custom quirks, GRUB keys) into the target's fresh boot configuration without copying kernel-tied files verbatim."
"body": "Restoring a ProxMenux backup does not just extract the filesystem. The restore flow reads the manifest to detect drift between the source and the target (hardware differences, NIC renames, kernel version, ZFS pool identity), replays the filesystem, then triggers the correct component installer for every service that was installed on the source (NVIDIA driver, Coral TPU, AMD GPU tools, Intel GPU tools). Each installer runs against the <strong>target's current kernel</strong>, so the restored host does not depend on the source's kernel being present. When the target's kernel is newer than the backup's, a kernel-agnostic <strong>hydration</strong> pass merges the user's own tuning (IOMMU tokens, VFIO device IDs, custom quirks, GRUB keys) into the target's fresh boot configuration without copying kernel-tied files verbatim."
},
"twoInterfaces": {
"heading": "Two interfaces, one backend",

File diff suppressed because one or more lines are too long

View File

@@ -39,7 +39,7 @@
"heading": "How attach mode works",
"intro": "PVE writes <code>vzdump</code> tasks to <code>/etc/pve/jobs.cfg</code> — one stanza per task, each pointing at a storage where the VM and LXC dumps land. Attach mode requires that storage to be a backend ProxMenux understands (Local or PBS); when the task fires, ProxMenux runs alongside it.",
"steps": [
{ "step": "1", "detail": "During job creation, ProxMenux lists compatible parent PVE tasks via <code>hb_pve_list_vzdump_jobs_for_backend</code>. The operator picks one." },
{ "step": "1", "detail": "During job creation, ProxMenux lists compatible parent PVE tasks via <code>hb_pve_list_vzdump_jobs_for_backend</code>. The user picks one." },
{ "step": "2", "detail": "The job's <code>.env</code> is written with <code>PVE_PARENT_JOB</code>, <code>PVE_STORAGE</code> and the inherited <code>KEEP_*</code> values; no systemd timer is created." },
{ "step": "3", "detail": "<code>hb_install_vzdump_hook</code> registers a script-hook in <code>/etc/vzdump.conf</code>. When PVE runs any vzdump task, the hook script fires; if the <code>$STOREID</code> passed to it matches an attached job's <code>PVE_STORAGE</code>, the runner is invoked for that job." },
{ "step": "4", "detail": "The archive lands in the same storage the vzdump dumps just wrote to: <code>path/dump/</code> for Local, or the PBS repository configured on the PVE storage entry." }
@@ -78,7 +78,7 @@
},
"notifications": {
"heading": "Notifications",
"body": "Every scheduled run fires the same <code>hb_notify_lifecycle</code> events as the interactive flow (<code>start</code>, <code>complete</code>, <code>fail</code>). If notification channels are configured in the Monitor, unattended jobs surface their results the same way manual backups do — an operator does not need to check the log to know whether a job succeeded."
"body": "Every scheduled run fires the same <code>hb_notify_lifecycle</code> events as the interactive flow (<code>start</code>, <code>complete</code>, <code>fail</code>). If notification channels are configured in the Monitor, unattended jobs surface their results the same way manual backups do — an user does not need to check the log to know whether a job succeeded."
},
"whereNext": {
"heading": "Where to go next",

View File

@@ -153,7 +153,7 @@
{
"href": "/docs/utils/system-update",
"label": "Proxmox System Update (interactive)",
"tail": " — wrapper that does all of this with a confirmation dialog and reboot prompt."
"tail": " — wrapper that does all of this with a confirmation dialog, an automatic DKMS rebuild of ProxMenux-managed drivers when a new kernel is installed, and a reboot prompt. Also launchable from the Update Now button of the ProxMenux Monitor dashboard."
},
{
"href": "/docs/utils/upgrade-pve8-pve9",

View File

@@ -57,7 +57,7 @@
},
"updating": {
"heading": "Updating",
"body": "ProxMenux self-updates. When a new version is available, you're prompted on the next <code>menu</code> launch and accepting replaces utility files and configurations in place. No manual download needed. Stable users get stable releases; beta users get beta releases (and the auto-switch prompt above when a stable cuts over)."
"body": "ProxMenux self-updates. When a new version is available, the next <code>menu</code> launch offers the update; accepting replaces utility files and configurations in place. No manual download needed. Stable users get stable releases; beta users get beta releases (and the auto-switch prompt above when a stable cuts over). The prompt shape depends on where <code>menu</code> runs: over SSH or the Proxmox console it is a classic yes / no dialog, and accepting applies the update immediately. Inside the WebSocket terminal of ProxMenux Monitor it is replaced by an informational msgbox — that terminal cannot survive a self-update mid-flight, so the msgbox shows the canonical one-line installer (<code>bash -c \"$(wget -qLO - https://raw.githubusercontent.com/MacRimi/ProxMenux/main/install_proxmenux.sh)\"</code> for stable, or the equivalent <code>install_proxmenux_beta.sh</code> URL for beta) to run from SSH or console."
},
"uninstall": {
"heading": "Uninstalling",

View File

@@ -10,7 +10,7 @@
},
"intro": {
"title": "A real PTY in the browser",
"body": "The terminal allocates a server-side PTY through <code>flask_terminal_routes</code>, pipes it over a WebSocket to <code>xterm.js</code> in the browser, and runs as <code>root</code> (the systemd unit's user). Anything you can do in <code>ssh root@&lt;host&gt;</code> works here — including <code>vim</code>, <code>tmux</code>, ncurses tools and Proxmox CLIs (<code>qm</code>, <code>pct</code>, <code>pvesh</code>, <code>pvecm</code>)."
"body": "The terminal allocates a server-side PTY through <code>flask_terminal_routes</code>, pipes it over a WebSocket to <code>xterm.js</code> in the browser, and runs as <code>root</code> (the systemd unit's user). Anything you can do in <code>ssh root@&lt;host&gt;</code> works here — including <code>vim</code>, <code>tmux</code>, ncurses tools and Proxmox CLIs (<code>qm</code>, <code>pct</code>, <code>pvesh</code>, <code>pvecm</code>). Every PTY started from this tab exports the environment variable <code>PROXMENUX_TERMINAL=monitor</code>, which is inherited by every child <code>bash</code> process — including a subsequent <code>menu</code> launch — so ProxMenux flows can tell when they are running inside the Monitor's browser terminal."
},
"singleAlt": "ProxMenux Monitor Terminal tab — single terminal session showing Fastfetch system summary on login",
"singleCaption": "One host terminal open — the toolbar above shows the count (<em>1 / 4 terminals</em>), <em>+ New</em>, <em>Search</em>, <em>Clear</em> and <em>Close</em>. The mobile keyboard helpers appear under the terminal on touch devices.",
@@ -19,6 +19,11 @@
"body1": "The Terminal tab opens a shell on the <strong>Proxmox host itself</strong> — the same login you would get over SSH. Each tab opens a brand-new host terminal.",
"body2": "To reach an <strong>LXC container</strong> from the browser, use the dedicated <em>Console</em> button on every running CT card in the <link>VMs & LXCs tab</link>. It opens a modal that runs <code>pct enter &lt;vmid&gt;</code> and reuses the same mobile-friendly toolbar described below."
},
"menuGuard": {
"heading": "Self-update guard when menu runs inside this terminal",
"body1": "ProxMenux's <code>menu</code> self-updates in place: accepting the prompt replaces its own script tree and reloads. That works cleanly over SSH or the Proxmox console, but the browser terminal cannot survive it — the update tears down the very PTY the shell is running in, and the WebSocket drops mid-flight. To avoid stranding the session, the internal handlers <code>check_updates_stable</code>, <code>check_updates_beta</code> and <code>apply_release_channel</code> read <code>PROXMENUX_TERMINAL</code> and detect the Monitor context.",
"body2": "When they detect it, the classic yes / no dialog is replaced by an informational msgbox with the canonical one-line installer — <code>bash -c \"$(wget -qLO - https://raw.githubusercontent.com/MacRimi/ProxMenux/main/install_proxmenux.sh)\"</code> for stable, and the equivalent <code>install_proxmenux_beta.sh</code> URL for beta. The update itself is deferred to an SSH session or the Proxmox host console; the browser terminal is used only to acknowledge and read the command."
},
"fourTerminals": {
"heading": "Up to four terminals at once",
"intro": "The tab lets you open up to four host terminals simultaneously. Each one gets its own PTY and its own WebSocket — they are fully independent sessions. Two layouts switch with the icons next to the \"New\" button:",

View File

@@ -98,7 +98,7 @@
{
"category": "System Updates",
"checks": "Pending updates, security updates, kernel / PVE version, system age",
"events": "Security updates available; pinned kernel several minor versions behind; host uptime > 90 days."
"events": "Security updates available; pinned kernel several minor versions behind; host uptime > 90 days. When updates are pending, the Overview header shows an <em>Update Now</em> button that runs the full ProxMenux update flow inline; on close it re-hits the health endpoint with <code>?refresh=1</code> so the badge count reflects the new state without waiting for the next 5-min cycle."
},
{
"category": "Security & Certificates",
@@ -151,11 +151,14 @@
"<strong>Dismissed</strong> — items previously acknowledged by the user that are still inside their suppression window. Each row shows how much of the suppression remains and the configured duration. When the window expires, the item disappears from this list; if the underlying condition is still present and the category supports re-firing, it re-appears in <em>Active</em>."
],
"pillTitle": "The pill mirrors the worst category",
"pillBody": "The dashboard header colour is the highest severity across the ten categories: any CRITICAL → red, else any WARNING → yellow, else any INFO → blue, else green. The same logic drives the favicon dot and the PWA badge."
"pillBody": "The dashboard header colour is the highest severity across the ten categories: any CRITICAL → red, else any WARNING → yellow, else any INFO → blue, else green. The same logic drives the favicon dot and the PWA badge.",
"updateNowTitle": "Update Now button in the Overview header",
"updateNowBody1": "When the <em>System Updates</em> category reports one or more pending updates, the Overview header renders an <strong>Update Now</strong> button next to the health pill. The button opens a modal that runs the same host-side flow as <em>ProxMenux → Settings post-install Proxmox → Proxmox System Update</em>: repo hygiene, <code>apt full-upgrade</code> via the safe worker <code>update-pve-safe.sh</code>, automatic DKMS rebuild of ProxMenux-managed drivers when a new kernel lands, autoremove / autoclean and a reboot prompt if the kernel changed. Progress lines stream in the modal as the worker prints them.",
"updateNowBody2": "When the run finishes, closing the modal calls <code>GET /api/health/details?refresh=1</code>, which invalidates the health-endpoint cache before serving. The pending-updates row and the pill severity are recomputed from the new package state on the spot, so the badge disappears without waiting for the next five-minute cycle. When the host is already up to date, the button is hidden — no visual noise on a healthy system."
},
"dismiss": {
"heading": "Dismissing alerts and the Suppression Duration",
"intro": "Some events are noisy by nature — a <em>System Updates: pending updates available</em> stays true until you patch the host, and you don't want a notification every five minutes for a week. The Health Monitor solves this with two coupled mechanisms:",
"intro": "Some events are noisy by nature — a <em>System Updates: pending updates available</em> stays true until you patch the host, and you don't want a notification every five minutes for a week. The <em>Update Now</em> button in the Overview header is the direct shortcut to resolve that specific case in place; for everything else, or when patching is deferred, the Health Monitor also offers two coupled silencing mechanisms:",
"step1": "<strong>Per-event Dismiss action</strong> in the modal. The Dismiss button opens a small dropdown with three options — <strong>24 hours</strong>, <strong>7 days</strong> or <strong>Permanently</strong> — letting you choose how long this specific alert stays silenced regardless of the category's default. Picking one calls <code>POST /api/health/acknowledge</code> with the <code>error_key</code> and the chosen <code>suppression_hours</code> (<code>-1</code> for permanent). The event moves to the Dismissed list with a timestamped <code>acknowledged_at</code>.",
"dropdownImageAlt": "Dismiss dropdown on a Health Monitor alert — 24 hours, 7 days or Permanently",
"dropdownImageCaption": "Per-event Dismiss dropdown. The chosen window applies to this single alert; if no per-event window is selected the category's default is used. Permanent dismisses are tagged with a distinct amber <em>Permanent</em> badge in the Dismissed list and never re-fire.",

View File

@@ -67,7 +67,7 @@
},
{
"tool": "Network stack tuning",
"what": "TCP buffer sizing, IPv4 hardening (redirects off, rp_filter=2, martian log off), local port range 1024-65535, TCP MTU probing, RFC 1337, plus a oneshot systemd unit to normalise virtual firewall bridges.",
"what": "TCP buffer sizing, IPv4 hardening (redirects off, rp_filter=2, martian log off), local port range 1024-65535, TCP MTU probing, RFC 1337, plus a helper at /usr/local/sbin/proxmenux-fwbr-tune wired to a oneshot systemd unit and a udev rule, so rp_filter=0 / log_martians=0 are re-applied to fwbr*/fwln*/fwpr*/tap* interfaces every time Proxmox recreates them at VM start/stop, reboot or live migration.",
"category": "Network",
"categorySlug": "network"
},
@@ -103,7 +103,7 @@
},
{
"tool": "Persistent interface names",
"what": "Writes /etc/systemd/network/10-*.link files matching each physical NIC by MAC so eth0 / enp… names stay stable across reboots and new NIC additions.",
"what": "Writes one /etc/systemd/network/10-proxmenux-<iface>.link per physical NIC (each starting with a 'Managed by ProxMenux' header) that pins the MAC to the current name, so eth0 / enp… names stay stable across reboots and new NIC additions.",
"category": "Network",
"categorySlug": "network"
}

View File

@@ -54,6 +54,8 @@
}
],
"sourceOutro": "It also adds <code>source /etc/network/interfaces.d/*</code> to <code>/etc/network/interfaces</code> if not already present — standard practice so you can drop modular interface snippets without editing the main file.",
"fwbrTitle": "Automatic tuning of virtual firewall bridges",
"fwbrBody": "Alongside the sysctl profile, ProxMenux installs a helper at <code>/usr/local/sbin/proxmenux-fwbr-tune</code> that applies <code>rp_filter=0</code> and <code>log_martians=0</code> to the <code>fwbr*</code> / <code>fwln*</code> / <code>fwpr*</code> / <code>tap*</code> interfaces Proxmox creates around VMs and containers. The helper is invoked by the <code>proxmenux-fwbr-tune.service</code> one-shot unit at boot, and by the <code>/etc/udev/rules.d/99-proxmenux-fwbr-tune.rules</code> rule on every <code>net add</code> event matching those prefixes — covering interfaces that Proxmox recreates on VM start/stop, reboot and live migration.",
"rpFilterTitle": "Why rp_filter=2 (loose) instead of 1 (strict)",
"rpFilterBody": "Strict reverse-path filtering drops packets whose source would be routed out a <em>different</em> interface. That's the right default on a client machine, but breaks badly on a Proxmox host where VM traffic often arrives on a bridge and leaves on an uplink with asymmetric routes. <code>rp_filter=2</code> (loose) only drops packets with truly unroutable sources. It's a pragmatic trade-off — slight reduction in local-IP-spoof detection in exchange for not breaking your VM network."
},
@@ -84,14 +86,14 @@
"LXC containers with <code>hotplug</code> NICs and bonded links can race on boot and end up named inconsistently. Pinning fixes that."
],
"writtenTitle": "What gets written",
"writtenIntro": "One file per physical NIC, at <code>/etc/systemd/network/10-&lt;iface&gt;.link</code>:",
"writtenOutro": "Any pre-existing <code>.link</code> files in that directory are copied to <code>/etc/systemd/network/backup-&lt;timestamp&gt;/</code> before touching anything.",
"writtenIntro": "One file per physical NIC, at <code>/etc/systemd/network/10-proxmenux-&lt;iface&gt;.link</code>. Each file starts with the header <code># Managed by ProxMenux — do not edit</code> so ProxMenux can distinguish its own files from user- or package-provided <code>.link</code> files.",
"writtenOutro": "<code>.link</code> files from other packages or hand-written by the user are left in place — only files carrying the ProxMenux header are managed. On re-run, the function reconciles its own set: it re-reads <code>MACAddress=</code> from every <code>10-proxmenux-*.link</code> and drops any whose MAC no longer appears under <code>/sys/class/net/</code>. Files written by an earlier ProxMenux release in the old <code>10-&lt;iface&gt;.link</code> format are migrated automatically the first time.",
"pveTitle": "PVE 9 vs PVE 8",
"pveBody": "On Proxmox VE 9 (<code>systemd-networkd</code> native), the script reloads udev rules after writing the <code>.link</code> files so new hotplug NICs pick up the correct name without a reboot. On PVE 8 (<code>ifupdown2</code>), interface naming is resolved at boot anyway — a reboot is required for the changes to take effect. The script sets the reboot flag either way so Customizable prompts you.",
"reviewTitle": "Review existing /etc/network/interfaces first",
"reviewBody": "If your host has legacy configuration in <code>/etc/network/interfaces</code> that references NIC names generated by the kernel's default scheme, pinning <em>today's</em> names is exactly what you want. But if you've already manually customised the config around specific names, double-check the pinning matches what the interfaces file expects before rebooting.",
"revertTitle": "Reversible from the Uninstall menu",
"revertBody": "<link>Uninstall Optimizations</link> deletes every <code>.link</code> file from <code>/etc/systemd/network/</code>, restoring the kernel's default naming on next reboot. The timestamped backup of the original files stays behind in case you need to restore specific ones manually."
"revertBody": "<link>Uninstall Optimizations</link> deletes only the <code>10-proxmenux-*.link</code> files carrying the ProxMenux header from <code>/etc/systemd/network/</code>. Any <code>.link</code> file added by the user or by another package is preserved. Interface names return to systemd's default behaviour on next reboot."
},
"related": {
"heading": "Related",

View File

@@ -145,10 +145,11 @@
"doesLabel": "What ProxMenux does:",
"doesItems": [
"Detects whether the root disk is SSD/NVMe by reading <code>/sys/block/&lt;dev&gt;/queue/rotational</code>. On a rotational disk the Automated flow asks first before installing.",
"Clones the upstream repository (<code>azlux/log2ram</code>) into <code>/tmp/log2ram</code> and runs its <code>install.sh</code>, then enables the <code>log2ram</code> systemd unit.",
"Clones the upstream repository (<code>azlux/log2ram</code>) into <code>/tmp/log2ram</code> and patches its <code>install.sh</code>, replacing <code>rsync -aAXv</code> with <code>rsync -aXv --no-acls</code>. That adjustment avoids the <em>set_acl: Operation not supported</em> exit 23 failure that the vanilla script triggers when moving /var/log on filesystems without ACL support. The <code>log2ram</code> systemd unit is enabled after install.",
"Sizes the ramdisk based on host RAM: <code>≤ 8 GB → 128M</code>, <code>≤ 16 GB → 256M</code>, <code>&gt; 16 GB → 512M</code>. Writes the value to <code>SIZE=</code> in <code>/etc/log2ram.conf</code>.",
"Schedules a periodic disk sync via <code>/etc/cron.d/log2ram</code>: every 1h / 3h / 6h according to the same RAM tier.",
"Installs an auto-sync guard at <code>/usr/local/bin/log2ram-check.sh</code>, wired to <code>/etc/cron.d/log2ram-auto-sync</code> to run every 10 minutes. When <code>/var/log</code> reaches 80% of the ramdisk size the guard vacuums journald; at 92% it also truncates <code>pveproxy access/error</code> and <code>pveam.log</code> before syncing — the plain <code>log2ram write</code> command copies tmpfs to disk but does NOT shrink the tmpfs, so this guard prevents PVE from crashing with <em>No space left on device</em> when logs grow uncontrolled.",
"Installs an auto-sync guard at <code>/usr/local/bin/log2ram-check.sh</code>, wired to <code>/etc/cron.d/log2ram-auto-sync</code> to run every 10 minutes. When <code>/var/log</code> reaches 80% of the ramdisk size the guard vacuums journald; at 92% it first runs <code>logrotate -f /etc/logrotate.d/proxmox-backup-api</code> when that file exists (forcing rotation of the PBS API logs, typically the largest consumer on a host running PBS) and then truncates <code>pveproxy access/error</code> and <code>pveam.log</code> before syncing. The plain <code>log2ram write</code> command copies tmpfs to disk but does NOT shrink the tmpfs, so this guard prevents PVE from crashing with <em>No space left on device</em> when logs grow uncontrolled.",
"Detects whether <code>proxmox-backup-server</code> runs as a service on the host. When present, drops <code>/etc/logrotate.d/proxmox-backup-api</code> (20 MB × 3 rotation) and <code>/etc/cron.hourly/proxmox-backup-logrotate</code>, so the PBS API logs — the usual source of growth on a PBS host — stay bounded even between Log2RAM's periodic syncs. On hosts without PBS this step is a no-op.",
"Adjusts systemd-journald limits (<code>SystemMaxUse</code>, <code>RuntimeMaxUse</code>) to fit within the ramdisk so a single burst cannot fill it.",
"Registers itself in <code>installed_tools.json</code> so it can be reverted from Uninstall Optimizations."
],

View File

@@ -18,30 +18,26 @@
"intro": "The <strong>Adaptive Replacement Cache (ARC)</strong> is ZFS's in-memory read cache. Without explicit tuning, ZFS happily grabs up to half the host RAM for itself, which is excessive on a Proxmox host that also needs memory for VMs and LXCs. This option caps ARC to a sane fraction of total RAM based on the size of the machine.",
"sizingTitle": "Sizing rules",
"headerRam": "Host RAM",
"headerMin": "ARC min",
"headerMax": "ARC max",
"headerMax": "ARC cap",
"rows": [
{
"ram": "≤ 16 GB",
"min": "512 MB",
"max": "512 MB"
"max": "512 MiB"
},
{
"ram": "17 32 GB",
"min": "1 GB",
"max": "1 GB"
"max": "1 GiB"
},
{
"ram": "> 32 GB",
"min": "RAM / 16",
"max": "RAM / 8"
"max": "RAM / 8 (floor 512 MiB)"
}
],
"after": "On a 64 GB host, that means 4 GB min / 8 GB max for ARC. The config is written to <code>/etc/modprobe.d/99-zfsarc.conf</code> and enables a few extra ZFS tunables (L2ARC prefetch on, L2ARC write max at 500 MB, longer TXG timeout).",
"after": "On a 64 GB host, that means an 8 GB cap for ARC. The file <code>/etc/modprobe.d/99-zfsarc.conf</code> contains a single directive — <code>options zfs zfs_arc_max=…</code>. Every other ZFS module parameter (<code>zfs_arc_min</code>, L2ARC prefetch/write throttle, TXG timeout) is left at its OpenZFS default. After writing the file, ProxMenux runs <code>update-initramfs -u -k all</code> and, when applicable, <code>proxmox-boot-tool refresh</code>, so the cap also lands in the initramfs used by ZFS-on-root setups.",
"rebootTitle": "Requires a reboot to take effect",
"rebootBody": "ARC settings are read when the <code>zfs</code> kernel module loads. They do <strong>not</strong> apply on a live system — you'll need to reboot the host for the cap to kick in. The script sets the \"reboot required\" flag automatically.",
"rebootBody": "ARC settings are read when the <code>zfs</code> kernel module loads. To make the cap take effect on ZFS-on-root hosts, ProxMenux regenerates the initramfs with <code>update-initramfs -u -k all</code> and, when applicable, refreshes the boot loader with <code>proxmox-boot-tool refresh</code>. A reboot is still required to pick up the new module parameter; the \"reboot required\" flag is set automatically.",
"safeTitle": "Safe on non-ZFS hosts",
"safeBody": "The function checks for the <code>zfs</code> command before touching anything. On ext4 / LVM-only Proxmox hosts, ticking this option is a no-op — nothing gets written.",
"safeBody": "The function only writes the config when <code>zpool list</code> reports at least one imported ZFS pool. On ext4 / LVM-only Proxmox hosts, or on machines with the ZFS tools installed but no pool imported, ticking this option is a no-op — nothing gets written.",
"verifyTitle": "Verification and manual rollback"
},
"autoSnap": {

View File

@@ -88,11 +88,11 @@
"items": [
{
"tool": "Network Optimizations",
"restores": "Removes /etc/sysctl.d/99-network.conf and the proxmenux-fwbr-tune.service unit. Reloads sysctl and systemd."
"restores": "Removes /etc/sysctl.d/99-network.conf together with the proxmenux-fwbr-tune.service unit, the /usr/local/sbin/proxmenux-fwbr-tune helper and the /etc/udev/rules.d/99-proxmenux-fwbr-tune.rules udev rule. Reloads sysctl, systemd and the udev ruleset."
},
{
"tool": "Persistent Interface Names",
"restores": "Removes every .link file from /etc/systemd/network/. Interface names return to systemd's default behaviour on next reboot."
"restores": "Removes only the 10-proxmenux-*.link files carrying the ProxMenux header from /etc/systemd/network/. Any .link file provided by another package or written by the user is left in place. Interface names return to systemd's default behaviour on next reboot."
}
]
},
@@ -171,7 +171,7 @@
"packageTitle": "Package reinstall touches live Proxmox packages",
"packageBody": "Reverting <strong>Subscription Banner Removal</strong> reinstalls <code>pve-manager</code>, <code>proxmox-widget-toolkit</code>, <code>libjs-extjs</code> and <code>libpve-http-server-perl</code> with <code>--force-confnew</code>. This is generally safe but does touch the running web UI — refresh your browser afterwards, and expect a few seconds of reconnection. Don't run this in the middle of a migration or clone operation.",
"rebootTitle": "Persistent names and VFIO need a reboot",
"rebootBody": "Removing the <code>.link</code> files (<em>Persistent Interface Names</em>) and reverting <em>IOMMU/VFIO</em> do not affect the running system — they only matter after a reboot. ProxMenux sets the reboot flag automatically for these.",
"rebootBody": "Removing ProxMenux's <code>10-proxmenux-*.link</code> files (<em>Persistent Interface Names</em>) and reverting <em>IOMMU/VFIO</em> do not affect the running system — they only matter after a reboot. ProxMenux sets the reboot flag automatically for these.",
"perItemTitle": "You can revert one thing and keep the rest",
"perItemBody": "The uninstaller operates per-item. If you only want to remove Log2RAM but keep the network tuning and bashrc changes, tick only <em>Log2RAM</em>. Nothing else is touched, and the registry is updated accordingly."
},

View File

@@ -53,7 +53,8 @@
"Your existing config, ProxMenux Monitor login (<code>auth.json</code>), notification channels, post-install registry, custom thresholds — none of those are touched. The channel switch only swaps the script tree and the binary AppImage.",
"A Monitor that was active before the switch stays active after; one that was deactivated stays deactivated.",
"ProxMenux Monitor <code>auth.json</code>, API tokens and the JWT secret are preserved across channel changes. Sessions don't get logged out.",
"You can flip channels as many times as you like — the dialog will accept it every time and rerun the installer. No restart of Proxmox is required."
"You can flip channels as many times as you like — the dialog will accept it every time and rerun the installer. No restart of Proxmox is required.",
"The channel switch installer replaces ProxMenux's own script tree, so it cannot run inside the Monitor's browser terminal (that PTY would be torn down mid-update and leave the dialog stranded). If you launch the option from there, the confirmation is replaced by an informational msgbox showing the canonical one-line installer for the target channel; run that command from an SSH session or the Proxmox host console."
]
},
"feedback": {

View File

@@ -7,7 +7,7 @@
},
"header": {
"title": "Proxmox System Update",
"description": "Wrapper that detects the running Proxmox major version and delegates to the matching worker (PVE 8 or PVE 9). Repos are cleaned up, the no-subscription source is enabled, all packages are upgraded, conflicting packages are removed, and the system is cleaned up afterwards. A reboot prompt fires only when the kernel was actually updated.",
"description": "Wrapper that delegates to a single safe worker (<code>update-pve-safe.sh</code>) which detects the running Proxmox major version by itself. Repositories are cleaned up when they overlap with the base Proxmox / Debian sources, all packages are upgraded, ProxMenux-managed DKMS drivers are rebuilt if a new kernel landed, and the reboot prompt fires only when the kernel actually changed. Also launchable from the <em>Update Now</em> button in the ProxMenux Monitor dashboard header when pending updates are detected.",
"section": "Utilities"
},
"calloutWhat": {
@@ -22,15 +22,15 @@
},
"onTop": {
"heading": "What ProxMenux runs on top — verified against the script",
"intro": "This option runs <strong>exactly</strong> the apt command above, wrapped with the repo hygiene and post-upgrade cleanup the official upgrade guide also recommends. The list below maps 1:1 to <code>scripts/utilities/proxmox_update.sh</code> and the per-version worker scripts — nothing implied, every step is in the code:",
"intro": "This option runs <strong>exactly</strong> the apt command above, wrapped with the repo hygiene, DKMS rebuild for ProxMenux-managed drivers and post-upgrade cleanup the official upgrade guide also recommends. Everything below maps 1:1 to <code>scripts/utilities/proxmox_update.sh</code> and the single safe worker <code>scripts/global/update-pve-safe.sh</code> — nothing implied, every step is in the code:",
"items": [
"<strong>Detects the PVE major version</strong> (<code>pveversion | grep -oP ''pve-manager/\\K[0-9]+''</code>) and dispatches to <code>update-pve8.sh</code> or <code>update-pve9_2.sh</code> so the right codename and repo URLs are used.",
"<strong>Cleans up repositories</strong> before touching apt: disables the enterprise source (which 401s without a subscription), removes legacy repo files, and writes a clean no-subscription source for the host's codename.",
"<strong>Runs the upgrade non-interactively</strong> with <code>DEBIAN_FRONTEND=noninteractive</code> and <code>--force-confdef --force-confold</code> — meaning if a configuration file you already modified also changed upstream, your version stays in place. No silent overwrites of custom configs.",
"<strong>Installs essential Proxmox packages</strong> if any are missing (<code>zfsutils-linux</code>, <code>proxmox-backup-restore-image</code>, <code>chrony</code>).",
"<strong>Detects the PVE major version</strong> from inside the worker (<code>pveversion | grep -oP ''pve-manager/\\K[0-9]+''</code>) and adapts the base Proxmox / Debian repo URLs (bookworm on PVE 8, trixie on PVE 9). There is no fan-out to per-version worker scripts — a single safe worker handles both.",
"<strong>Cleans up repositories in a conservative way.</strong> <code>ensure_repositories</code> runs first but only when the base Proxmox / Debian sources are missing — a bare host gets them written, a configured host is a no-op. <code>cleanup_duplicate_repos</code> then removes exact URL + Suite + Component duplicates only against <code>proxmox.sources</code> / <code>debian.sources</code>; user-authored files (enterprise, Ceph, alternative NTP mirrors, custom <code>download.proxmox.com/*</code> entries or hand-written <code>pve-*.list</code>) are left untouched, and every file is backed up before being edited.",
"<strong>Runs the upgrade non-interactively</strong> with <code>DEBIAN_FRONTEND=noninteractive</code> and <code>--force-confdef --force-confold</code> — if a configuration file you already modified also changed upstream, your version stays in place. No silent overwrites of custom configs.",
"<strong>Skips forcing optional utilities.</strong> The safe worker does not push <code>zfsutils-linux</code>, <code>chrony</code>, <code>ifupdown2</code> or similar packages onto the host — a Proxmox install that opted out of any of them keeps its choice. Missing packages are surfaced by the higher-level installer flows, not by the update path.",
"<strong>LVM metadata sanity check</strong> against stray PV headers from passthrough disks (warn-only, no automatic fix).",
"<strong>Cleans up afterwards:</strong> <code>apt-get autoremove -y</code> + <code>apt-get autoclean -y</code>.",
"<strong>Reboot prompt</strong> only if the kernel actually changed (<code>/var/run/reboot-required</code> present or <code>linux-image</code> in the upgrade log)."
"<strong>DKMS rebuild before the reboot prompt.</strong> When the upgrade staged a new kernel, the wrapper calls <code>pmx_rebuild_dkms_after_kernel</code> to rebuild every driver that ProxMenux installed via DKMS against the incoming kernel version — so the modules are ready before the box comes back up. Reboot detection uses <code>/var/run/reboot-required</code> when <code>needrestart</code> is present, and falls back to a <code>dpkg-query</code> comparison between the running kernel and the newest installed <code>proxmox-kernel-*-pve-signed</code> / <code>pve-kernel-*-pve</code> package when it isn't — a signal that survives the many Proxmox hosts that ship without <code>needrestart</code>."
]
},
"calloutOneSentence": {
@@ -50,8 +50,8 @@
"detail": "pveversion |\ngrep -oP ''pve-manager/\\K[0-9]+''"
},
"bridge": {
"label": "Worker selection",
"detail": "PVE 8 → update-pve8.sh\nPVE 9 → update-pve9_2.sh"
"label": "Single safe worker",
"detail": "update-pve-safe.sh\n(detects PVE 8 / 9\ninternally)"
},
"target": {
"label": "Post-update",
@@ -61,13 +61,18 @@
},
"worker": {
"heading": "What the worker does",
"intro": "Both workers (<code>scripts/global/update-pve8.sh</code> for PVE 8 and <code>scripts/global/update-pve9_2.sh</code> for PVE 9) follow the same outline, with version-appropriate repo URLs and package names:",
"intro": "A single worker (<code>scripts/global/update-pve-safe.sh</code>) handles both PVE 8 and PVE 9. It detects the major version internally and uses the version-appropriate codename (<code>bookworm</code> or <code>trixie</code>) for its base sources. The stages are:",
"items": [
"<strong>Repo hygiene.</strong> Removes duplicate entries from <code>/etc/apt/sources.list</code> and <code>/etc/apt/sources.list.d/</code>. Comments out the enterprise repo if the host has no subscription and writes / enables the no-subscription source.",
"<strong>Apt update + full-upgrade.</strong> Pulls the latest package lists and applies all available upgrades for the current major version, running with <code>DEBIAN_FRONTEND=noninteractive</code> and <code>--force-confdef --force-confold</code> so any configuration file you customised keeps its current contents when upstream also changed it.",
"<strong>Essential packages check.</strong> Installs <code>zfsutils-linux</code>, <code>chrony</code>, <code>ifupdown2</code> and a few others if the host is missing them.",
"<strong>LVM / storage sanity check.</strong> Repairs missing PV headers if detected.",
"<strong>Conflicting package removal.</strong> Drops packages known to clash on Proxmox (e.g. some time-sync daemons that fight chrony)."
"<strong>Sanity checks.</strong> Verifies at least ~1 GB free in <code>/var/cache/apt/archives</code> and pings <code>download.proxmox.com</code>. Aborts early with a clear message when either fails, so the run doesn't die in the middle of an apt transaction.",
"<strong>Repo bootstrap.</strong> <code>ensure_repositories</code> writes the base Proxmox / Debian sources only when they are missing (a fresh or hand-cleaned host); on a configured host it does nothing.",
"<strong>Apt update with GPG auto-recovery.</strong> On <code>NO_PUBKEY</code> for any repo (yours or a third-party one) the worker imports the missing key and retries automatically before failing.",
"<strong>Conservative duplicate cleanup.</strong> <code>cleanup_duplicate_repos</code> only removes exact URL + Suite + Component matches against <code>proxmox.sources</code> / <code>debian.sources</code>. User-authored files — enterprise, Ceph, alternative NTP mirrors, custom <code>download.proxmox.com/*</code> entries, hand-written <code>pve-*.list</code> — are left intact. Every file is backed up before modification.",
"<strong>Pending upgrades + security count.</strong> Reports how many packages will change and how many of those come from the security suite, so the confirmation dialog has real numbers to show.",
"<strong>Confirmation dialog.</strong> The wrapper asks for an explicit yes before touching apt.",
"<strong>apt full-upgrade.</strong> Runs with <code>DEBIAN_FRONTEND=noninteractive</code> and <code>--force-confdef --force-confold</code> so any configuration file you customised keeps its current contents when upstream also changed it. Never overwrites operator-edited configs silently.",
"<strong>LVM sanity check.</strong> <code>lvm_repair_check</code> refreshes VG metadata when disks passed through to guest VMs (DSM, TrueNAS, storage appliances) come back with old PV headers.",
"<strong>DKMS rebuild for ProxMenux-managed drivers.</strong> When the upgrade staged a new kernel, <code>pmx_rebuild_dkms_after_kernel</code> reads <code>components_status.json</code> for the drivers ProxMenux installed (currently <code>nvidia_driver</code> → module <code>nvidia</code>, <code>coral_driver</code> → module <code>gasket</code>), installs the matching kernel headers (<code>proxmox-headers-&lt;newkver&gt;</code> when available, else <code>pve-headers-&lt;newkver&gt;</code>) and runs <code>dkms autoinstall -k &lt;newkver&gt;</code>. If <code>dkms status</code> then doesn't show the modules built against the new kernel, the worker falls back to re-running each installer with <code>--auto-reinstall</code>. Any failure is logged but does not abort the update — you land on the reboot prompt in every case.",
"<strong>Post-cleanup.</strong> <code>apt-get autoremove</code> + <code>apt-get autoclean</code> before returning control to the wrapper."
]
},
"post": {
@@ -91,14 +96,14 @@
"body": "Running on an old kernel after upgrading <code>linux-image-*</code> means you're on a half-upgraded system: userspace is new, kernel is old. Most of the time things work, but ZFS modules, IOMMU groups, KSMBD and any out-of-tree drivers will only match the kernel they were built for — a mismatch produces obscure failures. Reboot at the earliest sensible moment."
},
"noSub": {
"heading": "When the no-subscription switch happens",
"intro": "Proxmox ships hosts with the enterprise repo enabled by default. Without a paid subscription, that repo returns 401 on <code>apt-get update</code>. The worker detects this and:",
"heading": "How the safe worker treats the enterprise repo",
"intro": "Proxmox ships hosts with the enterprise repo enabled by default. Without a paid subscription, that repo returns 401 on <code>apt-get update</code>. The safe worker deliberately does <strong>not</strong> touch enterprise or Ceph repositories — a host running with a real subscription must not have its config silently rewritten. What happens instead:",
"items": [
"Comments out (or disables) <code>/etc/apt/sources.list.d/pve-enterprise.list</code> (or the deb822 equivalent)",
"Writes <code>/etc/apt/sources.list.d/pve-no-subscription.list</code> (or the deb822 <code>proxmox.sources</code> for PVE 9) with the matching codename (<code>bookworm</code> for PVE 8, <code>trixie</code> for PVE 9)",
"Re-runs <code>apt-get update</code>"
"On a <strong>bare host</strong> with no base Proxmox / Debian sources at all, <code>ensure_repositories</code> writes the no-subscription source in the deb822 format (<code>proxmox.sources</code>) with the codename matching the detected major version (<code>bookworm</code> for PVE 8, <code>trixie</code> for PVE 9) and the matching Debian sources.",
"On a <strong>configured host</strong>, <code>ensure_repositories</code> is a no-op — whatever the operator chose (no-subscription, enterprise, or a mix) is preserved.",
"The enterprise <code>pve-enterprise.sources</code> / <code>ceph.sources</code> files are never modified by the update path. The removal of the enterprise repo when it's unwanted is handled elsewhere in ProxMenux (the Automated post-install script), not here."
],
"outro": "If you have a paid subscription, comment out the no-subscription source and uncomment the enterprise one before running this option."
"outro": "If you have a paid subscription, keep <code>pve-enterprise.sources</code> enabled and the safe worker will let it drive the upgrade unchanged. If you don't, either run the Automated post-install first (it does the switch and records it) or comment the enterprise source out manually — the update path will not do it for you."
},
"cluster": {
"heading": "Cluster considerations",
@@ -127,7 +132,7 @@
},
{
"title": "Kernel upgraded but the new modules are missing for an out-of-tree driver",
"body": "Out-of-tree modules (NVIDIA, ZFS via DKMS, custom NIC drivers) need to be rebuilt against the new kernel. Most are handled automatically by DKMS during the upgrade — confirm with <code>dkms status</code>. If something is missing: <code>dkms autoinstall</code>."
"body": "The drivers ProxMenux installed via DKMS (currently <code>nvidia_driver</code> and <code>coral_driver</code>) are rebuilt automatically at the end of the upgrade against the incoming kernel version, using <code>dkms autoinstall -k &lt;newkver&gt;</code> and, when needed, a fallback to each installer with <code>--auto-reinstall</code>. Confirm with <code>dkms status</code>. Third-party out-of-tree modules that aren't in ProxMenux's <code>components_status.json</code> registry (custom NIC drivers, hand-installed DKMS packages, …) still need a manual <code>dkms autoinstall</code> — the safe worker only touches what it originally installed."
},
{
"title": "The reboot prompt didn't appear but I'm sure the kernel changed",
@@ -137,7 +142,7 @@
},
"files": {
"heading": "Files involved",
"code": "scripts/utilities/proxmox_update.sh # this script (wrapper)\nscripts/global/update-pve8.sh # worker for PVE 8 hosts\nscripts/global/update-pve9_2.sh # worker for PVE 9 hosts\nscripts/global/common-functions.sh # cleanup_duplicate_repos used by workers\n/etc/apt/sources.list # may be edited\n/etc/apt/sources.list.d/* # may be edited / created\n/var/run/reboot-required # read to decide on reboot prompt\n/var/log/apt/history.log # read to detect kernel changes"
"code": "scripts/utilities/proxmox_update.sh # this script (wrapper)\nscripts/global/update-pve-safe.sh # single safe worker (PVE 8 + PVE 9)\nscripts/global/common-functions.sh # cleanup_duplicate_repos used by the worker\nscripts/global/utils-install-functions.sh # ensure_repositories + pmx_rebuild_dkms_after_kernel\n/usr/local/share/proxmenux/components_status.json # ProxMenux-managed DKMS driver registry\n/etc/apt/sources.list.d/proxmox.sources # deb822 no-subscription source (bare-host bootstrap)\n/etc/apt/sources.list.d/debian.sources # deb822 Debian sources (bare-host bootstrap)\n/var/run/reboot-required # read to decide on reboot prompt\n# Reboot fallback when needrestart isn't installed:\n# dpkg-query -W 'proxmox-kernel-*-pve-signed' 'pve-kernel-*-pve' vs. uname -r"
},
"related": {
"heading": "Related",

View File

@@ -98,7 +98,7 @@
{
"step": "1",
"name": "Ensamblado del rootfs",
"detail": "Ejecuta <code>rsync -a</code> por cada ruta seleccionada hacia <code>staging_root/rootfs/</code>. Excluye subrutas volátiles (historial de bash, cachés, papelera) de <code>/root/</code>. Las rutas ausentes en el origen se registran en <code>metadata/missing_paths.txt</code> sin detener la copia."
"detail": "Ejecuta <code>rsync -a</code> por cada ruta seleccionada hacia <code>staging_root/rootfs/</code>. La base pmxcfs en <code>/var/lib/pve-cluster/config.db</code> se captura aparte con <code>sqlite3 .backup</code> para obtener un snapshot consistente sin parar el clúster, y los sidecars <code>.db-wal</code> / <code>.db-shm</code> se excluyen del rsync; si <code>sqlite3</code> no está disponible, se deposita un <code>config.db.raw-fallback</code> que la restauración promociona al importar. Excluye subrutas volátiles (historial de bash, cachés, papelera) de <code>/root/</code>. Las rutas ausentes en el origen se registran en <code>metadata/missing_paths.txt</code> sin detener la copia."
},
{
"step": "2",
@@ -165,7 +165,7 @@
"archiveStructure": {
"heading": "Estructura del archivo",
"intro": "El directorio de staging que produce cada backend sigue el mismo layout con independencia del destino. El tarball, el <code>.pxar</code> de PBS o el archivo Borg almacenan este árbol verbatim.",
"tree": "backup-[timestamp]/\n├── manifest.json # estado estructurado del host (kernel_params, hardware, storage, guests, components, source_host)\n├── metadata/\n│ ├── packages.manual.list # salida de apt-mark showmanual\n│ ├── run_info.env # hostname, timestamp, versión del kernel\n│ ├── paths_archived.txt # lista exacta de rutas que llegaron a rootfs/\n│ └── missing_paths.txt # rutas del perfil ausentes en el origen\n└── rootfs/\n ├── etc/ # /etc/pve, /etc/network, /etc/ssh, /etc/apt, ...\n ├── root/ # /root sin subpaths volátiles\n ├── usr/local/ # /usr/local/bin, /usr/local/sbin, /usr/local/share/proxmenux (sólo estado)\n └── var/ # /var/lib/pve-cluster, /var/spool/cron/crontabs"
"tree": "backup-[timestamp]/\n├── manifest.json # estado estructurado del host (kernel_params, hardware, storage, guests, components, source_host)\n├── metadata/\n│ ├── packages.manual.list # salida de apt-mark showmanual\n│ ├── run_info.env # hostname, timestamp, versión del kernel, método pmxcfs\n│ ├── paths_archived.txt # lista exacta de rutas que llegaron a rootfs/\n│ └── missing_paths.txt # rutas del perfil ausentes en el origen\n└── rootfs/\n ├── etc/ # /etc/pve, /etc/network, /etc/systemd/network, /etc/ssh, /etc/apt, ...\n ├── root/ # /root sin subpaths volátiles\n ├── usr/local/ # /usr/local/bin, /usr/local/sbin, /usr/local/share/proxmenux (sólo estado)\n └── var/ # /var/lib/pve-cluster (config.db vía sqlite3 .backup), /var/spool/cron/crontabs"
},
"confirmation": {
"heading": "Resumen de confirmación",

View File

@@ -24,7 +24,7 @@
"id": "passphrase-recuperacion",
"term": "Passphrase de recuperación",
"also": "recovery passphrase",
"def": "Contraseña de texto que elige el operador para proteger el <a href=\"#sobre-recuperacion\">sobre de recuperación</a>. Se pide dos veces con validación de coincidencia y solo se usa para cifrar el sobre y para descifrarlo si algún día hay que recuperar la clave. <strong>No sirve para desbloquear la clave del keyfile</strong> ni se envía nunca a PBS. Nunca sale del host durante una copia normal."
"def": "Contraseña de texto que elige el usuario para proteger el <a href=\"#sobre-recuperacion\">sobre de recuperación</a>. Se pide dos veces con validación de coincidencia y solo se usa para cifrar el sobre y para descifrarlo si algún día hay que recuperar la clave. <strong>No sirve para desbloquear la clave del keyfile</strong> ni se envía nunca a PBS. Nunca sale del host durante una copia normal."
},
{
"id": "clave-cifrado",

View File

@@ -20,7 +20,7 @@
"heading": "Layout del archivo",
"intro": "Cada archivo respeta el mismo layout con independencia del destino. El subdirectorio <code>metadata/</code> contiene los bloques estructurados; el subdirectorio <code>rootfs/</code> contiene la copia del sistema de ficheros.",
"treeCaption": "El directorio de staging generado durante una copia. Todos los destinos reciben el mismo árbol (adaptado a su formato nativo: tar para local, chunks PBS para PBS, segmentos borg para Borg).",
"tree": "backup-[timestamp]/\n├── manifest.json # estado estructurado del host\n├── metadata/\n│ ├── packages.manual.list # apt-mark showmanual\n│ ├── run_info.env # identidad de la ejecución + versión del kernel\n│ ├── paths_archived.txt # lista exacta de rutas que llegaron a rootfs/\n│ └── missing_paths.txt # rutas del perfil ausentes en el origen\n└── rootfs/\n ├── etc/ # /etc/pve, /etc/network, /etc/ssh, …\n ├── root/ # /root (con subdirs volátiles excluidos)\n ├── usr/local/ # /usr/local/bin, /usr/local/share/proxmenux, …\n └── var/ # /var/lib/pve-cluster, /var/spool/cron/…"
"tree": "backup-[timestamp]/\n├── manifest.json # estado estructurado del host\n├── metadata/\n│ ├── packages.manual.list # apt-mark showmanual\n│ ├── run_info.env # identidad de la ejecución + versión del kernel + método pmxcfs\n│ ├── paths_archived.txt # lista exacta de rutas que llegaron a rootfs/\n│ └── missing_paths.txt # rutas del perfil ausentes en el origen\n└── rootfs/\n ├── etc/ # /etc/pve, /etc/network, /etc/systemd/network, /etc/ssh, …\n ├── root/ # /root (con subdirs volátiles excluidos)\n ├── usr/local/ # /usr/local/bin, /usr/local/share/proxmenux, …\n └── var/ # /var/lib/pve-cluster (config.db vía sqlite3 .backup), /var/spool/cron/…"
},
"rootfs": {
"heading": "El bloque rootfs",
@@ -32,12 +32,12 @@
{
"category": "Núcleo PVE",
"paths": "/etc/pve, /var/lib/pve-cluster, /etc/vzdump.conf",
"why": "Contenido del filesystem del clúster, datos vivos del clúster, valores por defecto de vzdump."
"why": "Contenido del filesystem del clúster y valores por defecto de vzdump. La base pmxcfs (<code>config.db</code>) se captura con un snapshot consistente vía <code>sqlite3 .backup</code> sin parar el clúster, y los sidecars <code>.db-wal</code> y <code>.db-shm</code> quedan excluidos del rsync. Si <code>sqlite3</code> no está disponible, la copia deposita un <code>config.db.raw-fallback</code> que la restauración promociona automáticamente."
},
{
"category": "Identidad del host y red",
"paths": "/etc/hostname, /etc/hosts, /etc/timezone, /etc/resolv.conf, /etc/network",
"why": "Todo lo necesario para que el host arranque en red con la misma identidad."
"paths": "/etc/hostname, /etc/hosts, /etc/timezone, /etc/resolv.conf, /etc/network, /etc/systemd/network",
"why": "Todo lo necesario para que el host arranque en red con la misma identidad. Los ficheros <code>.link</code> bajo <code>/etc/systemd/network</code> pinnean los nombres de las NICs a su MAC, de modo que reinstalaciones y cambios de hardware no rebautizan las interfaces."
},
{
"category": "Acceso y autenticación",
@@ -102,7 +102,7 @@
{
"collector": "collect_storage.sh",
"produces": "storage_inventory",
"content": "Pools ZFS (con tipo de pool y discos miembro resueltos a <code>/dev/disk/by-id/*</code> para portabilidad), grupos de volúmenes LVM + thin pools, discos físicos con capacidad SMART, entradas de <code>storage.cfg</code> de PVE, puntos de montaje externos."
"content": "Pools ZFS (con tipo de pool y discos miembro resueltos a <code>/dev/disk/by-id/*</code>, <code>/dev/disk/by-partuuid/*</code> o rutas <code>/dev/sdX</code> en bruto según cómo se creara cada pool en el origen), grupos de volúmenes LVM + thin pools, discos físicos con capacidad SMART, entradas de <code>storage.cfg</code> de PVE, puntos de montaje externos."
},
{
"collector": "collect_kernel.sh",
@@ -157,8 +157,8 @@
},
"restoreFlow": {
"heading": "Cómo consume la restauración los tres bloques",
"intro": "La restauración es un pipeline de cinco etapas. Cada etapa lee un subconjunto concreto del archivo y actualiza el host de destino. Ninguna etapa requiere que el host de origen esté accesible — el archivo es totalmente autocontenido.",
"stagesCaption": "La etapa 1 usa el manifiesto para decidir qué tocar. La etapa 2 copia las rutas del rootfs seguras de aplicar en un sistema en ejecución. La etapa 3 deja las rutas de riesgo preparadas para el siguiente arranque. La etapa 4 gestiona los paquetes. La etapa 5 corre tras el reinicio y reinstala los componentes contra el kernel del destino.",
"intro": "La restauración es un pipeline de seis etapas. Cada etapa lee un subconjunto concreto del archivo y actualiza el host de destino. Ninguna etapa requiere que el host de origen esté accesible — el archivo es totalmente autocontenido.",
"stagesCaption": "La etapa 1 usa el manifiesto para decidir qué tocar. La etapa 2 copia las rutas del rootfs seguras de aplicar en un sistema en ejecución. La etapa 3 deja las rutas de riesgo preparadas para el siguiente arranque. La etapa 3b importa automáticamente los pools ZFS de datos cuyos discos están todos presentes. La etapa 4 gestiona los paquetes. La etapa 5 corre tras el reinicio y reinstala los componentes contra el kernel del destino.",
"stageRows": [
{
"stage": "1",
@@ -178,6 +178,12 @@
"reads": "rootfs/ (rutas reboot + dangerous)",
"action": "<code>_rs_prepare_pending_restore</code> deja las rutas de riesgo preparadas bajo <code>/var/lib/proxmenux/pending-restore/</code>, escribe <code>plan.env</code>, <code>apply-on-boot.list</code> y <code>rs-skip-paths.txt</code>, y habilita <code>proxmenux-restore-onboot.service</code> para que dispare en el siguiente arranque."
},
{
"stage": "3b",
"name": "Importación de pools de datos",
"reads": "manifest.storage_inventory + estado ZFS del destino",
"action": "<code>_rs_import_data_pools</code> intenta importar cada pool ZFS no-raíz cuyo conjunto completo de discos está presente en el destino. Los pools con hostid ajeno se reintentan con <code>zpool import -f</code>. Los pools con discos faltantes se saltan con una advertencia. El resultado por pool queda registrado en <code>/var/log/proxmenux/restore-datapools-&lt;timestamp&gt;.log</code>."
},
{
"stage": "4",
"name": "Instalación de paquetes",

View File

@@ -36,7 +36,7 @@
"intro": "Un archivo de ProxMenux se estructura en torno a tres cargas útiles autocontenidas. La restauración utiliza las tres en conjunto para reproducir el host de origen sobre un destino que puede no tener siquiera el mismo kernel instalado.",
"diagramCaption": "Cada copia contiene los mismos tres bloques con independencia del destino. La restauración los consume todos: rootfs para colocar los ficheros, manifiesto para detectar drift y diferencias cross-kernel, e inventario de aplicaciones para reinstalar paquetes y componentes contra el kernel del propio destino.",
"pillar1Label": "Sistema de ficheros",
"pillar1Detail": "rootfs/\n(rsync de\n/etc, /root,\n/var/lib/pve-cluster,\n+ rutas opcionales)",
"pillar1Detail": "rootfs/\n(rsync de /etc,\n/etc/systemd/network,\n/root, más snapshot\nsqlite3 de\n/var/lib/pve-cluster,\n+ rutas opcionales)",
"pillar2Label": "Manifiesto",
"pillar2Detail": "manifest.json\n(hardware, params\ndel kernel, red,\nZFS, usuarios, cron,\npools ZFS, storage)",
"pillar3Label": "Aplicaciones",

File diff suppressed because one or more lines are too long

View File

@@ -153,7 +153,7 @@
{
"href": "/docs/utils/system-update",
"label": "Actualización del sistema Proxmox (interactivo)",
"tail": " — wrapper que hace todo esto con un diálogo de confirmación y preguntar antes de reiniciar."
"tail": " — wrapper que hace todo esto con un diálogo de confirmación, una recompilación DKMS automática de los drivers gestionados por ProxMenux cuando se instala un kernel nuevo y un prompt de reinicio. También lanzable desde el botón Update Now del dashboard de ProxMenux Monitor."
},
{
"href": "/docs/utils/upgrade-pve8-pve9",

View File

@@ -57,7 +57,7 @@
},
"updating": {
"heading": "Actualizar",
"body": "ProxMenux se autoactualiza. Cuando hay una nueva versión disponible, te lo pide en el siguiente lanzamiento de <code>menu</code> y aceptar reemplaza los archivos de utilidad y las configuraciones en sitio. No hace falta descarga manual. Los usuarios estables reciben releases estables; los usuarios beta reciben releases beta (y el prompt de auto-cambio de arriba cuando una estable se publica)."
"body": "ProxMenux se autoactualiza. Cuando hay una nueva versión disponible, el siguiente lanzamiento de <code>menu</code> ofrece la actualización; al aceptar se reemplazan los archivos de utilidad y las configuraciones en sitio. No hace falta descarga manual. Los usuarios estables reciben releases estables; los usuarios beta reciben releases beta (y el prompt de auto-cambio de arriba cuando una estable se publica). La forma del prompt depende de dónde se ejecute <code>menu</code>: por SSH o desde la consola de Proxmox aparece el diálogo clásico sí / no y al aceptar la actualización se aplica de inmediato. Dentro del terminal WebSocket de ProxMenux Monitor se sustituye por un msgbox informativo — ese terminal no puede sobrevivir a una autoactualización en curso, así que el msgbox muestra el instalador canónico de una línea (<code>bash -c \"$(wget -qLO - https://raw.githubusercontent.com/MacRimi/ProxMenux/main/install_proxmenux.sh)\"</code> para el canal estable, o la URL equivalente de <code>install_proxmenux_beta.sh</code> para beta) para lanzarlo desde SSH o consola."
},
"uninstall": {
"heading": "Desinstalar",

View File

@@ -10,7 +10,7 @@
},
"intro": {
"title": "Un PTY real en el navegador",
"body": "El terminal asigna una PTY del lado del servidor a través de <code>flask_terminal_routes</code>, la canaliza por un WebSocket hacia <code>xterm.js</code> en el navegador y se ejecuta como <code>root</code> (el usuario de la unidad systemd). Cualquier cosa que puedas hacer en <code>ssh root@&lt;host&gt;</code> funciona aquí — incluidos <code>vim</code>, <code>tmux</code>, herramientas ncurses y las CLIs de Proxmox (<code>qm</code>, <code>pct</code>, <code>pvesh</code>, <code>pvecm</code>)."
"body": "El terminal asigna una PTY del lado del servidor a través de <code>flask_terminal_routes</code>, la canaliza por un WebSocket hacia <code>xterm.js</code> en el navegador y se ejecuta como <code>root</code> (el usuario de la unidad systemd). Cualquier cosa que puedas hacer en <code>ssh root@&lt;host&gt;</code> funciona aquí — incluidos <code>vim</code>, <code>tmux</code>, herramientas ncurses y las CLIs de Proxmox (<code>qm</code>, <code>pct</code>, <code>pvesh</code>, <code>pvecm</code>). Cada PTY lanzada desde esta pestaña exporta la variable de entorno <code>PROXMENUX_TERMINAL=monitor</code>, que hereda todo proceso <code>bash</code> hijo — incluida una llamada posterior a <code>menu</code> — para que los flujos de ProxMenux puedan saber si se están ejecutando dentro del terminal del navegador del Monitor."
},
"singleAlt": "Pestaña Terminal de ProxMenux Monitor — una única sesión de terminal mostrando el resumen del sistema de Fastfetch al hacer login",
"singleCaption": "Un terminal del host abierto — la barra de arriba muestra el recuento (<em>1 / 4 terminals</em>), <em>+ New</em>, <em>Search</em>, <em>Clear</em> y <em>Close</em>. Las ayudas de teclado para móvil aparecen bajo el terminal en dispositivos táctiles.",
@@ -19,6 +19,11 @@
"body1": "La pestaña Terminal abre una shell en el <strong>propio host Proxmox</strong> — el mismo login que obtendrías por SSH. Cada pestaña abre un terminal del host completamente nuevo.",
"body2": "Para llegar a un <strong>contenedor LXC</strong> desde el navegador, usa el botón <em>Console</em> dedicado en cada tarjeta de CT en ejecución de la <link>pestaña VMs y LXCs</link>. Abre una modal que ejecuta <code>pct enter &lt;vmid&gt;</code> y reutiliza la misma barra para móvil descrita abajo."
},
"menuGuard": {
"heading": "Guarda de autoactualización cuando menu corre dentro de este terminal",
"body1": "El <code>menu</code> de ProxMenux se autoactualiza en sitio: aceptar el prompt reemplaza su propio árbol de scripts y recarga. Eso funciona bien por SSH o desde la consola de Proxmox, pero el terminal del navegador no puede sobrevivir a ello — la actualización desmonta la propia PTY sobre la que corre la shell y el WebSocket cae a mitad. Para no dejar la sesión colgada, los handlers internos <code>check_updates_stable</code>, <code>check_updates_beta</code> y <code>apply_release_channel</code> leen <code>PROXMENUX_TERMINAL</code> y detectan el contexto Monitor.",
"body2": "Al detectarlo, el diálogo clásico sí / no se sustituye por un msgbox informativo con el instalador canónico de una línea — <code>bash -c \"$(wget -qLO - https://raw.githubusercontent.com/MacRimi/ProxMenux/main/install_proxmenux.sh)\"</code> para el canal estable, y la URL equivalente de <code>install_proxmenux_beta.sh</code> para beta. La actualización propiamente dicha se deja para una sesión SSH o la consola del host Proxmox; el terminal del navegador solo se usa para confirmar y leer el comando."
},
"fourTerminals": {
"heading": "Hasta cuatro terminales a la vez",
"intro": "La pestaña te permite abrir hasta cuatro terminales del host simultáneamente. Cada uno obtiene su propia PTY y su propio WebSocket — son sesiones totalmente independientes. Dos layouts se alternan con los iconos junto al botón \"New\":",

View File

@@ -98,7 +98,7 @@
{
"category": "Actualizaciones del sistema",
"checks": "Actualizaciones pendientes, actualizaciones de seguridad, kernel / versión de PVE, antigüedad del sistema",
"events": "Actualizaciones de seguridad disponibles; kernel pinneado varias versiones menores por detrás; uptime del host > 90 días."
"events": "Actualizaciones de seguridad disponibles; kernel pinneado varias versiones menores por detrás; uptime del host > 90 días. Cuando hay actualizaciones pendientes, la cabecera de Overview muestra un botón <em>Update Now</em> que ejecuta el flujo completo de actualización de ProxMenux en línea; al cerrar vuelve a golpear el endpoint de health con <code>?refresh=1</code> para que el badge refleje el nuevo estado sin esperar al siguiente ciclo de 5 minutos."
},
{
"category": "Seguridad y Certificados",
@@ -151,11 +151,14 @@
"<strong>Dismissed</strong> — items previamente reconocidos por el usuario que aún están dentro de su ventana de supresión. Cada fila muestra cuánto queda de la supresión y la duración configurada. Cuando la ventana caduca, el item desaparece de esta lista; si la condición subyacente sigue presente y la categoría soporta re-firing, reaparece en <em>Active</em>."
],
"pillTitle": "La pildora refleja la peor categoría",
"pillBody": "El color de la cabecera del panel es la severidad más alta entre las diez categorías: cualquier CRITICAL → rojo, si no cualquier WARNING → amarillo, si no cualquier INFO → azul, si no verde. La misma lógica dirige el punto del favicon y el badge del PWA."
"pillBody": "El color de la cabecera del panel es la severidad más alta entre las diez categorías: cualquier CRITICAL → rojo, si no cualquier WARNING → amarillo, si no cualquier INFO → azul, si no verde. La misma lógica dirige el punto del favicon y el badge del PWA.",
"updateNowTitle": "Botón Update Now en la cabecera de Overview",
"updateNowBody1": "Cuando la categoría <em>Actualizaciones del sistema</em> reporta una o más actualizaciones pendientes, la cabecera de Overview renderiza un botón <strong>Update Now</strong> al lado de la pildora de salud. El botón abre una modal que ejecuta el mismo flujo del lado del host que <em>ProxMenux → Settings post-install Proxmox → Proxmox System Update</em>: higiene de repos, <code>apt full-upgrade</code> vía el worker seguro <code>update-pve-safe.sh</code>, recompilación DKMS automática de los drivers gestionados por ProxMenux cuando aterriza un kernel nuevo, autoremove / autoclean y prompt de reinicio si el kernel cambió. Las líneas de progreso van llegando a la modal según las va imprimiendo el worker.",
"updateNowBody2": "Al terminar la ejecución, cerrar la modal llama a <code>GET /api/health/details?refresh=1</code>, que invalida la caché del endpoint de health antes de servir. La fila de actualizaciones pendientes y la severidad de la pildora se recomputan al momento a partir del nuevo estado de paquetes, así que el badge desaparece sin esperar al siguiente ciclo de cinco minutos. Cuando el host ya está al día el botón queda oculto — sin ruido visual en un sistema sano."
},
"dismiss": {
"heading": "Hacer dismiss de alertas y la Suppression Duration",
"intro": "Algunos eventos son ruidosos por naturaleza — un <em>System Updates: actualizaciones pendientes disponibles</em> permanece cierto hasta que parches el host, y no quieres una notificación cada cinco minutos durante una semana. El Monitor de salud resuelve esto con dos mecanismos acoplados:",
"intro": "Algunos eventos son ruidosos por naturaleza — un <em>System Updates: actualizaciones pendientes disponibles</em> permanece cierto hasta que parches el host, y no quieres una notificación cada cinco minutos durante una semana. El botón <em>Update Now</em> de la cabecera de Overview es el atajo directo para resolver ese caso concreto en sitio; para todo lo demás, o cuando el parche se aplaza, el Monitor de salud ofrece además dos mecanismos de silenciamiento acoplados:",
"step1": "<strong>Acción Dismiss por evento</strong> en el modal. El botón Dismiss abre un dropdown con tres opciones — <strong>24 horas</strong>, <strong>7 días</strong> o <strong>Permanently</strong> — que te permite elegir cuánto tiempo se silencia esta alerta concreta independientemente del valor por defecto de la categoría. Elegir una llama a <code>POST /api/health/acknowledge</code> con el <code>error_key</code> y el <code>suppression_hours</code> escogido (<code>-1</code> para permanente). El evento se mueve a la lista Dismissed con un <code>acknowledged_at</code> con timestamp.",
"dropdownImageAlt": "Dropdown de Dismiss sobre una alerta del Monitor de salud — 24 horas, 7 días o Permanently",
"dropdownImageCaption": "Dropdown Dismiss por evento. La ventana elegida aplica solo a esta alerta; si no eliges ninguna por evento se usa el valor por defecto de la categoría. Los dismisses permanentes se marcan con un badge ámbar distinto <em>Permanent</em> en la lista Dismissed y no se re-disparan.",

View File

@@ -67,7 +67,7 @@
},
{
"tool": "Tuning del stack de red",
"what": "Dimensionado de buffers TCP, hardening IPv4 (redirects off, rp_filter=2, martian log off), rango de puertos locales 1024-65535, TCP MTU probing, RFC 1337, más una unit systemd oneshot para normalizar los bridges de firewall virtual.",
"what": "Dimensionado de buffers TCP, hardening IPv4 (redirects off, rp_filter=2, martian log off), rango de puertos locales 1024-65535, TCP MTU probing, RFC 1337, más un helper en /usr/local/sbin/proxmenux-fwbr-tune conectado a una unit systemd oneshot y a una regla udev, de forma que rp_filter=0 / log_martians=0 se reapliquen a las interfaces fwbr*/fwln*/fwpr*/tap* cada vez que Proxmox las recrea al iniciar/parar VMs, en reinicios o en migraciones en vivo.",
"category": "Network",
"categorySlug": "network"
},
@@ -103,7 +103,7 @@
},
{
"tool": "Nombres de interfaz persistentes",
"what": "Escribe archivos /etc/systemd/network/10-*.link que emparejan cada NIC física por MAC para que los nombres eth0 / enp… se mantengan estables tras reinicios y al añadir nuevas NICs.",
"what": "Escribe un /etc/systemd/network/10-proxmenux-<iface>.link por NIC física (cada uno comenzando con la cabecera 'Managed by ProxMenux') que fija el MAC al nombre actual, de forma que los nombres eth0 / enp… se mantengan estables tras reinicios y al añadir nuevas NICs.",
"category": "Network",
"categorySlug": "network"
}

View File

@@ -54,6 +54,8 @@
}
],
"sourceOutro": "También añade <code>source /etc/network/interfaces.d/*</code> a <code>/etc/network/interfaces</code> si no está ya presente — práctica estándar para que puedas dejar snippets modulares de interfaz sin editar el archivo principal.",
"fwbrTitle": "Ajuste automático de los bridges de firewall virtual",
"fwbrBody": "Junto al perfil sysctl, ProxMenux instala un helper en <code>/usr/local/sbin/proxmenux-fwbr-tune</code> que aplica <code>rp_filter=0</code> y <code>log_martians=0</code> a las interfaces <code>fwbr*</code> / <code>fwln*</code> / <code>fwpr*</code> / <code>tap*</code> que Proxmox crea alrededor de VMs y contenedores. El helper lo lanza la unit oneshot <code>proxmenux-fwbr-tune.service</code> al arranque y la regla <code>/etc/udev/rules.d/99-proxmenux-fwbr-tune.rules</code> en cada evento <code>net add</code> que coincida con esos prefijos — cubriendo las interfaces que Proxmox recrea al iniciar/parar VMs, en reinicios y en migraciones en vivo.",
"rpFilterTitle": "Por qué rp_filter=2 (loose) en lugar de 1 (strict)",
"rpFilterBody": "El reverse-path filtering strict descarta paquetes cuya fuente se rutearía por una interfaz <em>distinta</em>. Es el valor por defecto correcto en una máquina cliente, pero rompe gravemente en un host Proxmox donde el tráfico de VMs a menudo llega por un bridge y sale por un uplink con rutas asimétricas. <code>rp_filter=2</code> (loose) solo descarta paquetes con fuentes verdaderamente no enrutables. Es un trade-off pragmático — ligera reducción en la detección de spoofing de IP local a cambio de no romper tu red de VMs."
},
@@ -84,14 +86,14 @@
"Contenedores LXC con NICs <code>hotplug</code> y enlaces bonded pueden hacer race en el arranque y acabar nombrados de forma inconsistente. Fijarlos lo soluciona."
],
"writtenTitle": "Qué se escribe",
"writtenIntro": "Un archivo por NIC física, en <code>/etc/systemd/network/10-&lt;iface&gt;.link</code>:",
"writtenOutro": "Cualquier archivo <code>.link</code> preexistente en ese directorio se copia a <code>/etc/systemd/network/backup-&lt;timestamp&gt;/</code> antes de tocar nada.",
"writtenIntro": "Un archivo por NIC física, en <code>/etc/systemd/network/10-proxmenux-&lt;iface&gt;.link</code>. Cada archivo comienza con la cabecera <code># Managed by ProxMenux — do not edit</code>, marca que permite a ProxMenux distinguir sus propios archivos de los <code>.link</code> provistos por el usuario o por otros paquetes.",
"writtenOutro": "Los archivos <code>.link</code> de otros paquetes o escritos a mano por el usuario se dejan intactos — solo se gestionan los archivos que llevan la cabecera de ProxMenux. En cada nueva ejecución la función reconcilia su propio conjunto: relee <code>MACAddress=</code> de cada <code>10-proxmenux-*.link</code> y elimina los cuya MAC ya no aparece en <code>/sys/class/net/</code>. Los archivos escritos por una versión antigua de ProxMenux en el formato <code>10-&lt;iface&gt;.link</code> se migran automáticamente en la primera ejecución.",
"pveTitle": "PVE 9 vs PVE 8",
"pveBody": "En Proxmox VE 9 (<code>systemd-networkd</code> nativo), el script recarga las reglas udev tras escribir los archivos <code>.link</code> para que las nuevas NICs hotplug cojan el nombre correcto sin reiniciar. En PVE 8 (<code>ifupdown2</code>), el naming de interfaz se resuelve en el arranque de todos modos — se requiere un reinicio para que los cambios surtan efecto. El script activa el flag de reinicio en cualquier caso para que Personalizable te lo pregunte.",
"reviewTitle": "Revisa antes el /etc/network/interfaces existente",
"reviewBody": "Si tu host tiene configuración legacy en <code>/etc/network/interfaces</code> que referencia nombres de NIC generados por el esquema por defecto del kernel, fijar los nombres <em>de hoy</em> es exactamente lo que quieres. Pero si ya has customizado manualmente la configuración alrededor de nombres específicos, comprueba dos veces que el pinning coincide con lo que el archivo interfaces espera antes de reiniciar.",
"revertTitle": "Reversible desde el menú Uninstall",
"revertBody": "<link>Uninstall Optimizations</link> borra cada archivo <code>.link</code> de <code>/etc/systemd/network/</code>, restaurando el naming por defecto del kernel en el siguiente reinicio. El backup con marca de tiempo de los archivos originales se queda por si necesitas restaurar alguno manualmente."
"revertBody": "<link>Uninstall Optimizations</link> solo borra los archivos <code>10-proxmenux-*.link</code> que llevan la cabecera de ProxMenux en <code>/etc/systemd/network/</code>. Cualquier archivo <code>.link</code> añadido por el usuario o por otro paquete se preserva. Los nombres de interfaz vuelven al comportamiento por defecto de systemd en el siguiente reinicio."
},
"related": {
"heading": "Relacionado",

View File

@@ -145,10 +145,11 @@
"doesLabel": "Qué hace ProxMenux:",
"doesItems": [
"Detecta si el disco raíz es SSD/NVMe leyendo <code>/sys/block/&lt;dev&gt;/queue/rotational</code>. En un disco rotacional el flujo Automatizado pregunta antes de instalar.",
"Clona el repositorio oficial (<code>azlux/log2ram</code>) en <code>/tmp/log2ram</code> y ejecuta su <code>install.sh</code>, luego habilita la unit systemd <code>log2ram</code>.",
"Clona el repositorio oficial (<code>azlux/log2ram</code>) en <code>/tmp/log2ram</code> y aplica un patch a su <code>install.sh</code>, sustituyendo <code>rsync -aAXv</code> por <code>rsync -aXv --no-acls</code>. Ese ajuste evita el fallo <em>set_acl: Operation not supported</em> con exit 23 que el script vanilla produce al mover /var/log en sistemas de archivos sin soporte de ACLs. La unit systemd <code>log2ram</code> se habilita después de la instalación.",
"Dimensiona la ramdisk según la RAM del host: <code>≤ 8 GB → 128M</code>, <code>≤ 16 GB → 256M</code>, <code>&gt; 16 GB → 512M</code>. Escribe el valor en <code>SIZE=</code> de <code>/etc/log2ram.conf</code>.",
"Programa una sincronización periódica a disco vía <code>/etc/cron.d/log2ram</code>: cada 1h / 3h / 6h según el mismo tramo de RAM.",
"Instala un guardián de auto-sync en <code>/usr/local/bin/log2ram-check.sh</code>, conectado a <code>/etc/cron.d/log2ram-auto-sync</code> para ejecutarse cada 10 minutos. Cuando <code>/var/log</code> alcanza el 80% del tamaño de la ramdisk el guardián compacta journald; al 92% además trunca <code>pveproxy access/error</code> y <code>pveam.log</code> antes de sincronizar — el comando <code>log2ram write</code> por sí solo copia tmpfs a disco pero NO reduce la tmpfs, así que este guardián evita que PVE caiga con <em>No space left on device</em> cuando los logs crecen sin control.",
"Instala un guardián de auto-sync en <code>/usr/local/bin/log2ram-check.sh</code>, conectado a <code>/etc/cron.d/log2ram-auto-sync</code> para ejecutarse cada 10 minutos. Cuando <code>/var/log</code> alcanza el 80% del tamaño de la ramdisk el guardián compacta journald; al 92% primero ejecuta <code>logrotate -f /etc/logrotate.d/proxmox-backup-api</code> si ese archivo existe (para forzar la rotación de los logs del API de PBS, que suelen ser los que más crecen en un host con PBS) y a continuación trunca <code>pveproxy access/error</code> y <code>pveam.log</code> antes de sincronizar. El comando <code>log2ram write</code> por sí solo copia tmpfs a disco pero NO reduce la tmpfs, así que este guardián evita que PVE caiga con <em>No space left on device</em> cuando los logs crecen sin control.",
"Detecta si <code>proxmox-backup-server</code> corre como servicio en el host. Cuando está presente, deposita <code>/etc/logrotate.d/proxmox-backup-api</code> (rotación de 20 MB × 3) y <code>/etc/cron.hourly/proxmox-backup-logrotate</code>, de forma que los logs del API de PBS — el origen habitual del crecimiento en un host con PBS — queden acotados incluso entre las sincronizaciones periódicas de Log2RAM. En hosts sin PBS este paso es un no-op.",
"Ajusta los límites de systemd-journald (<code>SystemMaxUse</code>, <code>RuntimeMaxUse</code>) para que quepan en la ramdisk y así una ráfaga puntual no la llene.",
"Se registra en <code>installed_tools.json</code> para poder revertirlo desde Uninstall Optimizations."
],

View File

@@ -18,30 +18,26 @@
"intro": "El <strong>Adaptive Replacement Cache (ARC)</strong> es la caché de lectura en memoria de ZFS. Sin tuning explícito, ZFS coge alegremente hasta la mitad de la RAM del host para sí mismo, lo que es excesivo en un host Proxmox que también necesita memoria para VMs y LXCs. Esta opción limita el ARC a una fracción sensata de la RAM total según el tamaño de la máquina.",
"sizingTitle": "Reglas de dimensionado",
"headerRam": "RAM del host",
"headerMin": "ARC mín",
"headerMax": "ARC máx",
"headerMax": "Cap ARC",
"rows": [
{
"ram": "≤ 16 GB",
"min": "512 MB",
"max": "512 MB"
"max": "512 MiB"
},
{
"ram": "17 32 GB",
"min": "1 GB",
"max": "1 GB"
"max": "1 GiB"
},
{
"ram": "> 32 GB",
"min": "RAM / 16",
"max": "RAM / 8"
"max": "RAM / 8 (mínimo 512 MiB)"
}
],
"after": "En un host de 64 GB, eso significa 4 GB mín / 8 GB máx para el ARC. La config se escribe en <code>/etc/modprobe.d/99-zfsarc.conf</code> y activa unos cuantos tunables ZFS extra (L2ARC prefetch activo, L2ARC write max en 500 MB, timeout TXG más largo).",
"after": "En un host de 64 GB, eso se traduce en un cap de 8 GB para el ARC. El archivo <code>/etc/modprobe.d/99-zfsarc.conf</code> contiene una única directiva — <code>options zfs zfs_arc_max=…</code>. El resto de parámetros del módulo (<code>zfs_arc_min</code>, prefetch/write throttle de L2ARC, timeout de TXG) se dejan en los valores por defecto de OpenZFS. Tras escribir el archivo, ProxMenux ejecuta <code>update-initramfs -u -k all</code> y, cuando corresponde, <code>proxmox-boot-tool refresh</code>, para que el cap llegue también al initramfs que usan las instalaciones con ZFS-on-root.",
"rebootTitle": "Requiere reinicio para surtir efecto",
"rebootBody": "Los ajustes del ARC se leen cuando se carga el módulo del kernel <code>zfs</code>. <strong>No</strong> se aplican en un sistema en vivo — necesitarás reiniciar el host para que el límite entre en juego. El script activa el flag de \"se requiere reinicio\" automáticamente.",
"rebootBody": "Los ajustes del ARC se leen cuando se carga el módulo del kernel <code>zfs</code>. Para que el cap se aplique en hosts ZFS-on-root, ProxMenux regenera el initramfs con <code>update-initramfs -u -k all</code> y, cuando corresponde, refresca el cargador de arranque con <code>proxmox-boot-tool refresh</code>. Un reinicio sigue siendo necesario para que el módulo relea el parámetro; el flag de \"se requiere reinicio\" se activa automáticamente.",
"safeTitle": "Seguro en hosts sin ZFS",
"safeBody": "La función comprueba el comando <code>zfs</code> antes de tocar nada. En hosts Proxmox solo ext4 / LVM, marcar esta opción es un no-op — no se escribe nada.",
"safeBody": "La función solo escribe la configuración cuando <code>zpool list</code> reporta al menos un pool ZFS importado. En hosts Proxmox solo ext4 / LVM, o en máquinas con las utilidades ZFS instaladas pero sin ningún pool importado, marcar esta opción es un no-op — no se escribe nada.",
"verifyTitle": "Verificación y rollback manual"
},
"autoSnap": {

View File

@@ -88,11 +88,11 @@
"items": [
{
"tool": "Network Optimizations",
"restores": "Elimina /etc/sysctl.d/99-network.conf y la unit proxmenux-fwbr-tune.service. Recarga sysctl y systemd."
"restores": "Elimina /etc/sysctl.d/99-network.conf junto con la unit proxmenux-fwbr-tune.service, el helper /usr/local/sbin/proxmenux-fwbr-tune y la regla /etc/udev/rules.d/99-proxmenux-fwbr-tune.rules. Recarga sysctl, systemd y el conjunto de reglas udev."
},
{
"tool": "Persistent Interface Names",
"restores": "Elimina cada archivo .link de /etc/systemd/network/. Los nombres de interfaz vuelven al comportamiento por defecto de systemd en el siguiente reinicio."
"restores": "Elimina solo los archivos 10-proxmenux-*.link que llevan la cabecera de ProxMenux en /etc/systemd/network/. Cualquier archivo .link provisto por otro paquete o escrito por el usuario se mantiene intacto. Los nombres de interfaz vuelven al comportamiento por defecto de systemd en el siguiente reinicio."
}
]
},
@@ -171,7 +171,7 @@
"packageTitle": "La reinstalación de paquetes toca paquetes vivos de Proxmox",
"packageBody": "Revertir <strong>Subscription Banner Removal</strong> reinstala <code>pve-manager</code>, <code>proxmox-widget-toolkit</code>, <code>libjs-extjs</code> y <code>libpve-http-server-perl</code> con <code>--force-confnew</code>. Es generalmente seguro pero sí toca la UI web en ejecución — refresca tu navegador después, y espera unos segundos de reconexión. No lo ejecutes a mitad de una migración o una operación de clonado.",
"rebootTitle": "Los nombres persistentes y VFIO necesitan reinicio",
"rebootBody": "Eliminar los archivos <code>.link</code> (<em>Persistent Interface Names</em>) y revertir <em>IOMMU/VFIO</em> no afectan al sistema en ejecución — solo importan tras un reinicio. ProxMenux activa el flag de reinicio automáticamente para estos.",
"rebootBody": "Eliminar los archivos <code>10-proxmenux-*.link</code> propios de ProxMenux (<em>Persistent Interface Names</em>) y revertir <em>IOMMU/VFIO</em> no afectan al sistema en ejecución — solo importan tras un reinicio. ProxMenux activa el flag de reinicio automáticamente para estos.",
"perItemTitle": "Puedes revertir una cosa y mantener el resto",
"perItemBody": "El uninstaller opera por item. Si solo quieres eliminar Log2RAM pero mantener el tuning de red y los cambios de bashrc, marca solo <em>Log2RAM</em>. Nada más se toca, y el registro se actualiza en consecuencia."
},

View File

@@ -53,7 +53,8 @@
"Tu configuración existente, login de ProxMenux Monitor (<code>auth.json</code>), canales de notificación, registro post-instalación, umbrales personalizados — nada de eso se toca. El cambio de canal solo intercambia el árbol de scripts y el AppImage binario.",
"Un Monitor que estaba activo antes del cambio sigue activo después; uno que estaba desactivado sigue desactivado.",
"El <code>auth.json</code>, los tokens de API y el secreto JWT de ProxMenux Monitor se preservan entre cambios de canal. Las sesiones no se cierran.",
"Puedes saltar de canal tantas veces como quieras — el diálogo lo aceptará cada vez y volverá a ejecutar el instalador. No hace falta reiniciar Proxmox."
"Puedes saltar de canal tantas veces como quieras — el diálogo lo aceptará cada vez y volverá a ejecutar el instalador. No hace falta reiniciar Proxmox.",
"El instalador de cambio de canal reemplaza el propio árbol de scripts de ProxMenux, así que no puede correr dentro del terminal del navegador del Monitor (esa PTY sería desmontada a mitad de la actualización y dejaría el diálogo colgado). Si lanzas la opción desde ahí, la confirmación se sustituye por un msgbox informativo con el instalador canónico de una línea para el canal de destino; ejecuta ese comando desde una sesión SSH o desde la consola del host Proxmox."
]
},
"feedback": {

View File

@@ -7,7 +7,7 @@
},
"header": {
"title": "Actualización del sistema Proxmox",
"description": "Wrapper que detecta la versión mayor de Proxmox en ejecución y delega en el worker correspondiente (PVE 8 o PVE 9). Los repos se limpian, se habilita la fuente sin suscripción, se actualizan todos los paquetes, se eliminan los paquetes conflictivos y el sistema se limpia después. El preguntar antes de reiniciar solo se dispara cuando el kernel realmente se ha actualizado.",
"description": "Wrapper que delega en un único worker seguro (<code>update-pve-safe.sh</code>) que detecta la versión mayor de Proxmox por sí mismo. Los repositorios se limpian cuando se solapan con las fuentes base de Proxmox / Debian, se actualizan todos los paquetes, se recompilan los drivers DKMS gestionados por ProxMenux si aterriza un kernel nuevo y el prompt de reinicio solo se dispara cuando el kernel realmente cambió. También lanzable desde el botón <em>Update Now</em> de la cabecera del dashboard de ProxMenux Monitor cuando se detectan actualizaciones pendientes.",
"section": "Utilidades"
},
"calloutWhat": {
@@ -22,15 +22,15 @@
},
"onTop": {
"heading": "Qué hace ProxMenux encima — verificado contra el script",
"intro": "Esta opción ejecuta <strong>exactamente</strong> el comando apt de arriba, envuelto con la higiene de repos y la limpieza post-upgrade que la guía oficial de upgrade también recomienda. La lista de abajo mapea 1:1 a <code>scripts/utilities/proxmox_update.sh</code> y a los scripts worker por versión — nada implícito, cada paso está en el código:",
"intro": "Esta opción ejecuta <strong>exactamente</strong> el comando apt de arriba, envuelto con la higiene de repos, la recompilación DKMS de los drivers gestionados por ProxMenux y la limpieza post-upgrade que la guía oficial de upgrade también recomienda. Todo lo de abajo mapea 1:1 a <code>scripts/utilities/proxmox_update.sh</code> y al único worker seguro <code>scripts/global/update-pve-safe.sh</code> — nada implícito, cada paso está en el código:",
"items": [
"<strong>Detecta la versión mayor de PVE</strong> (<code>pveversion | grep -oP ''pve-manager/\\K[0-9]+''</code>) y despacha a <code>update-pve8.sh</code> o <code>update-pve9_2.sh</code> para que se usen el codename y las URLs de repo correctas.",
"<strong>Limpia los repositorios</strong> antes de tocar apt: deshabilita la fuente enterprise (que devuelve 401 sin suscripción), elimina archivos de repo heredados y escribe una fuente sin suscripción limpia para el codename del host.",
"<strong>Ejecuta el upgrade no interactivamente</strong> con <code>DEBIAN_FRONTEND=noninteractive</code> y <code>--force-confdef --force-confold</code> — esto significa que si un archivo de configuración que ya modificaste también cambió upstream, tu versión se queda en su sitio. Sin sobrescrituras silenciosas de configuraciones propias.",
"<strong>Instala paquetes esenciales de Proxmox</strong> si falta alguno (<code>zfsutils-linux</code>, <code>proxmox-backup-restore-image</code>, <code>chrony</code>).",
"<strong>Detecta la versión mayor de PVE</strong> desde dentro del worker (<code>pveversion | grep -oP ''pve-manager/\\K[0-9]+''</code>) y adapta las URLs base de repo Proxmox / Debian (bookworm en PVE 8, trixie en PVE 9). No hay reparto a scripts worker por versión — un único worker seguro maneja ambas.",
"<strong>Limpia los repositorios de forma conservadora.</strong> <code>ensure_repositories</code> se ejecuta primero pero solo cuando faltan las fuentes base de Proxmox / Debian — un host bare las recibe escritas, un host configurado es un no-op. <code>cleanup_duplicate_repos</code> elimina después duplicados exactos por URL + Suite + Component solo contra <code>proxmox.sources</code> / <code>debian.sources</code>; los archivos propios del usuario (enterprise, Ceph, mirrors NTP alternativos, entradas custom de <code>download.proxmox.com/*</code> o <code>pve-*.list</code> escritos a mano) no se tocan, y cada archivo se respalda antes de editarse.",
"<strong>Ejecuta el upgrade no interactivamente</strong> con <code>DEBIAN_FRONTEND=noninteractive</code> y <code>--force-confdef --force-confold</code> — si un archivo de configuración que ya modificaste también cambió upstream, tu versión se queda en su sitio. Sin sobrescrituras silenciosas de configuraciones propias.",
"<strong>No fuerza utilidades opcionales.</strong> El worker seguro no empuja <code>zfsutils-linux</code>, <code>chrony</code>, <code>ifupdown2</code> ni paquetes similares al host — una instalación de Proxmox que optó por no tener alguno de ellos conserva su elección. La ausencia de paquetes la surface los flujos de instalación de alto nivel, no la ruta de actualización.",
"<strong>Comprobación de sanity de metadatos LVM</strong> contra cabeceras PV sueltas de discos en passthrough (solo aviso, sin arreglo automático).",
"<strong>Limpia después:</strong> <code>apt-get autoremove -y</code> + <code>apt-get autoclean -y</code>.",
"<strong>Preguntar antes de reiniciar</strong> solo si el kernel realmente cambió (presencia de <code>/var/run/reboot-required</code> o <code>linux-image</code> en el log del upgrade)."
"<strong>Recompilación DKMS antes del prompt de reinicio.</strong> Cuando el upgrade dejó staged un kernel nuevo, el wrapper llama a <code>pmx_rebuild_dkms_after_kernel</code> para recompilar cada driver que ProxMenux instaló vía DKMS contra la versión del kernel entrante — así los módulos están listos antes de que la caja vuelva a arrancar. La detección de reinicio usa <code>/var/run/reboot-required</code> cuando <code>needrestart</code> está presente, y como fallback una comparación por <code>dpkg-query</code> entre el kernel en ejecución y el paquete <code>proxmox-kernel-*-pve-signed</code> / <code>pve-kernel-*-pve</code> más nuevo instalado cuando no lo está — señal que sobrevive a los muchos hosts Proxmox que se entregan sin <code>needrestart</code>."
]
},
"calloutOneSentence": {
@@ -50,8 +50,8 @@
"detail": "pveversion |\ngrep -oP ''pve-manager/\\K[0-9]+''"
},
"bridge": {
"label": "Selección del worker",
"detail": "PVE 8 → update-pve8.sh\nPVE 9 → update-pve9_2.sh"
"label": "Único worker seguro",
"detail": "update-pve-safe.sh\n(detecta PVE 8 / 9\ninternamente)"
},
"target": {
"label": "Post-actualización",
@@ -61,13 +61,18 @@
},
"worker": {
"heading": "Qué hace el worker",
"intro": "Ambos workers (<code>scripts/global/update-pve8.sh</code> para PVE 8 y <code>scripts/global/update-pve9_2.sh</code> para PVE 9) siguen el mismo esquema, con URLs de repo y nombres de paquete propios de cada versión:",
"intro": "Un único worker (<code>scripts/global/update-pve-safe.sh</code>) maneja tanto PVE 8 como PVE 9. Detecta la versión mayor internamente y usa el codename propio de cada versión (<code>bookworm</code> o <code>trixie</code>) para sus fuentes base. Las etapas son:",
"items": [
"<strong>Higiene de repos.</strong> Elimina entradas duplicadas de <code>/etc/apt/sources.list</code> y <code>/etc/apt/sources.list.d/</code>. Comenta el repo enterprise si el host no tiene suscripción y escribe / habilita la fuente sin suscripción.",
"<strong>Apt update + full-upgrade.</strong> Trae las últimas listas de paquetes y aplica todas las actualizaciones disponibles para la versión mayor actual, ejecutándose con <code>DEBIAN_FRONTEND=noninteractive</code> y <code>--force-confdef --force-confold</code> para que cualquier archivo de configuración que personalizaste mantenga su contenido actual cuando upstream también lo cambió.",
"<strong>Comprobación de paquetes esenciales.</strong> Instala <code>zfsutils-linux</code>, <code>chrony</code>, <code>ifupdown2</code> y unos pocos más si el host no los tiene.",
"<strong>Comprobación de sanity de LVM / almacenamiento.</strong> Repara cabeceras PV faltantes si se detectan.",
"<strong>Eliminación de paquetes conflictivos.</strong> Quita paquetes conocidos por chocar en Proxmox (p. ej. algunos demonios de sincronización horaria que pelean con chrony)."
"<strong>Comprobaciones previas.</strong> Verifica al menos ~1 GB libres en <code>/var/cache/apt/archives</code> y hace ping a <code>download.proxmox.com</code>. Aborta pronto con un mensaje claro cuando falla cualquiera de las dos, para que la ejecución no muera a mitad de una transacción apt.",
"<strong>Bootstrap de repos.</strong> <code>ensure_repositories</code> escribe las fuentes base Proxmox / Debian solo cuando faltan (un host fresco o limpiado a mano); en un host configurado no hace nada.",
"<strong>Apt update con auto-recuperación de GPG.</strong> Ante un <code>NO_PUBKEY</code> de cualquier repo (los propios o uno de terceros), el worker importa la clave que falta y vuelve a intentar automáticamente antes de fallar.",
"<strong>Limpieza conservadora de duplicados.</strong> <code>cleanup_duplicate_repos</code> elimina solo coincidencias exactas por URL + Suite + Component contra <code>proxmox.sources</code> / <code>debian.sources</code>. Los archivos propios del usuario — enterprise, Ceph, mirrors NTP alternativos, entradas custom de <code>download.proxmox.com/*</code>, <code>pve-*.list</code> escritos a mano — se dejan intactos. Cada archivo se respalda antes de modificarse.",
"<strong>Actualizaciones pendientes + conteo de seguridad.</strong> Reporta cuántos paquetes van a cambiar y cuántos de esos vienen de la suite de seguridad, para que el diálogo de confirmación tenga números reales que mostrar.",
"<strong>Diálogo de confirmación.</strong> El wrapper pide un sí explícito antes de tocar apt.",
"<strong>apt full-upgrade.</strong> Se ejecuta con <code>DEBIAN_FRONTEND=noninteractive</code> y <code>--force-confdef --force-confold</code> para que cualquier archivo de configuración que personalizaste mantenga su contenido actual cuando upstream también lo cambió. Nunca sobrescribe silenciosamente configs editados por el usuario.",
"<strong>Comprobación de sanity LVM.</strong> <code>lvm_repair_check</code> refresca los metadatos de VG cuando discos en passthrough a VMs guest (DSM, TrueNAS, appliances de almacenamiento) vuelven con cabeceras PV antiguas.",
"<strong>Recompilación DKMS de los drivers gestionados por ProxMenux.</strong> Cuando el upgrade dejó staged un kernel nuevo, <code>pmx_rebuild_dkms_after_kernel</code> lee <code>components_status.json</code> para los drivers que ProxMenux instaló (actualmente <code>nvidia_driver</code> → módulo <code>nvidia</code>, <code>coral_driver</code> → módulo <code>gasket</code>), instala los headers de kernel correspondientes (<code>proxmox-headers-&lt;newkver&gt;</code> cuando está disponible, si no <code>pve-headers-&lt;newkver&gt;</code>) y ejecuta <code>dkms autoinstall -k &lt;newkver&gt;</code>. Si <code>dkms status</code> no muestra los módulos construidos contra el kernel nuevo, el worker cae a reejecutar cada instalador con <code>--auto-reinstall</code>. Cualquier fallo se registra pero no aborta la actualización — llegas al prompt de reinicio en todos los casos.",
"<strong>Post-limpieza.</strong> <code>apt-get autoremove</code> + <code>apt-get autoclean</code> antes de devolver el control al wrapper."
]
},
"post": {
@@ -91,14 +96,14 @@
"body": "Correr en un kernel antiguo tras actualizar <code>linux-image-*</code> significa que estás en un sistema medio actualizado: userspace nuevo, kernel viejo. La mayoría del tiempo las cosas funcionan, pero los módulos ZFS, grupos IOMMU, KSMBD y cualquier driver fuera del árbol solo cuadran con el kernel para el que fueron construidos — un desajuste produce fallos oscuros. Reinicia en el primer momento razonable."
},
"noSub": {
"heading": "Cuándo ocurre el cambio a sin suscripción",
"intro": "Proxmox entrega los hosts con el repo enterprise habilitado por defecto. Sin una suscripción de pago, ese repo devuelve 401 en <code>apt-get update</code>. El worker detecta esto y:",
"heading": "Cómo trata el worker seguro al repo enterprise",
"intro": "Proxmox entrega los hosts con el repo enterprise habilitado por defecto. Sin una suscripción de pago, ese repo devuelve 401 en <code>apt-get update</code>. El worker seguro deliberadamente <strong>no</strong> toca los repositorios enterprise ni Ceph — un host corriendo con una suscripción real no debe verse la configuración reescrita en silencio. Lo que ocurre en su lugar:",
"items": [
"Comenta (o deshabilita) <code>/etc/apt/sources.list.d/pve-enterprise.list</code> (o el equivalente deb822)",
"Escribe <code>/etc/apt/sources.list.d/pve-no-subscription.list</code> (o el deb822 <code>proxmox.sources</code> para PVE 9) con el codename correspondiente (<code>bookworm</code> para PVE 8, <code>trixie</code> para PVE 9)",
"Vuelve a ejecutar <code>apt-get update</code>"
"En un <strong>host bare</strong> sin fuentes base de Proxmox / Debian, <code>ensure_repositories</code> escribe la fuente sin suscripción en formato deb822 (<code>proxmox.sources</code>) con el codename correspondiente a la versión mayor detectada (<code>bookworm</code> para PVE 8, <code>trixie</code> para PVE 9) y las fuentes Debian correspondientes.",
"En un <strong>host configurado</strong>, <code>ensure_repositories</code> es un no-op — lo que el usuario haya elegido (sin suscripción, enterprise o una mezcla) se preserva.",
"Los archivos enterprise <code>pve-enterprise.sources</code> / <code>ceph.sources</code> nunca son modificados por la ruta de actualización. La eliminación del repo enterprise cuando no se quiere se gestiona desde otra parte de ProxMenux (el script post-instalación automatizado), no desde aquí."
],
"outro": "Si tienes una suscripción de pago, comenta la fuente sin suscripción y descomenta la enterprise antes de ejecutar esta opción."
"outro": "Si tienes una suscripción de pago, mantén <code>pve-enterprise.sources</code> habilitado y el worker seguro lo dejará dirigir el upgrade sin cambios. Si no la tienes, o bien ejecuta primero el post-instalación automatizado (que hace el cambio y lo registra) o comenta la fuente enterprise a mano — la ruta de actualización no lo hará por ti."
},
"cluster": {
"heading": "Consideraciones de clúster",
@@ -127,7 +132,7 @@
},
{
"title": "El kernel se actualizó pero los nuevos módulos faltan para un driver fuera del árbol",
"body": "Los módulos fuera del árbol (NVIDIA, ZFS vía DKMS, drivers de NIC propios) necesitan reconstruirse contra el nuevo kernel. La mayoría los gestiona automáticamente DKMS durante el upgrade — confirma con <code>dkms status</code>. Si falta algo: <code>dkms autoinstall</code>."
"body": "Los drivers que ProxMenux instaló vía DKMS (actualmente <code>nvidia_driver</code> y <code>coral_driver</code>) se recompilan automáticamente al final del upgrade contra la versión del kernel entrante, usando <code>dkms autoinstall -k &lt;newkver&gt;</code> y, si hace falta, un fallback a cada instalador con <code>--auto-reinstall</code>. Confirma con <code>dkms status</code>. Los módulos fuera del árbol de terceros que no están en el registro <code>components_status.json</code> de ProxMenux (drivers de NIC propios, paquetes DKMS instalados a mano, …) siguen necesitando un <code>dkms autoinstall</code> manual — el worker seguro solo toca lo que instaló originalmente."
},
{
"title": "El preguntar antes de reiniciar no apareció pero estoy seguro de que el kernel cambió",
@@ -137,7 +142,7 @@
},
"files": {
"heading": "Archivos implicados",
"code": "scripts/utilities/proxmox_update.sh # this script (wrapper)\nscripts/global/update-pve8.sh # worker for PVE 8 hosts\nscripts/global/update-pve9_2.sh # worker for PVE 9 hosts\nscripts/global/common-functions.sh # cleanup_duplicate_repos used by workers\n/etc/apt/sources.list # may be edited\n/etc/apt/sources.list.d/* # may be edited / created\n/var/run/reboot-required # read to decide on reboot prompt\n/var/log/apt/history.log # read to detect kernel changes"
"code": "scripts/utilities/proxmox_update.sh # este script (wrapper)\nscripts/global/update-pve-safe.sh # único worker seguro (PVE 8 + PVE 9)\nscripts/global/common-functions.sh # cleanup_duplicate_repos usado por el worker\nscripts/global/utils-install-functions.sh # ensure_repositories + pmx_rebuild_dkms_after_kernel\n/usr/local/share/proxmenux/components_status.json # registro de drivers DKMS gestionados por ProxMenux\n/etc/apt/sources.list.d/proxmox.sources # fuente deb822 sin suscripción (bootstrap host bare)\n/etc/apt/sources.list.d/debian.sources # fuentes deb822 de Debian (bootstrap host bare)\n/var/run/reboot-required # se lee para decidir el prompt de reinicio\n# Fallback de reinicio cuando needrestart no está instalado:\n# dpkg-query -W 'proxmox-kernel-*-pve-signed' 'pve-kernel-*-pve' vs. uname -r"
},
"related": {
"heading": "Relacionado",