diff --git a/AppImage/components/network-flow.tsx b/AppImage/components/network-flow.tsx index 8f6e4807..6a6c1517 100644 --- a/AppImage/components/network-flow.tsx +++ b/AppImage/components/network-flow.tsx @@ -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 = { nic: ``, bridge: ``, host: ``, + // 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: ``, lxc: ``, vm: ``, } @@ -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 + nics: NIC[] +} { + const declared = data.bonds || [] + const byId = new Map(declared.map((b) => [b.id, b])) + const bondOfNic = new Map() + 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() + 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(` + // 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(` ${svgIcon("nic", nicX, y, 18, stroke)} ${n.id} - ${n.link} + ${nicSubLabel(n)} + `) + }) + + // 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() + 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(` + + ${svgIcon("bond", bondX, y, 16, stroke)} + ${b.id} + ${b.mode || "bond"} + ${fmt(b.rx + b.tx)} `) }) @@ -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(``) // 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(``) + 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 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(``) 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(` + const opacity = isDown ? 0.45 : n.role === "standby" ? 0.72 : 1 + nodes.push(` ${svgIcon("nic", cx, cy, 13, color)} ${n.id} - ${isDown ? "down" : n.link} + ${nicSubLabel(n)} + `) + }) + + // 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(``) + 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(` + + ${svgIcon("bond", cx, cy, 13, color)} + ${b.id} + ${b.mode || "bond"} + ${fmt(rate)} `) }) @@ -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(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) }} > diff --git a/AppImage/components/network-metrics.tsx b/AppImage/components/network-metrics.tsx index 162f4eb4..fedd8faf 100644 --- a/AppImage/components/network-metrics.tsx +++ b/AppImage/components/network-metrics.tsx @@ -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 + // 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//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 && (
→ {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 ( - - ({bondInterface.bond_slaves.join(", ")}) - - ) - } - return null - })()} - - )} {interface_.bridge_bond_slaves && interface_.bridge_bond_slaves.length > 0 && ( ({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() {
{displayInterface.bridge_physical_interface}
- {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 ( -
-
Bond Members
-
- {bondInterface.bond_slaves.map((slave, idx) => ( - - {slave} - - ))} -
-
- ) - } - 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 && (
Bond Members
@@ -1418,26 +1413,53 @@ export function NetworkMetrics() {
Bonding Mode
-
{displayInterface.bond_mode || "Unknown"}
+
+ {displayInterface.bond_mode || "Unknown"} + {displayInterface.bond_mode_detail && + displayInterface.bond_mode_detail !== displayInterface.bond_mode && ( + + {" "} + ({displayInterface.bond_mode_detail}) + + )} +
{displayInterface.bond_active_slave && (
-
Active Slave
+
+ {displayInterface.bond_supports_failover ? "Active Slave" : "Primary Slave"} +
{displayInterface.bond_active_slave}
)}
Slave Interfaces
- {displayInterface.bond_slaves.map((slave, idx) => ( - - {slave} - - ))} + {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 ( + + {slave} + {role && · {role}} + + ) + })}
diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py index 4a484a20..f2cf5630 100644 --- a/AppImage/scripts/flask_server.py +++ b/AppImage/scripts/flask_server.py @@ -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//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/ 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 `@:` + 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: diff --git a/AppImage/scripts/health_monitor.py b/AppImage/scripts/health_monitor.py index 56554866..4ec5160e 100644 --- a/AppImage/scripts/health_monitor.py +++ b/AppImage/scripts/health_monitor.py @@ -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//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 diff --git a/AppImage/scripts/post_install_versions.py b/AppImage/scripts/post_install_versions.py index f3a86ba2..a19d5cf8 100644 --- a/AppImage/scripts/post_install_versions.py +++ b/AppImage/scripts/post_install_versions.py @@ -48,6 +48,9 @@ _FN_DEF_RE = re.compile(r"^(?P[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 +# `< 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 >... < + ProxMenux Logo +
+ +
+ + + +
+ Latest release + Latest beta + License + GitHub stars + Open issues +
+ +
+ +

+ ProxMenux is an interactive toolkit for Proxmox VE — a menu-driven CLI plus a web dashboard for post-install, backup/restore and continuous health monitoring of your homelab. +

+ +--- + +## 📌 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://: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. + + + ProxMenux contributors + + +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: + +

+ + Support on Ko-fi + +

+ +--- + +## 📈 Project Growth + +

+ + ProxMenux project growth + +

+ +

+ Stars and forks tracked with Repo Growth. +

diff --git a/install_proxmenux.sh b/install_proxmenux.sh index a98a5681..891d3b7e 100755 --- a/install_proxmenux.sh +++ b/install_proxmenux.sh @@ -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() { diff --git a/lang/de.json b/lang/de.json index 1a752ec8..46baaeb2 100644 --- a/lang/de.json +++ b/lang/de.json @@ -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", diff --git a/lang/es.json b/lang/es.json index 4e08e379..8a2cf4ac 100644 --- a/lang/es.json +++ b/lang/es.json @@ -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", diff --git a/lang/fr.json b/lang/fr.json index 5a56ea96..64a4b138 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -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", diff --git a/lang/it.json b/lang/it.json index a0f8dc68..a6b7c0c0 100644 --- a/lang/it.json +++ b/lang/it.json @@ -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", diff --git a/lang/pt.json b/lang/pt.json index 76a206ab..c5a59c6b 100644 --- a/lang/pt.json +++ b/lang/pt.json @@ -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", diff --git a/scripts/backup_restore/backup_scheduler.sh b/scripts/backup_restore/backup_scheduler.sh index 35174a8d..52b70822 100755 --- a/scripts/backup_restore/backup_scheduler.sh +++ b/scripts/backup_restore/backup_scheduler.sh @@ -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 diff --git a/scripts/backup_restore/lib_host_backup_common.sh b/scripts/backup_restore/lib_host_backup_common.sh index 00fd3d30..b94c5ae5 100755 --- a/scripts/backup_restore/lib_host_backup_common.sh +++ b/scripts/backup_restore/lib_host_backup_common.sh @@ -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 diff --git a/scripts/backup_restore/run_scheduled_backup.sh b/scripts/backup_restore/run_scheduled_backup.sh index f7f1c1c8..e21852dc 100755 --- a/scripts/backup_restore/run_scheduled_backup.sh +++ b/scripts/backup_restore/run_scheduled_backup.sh @@ -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 `|@:` +# lines, and the secret itself is in the `pbs--.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 diff --git a/web/app/[locale]/docs/monitor/dashboard/terminal/page.tsx b/web/app/[locale]/docs/monitor/dashboard/terminal/page.tsx index e2856e2c..c363c69a 100644 --- a/web/app/[locale]/docs/monitor/dashboard/terminal/page.tsx +++ b/web/app/[locale]/docs/monitor/dashboard/terminal/page.tsx @@ -113,6 +113,14 @@ export default async function TerminalTabPage({ {t.rich("target.body2", { strong, em, code, link: vmsLink })}

+

{t("menuGuard.heading")}

+

+ {t.rich("menuGuard.body1", { strong, em, code })} +

+

+ {t.rich("menuGuard.body2", { strong, em, code })} +

+

{t("fourTerminals.heading")}

{t("fourTerminals.intro")}

    diff --git a/web/app/[locale]/docs/monitor/health-monitor/page.tsx b/web/app/[locale]/docs/monitor/health-monitor/page.tsx index ecb58279..3afc57ee 100644 --- a/web/app/[locale]/docs/monitor/health-monitor/page.tsx +++ b/web/app/[locale]/docs/monitor/health-monitor/page.tsx @@ -225,6 +225,14 @@ export default async function HealthMonitorPage({ {t("dashboardView.pillBody")} +

    {t("dashboardView.updateNowTitle")}

    +

    + {t.rich("dashboardView.updateNowBody1", { strong, em, code })} +

    +

    + {t.rich("dashboardView.updateNowBody2", { strong, em, code })} +

    +

    {t("dismiss.heading")}

    {t.rich("dismiss.intro", { em })} diff --git a/web/app/[locale]/docs/post-install/network/page.tsx b/web/app/[locale]/docs/post-install/network/page.tsx index 609b31c8..121a72d5 100644 --- a/web/app/[locale]/docs/post-install/network/page.tsx +++ b/web/app/[locale]/docs/post-install/network/page.tsx @@ -96,6 +96,11 @@ export default async function PostInstallNetworkPage({ {t.rich("sysctl.sourceOutro", { code })}

    +

    {t("sysctl.fwbrTitle")}

    +

    + {t.rich("sysctl.fwbrBody", { code })} +

    + {t.rich("sysctl.rpFilterBody", { em, code })} diff --git a/web/app/[locale]/docs/post-install/storage/page.tsx b/web/app/[locale]/docs/post-install/storage/page.tsx index c8665852..f355a63f 100644 --- a/web/app/[locale]/docs/post-install/storage/page.tsx +++ b/web/app/[locale]/docs/post-install/storage/page.tsx @@ -91,7 +91,6 @@ export default async function PostInstallStoragePage({ {t("arc.headerRam")} - {t("arc.headerMin")} {t("arc.headerMax")} @@ -99,7 +98,6 @@ export default async function PostInstallStoragePage({ {arcRows.map((row) => ( {row.ram} - {row.min} {row.max} ))} diff --git a/web/messages/en/docs/backup-restore/creating-backups.json b/web/messages/en/docs/backup-restore/creating-backups.json index 92c9f6aa..dbefcdf7 100644 --- a/web/messages/en/docs/backup-restore/creating-backups.json +++ b/web/messages/en/docs/backup-restore/creating-backups.json @@ -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: manual (the interactive flow this page documents — the operator picks a destination and a profile from a menu and watches the archive land) or scheduled (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 run_scheduled_backup.sh; the archives produced are indistinguishable.", + "body": "Backups can be produced in two modes: manual (the interactive flow this page documents — the user picks a destination and a profile from a menu and watches the archive land) or scheduled (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 run_scheduled_backup.sh; 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 .tar.zst 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 .tar.zst." } ] @@ -45,11 +45,11 @@ "profiles": { "heading": "Default vs Custom profile", "defaultTitle": "Default profile", - "defaultBody": "The default profile is the curated list from hb_default_profile_paths (documented in How it works under Path categories) plus every entry in the persistent extras file /usr/local/share/proxmenux/backup-extra-paths.txt. 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 hb_default_profile_paths (documented in How it works under Path categories) plus every entry in the persistent extras file /usr/local/share/proxmenux/backup-extra-paths.txt. 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 [+]). The operator ticks the set for this run and can press Add custom path to append a new absolute path. Any path added inline is persisted to backup-extra-paths.txt 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 Manage custom paths 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 [+]). The user ticks the set for this run and can press Add custom path to append a new absolute path. Any path added inline is persisted to backup-extra-paths.txt 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 Manage custom paths 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. hb_prepare_staging assembles the archive tree in /tmp/proxmenux-DESTINATION-stage.XXXXXX and populates each of the three payloads.", "steps": [ - { "step": "1", "name": "rootfs assembly", "detail": "Runs rsync -a for each selected path into staging_root/rootfs/. Excludes volatile subpaths (bash history, caches, trash) from /root/. Paths absent from the source are recorded in metadata/missing_paths.txt without stopping the backup." }, + { "step": "1", "name": "rootfs assembly", "detail": "Runs rsync -a for each selected path into staging_root/rootfs/. The pmxcfs database at /var/lib/pve-cluster/config.db is captured separately via sqlite3 .backup to produce a consistent snapshot without stopping the cluster, and the .db-wal / .db-shm sidecars are excluded from the rsync; when sqlite3 is not available a config.db.raw-fallback is dropped instead, and the restore promotes it on import. Volatile subpaths (bash history, caches, trash) are excluded from /root/. Paths absent from the source are recorded in metadata/missing_paths.txt without stopping the backup." }, { "step": "2", "name": "Manifest generation", "detail": "build_manifest.sh orchestrates the six collectors and writes manifest.json 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": "apt-mark showmanual is captured verbatim into metadata/packages.manual.list. Component state is already inside the restored rootfs (components_status.json) because /usr/local/share/proxmenux/ is part of the default profile." }, { "step": "4", "name": "Run info", "detail": "metadata/run_info.env 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": "hb_notify_lifecycle \"start\" fires. If notifications are configured in the Monitor, an operator-facing Host backup started event is emitted. Silent if no channels are configured." } + { "step": "5", "name": "Notification (start)", "detail": "hb_notify_lifecycle \"start\" fires. If notifications are configured in the Monitor, an user-facing Host backup started event is emitted. Silent if no channels are configured." } ] }, "included": { @@ -75,7 +75,7 @@ "*.log — log files." ], "rootTitle": "/root/ exclusions", - "rootBody": "/root/ is part of the default profile so operator scripts and config land in the archive. Volatile subpaths are dropped:", + "rootBody": "/root/ is part of the default profile so user scripts and config land in the archive. Volatile subpaths are dropped:", "rootItems": [ ".bash_history", ".cache/", @@ -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 .pxar 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 /tmp/proxmenux-DESTINATION-backup-YYYYMMDD_HHMMSS.log 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": "hb_write_archive_sidecar drops a *.proxmenux.json next to the local archive so the Monitor identifies it as a ProxMenux host backup even after moves or renames." }, diff --git a/web/messages/en/docs/backup-restore/cross-kernel.json b/web/messages/en/docs/backup-restore/cross-kernel.json index 5dbc4709..e774df5e 100644 --- a/web/messages/en/docs/backup-restore/cross-kernel.json +++ b/web/messages/en/docs/backup-restore/cross-kernel.json @@ -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 GRUB_TIMEOUT, 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). _rs_hyd_grub merges every token from the backup's manifest.kernel_params.cmdline_extra into the target's live GRUB_CMDLINE_LINUX_DEFAULT, skipping tokens whose key the target already carries. Then merges whitelisted GRUB_* keys (GRUB_TIMEOUT, GRUB_TIMEOUT_STYLE, GRUB_DEFAULT, GRUB_TERMINAL, GRUB_DISABLE_OS_PROBER, GRUB_SERIAL_COMMAND, GRUB_GFXMODE, GRUB_GFXPAYLOAD_LINUX) from the backup's /etc/default/grub if they differ from the target's." }, - { "phase": "1b — systemd-boot / ZFS path", "detail": "For hosts using systemd-boot (typically ZFS-on-root). _rs_hyd_kernel_cmdline merges operator tokens from cmdline_extra into the target's /etc/kernel/cmdline, keeping the target's own root=, boot= and rootflags= boilerplate intact." }, + { "phase": "1b — systemd-boot / ZFS path", "detail": "For hosts using systemd-boot (typically ZFS-on-root). _rs_hyd_kernel_cmdline merges user tokens from cmdline_extra into the target's /etc/kernel/cmdline, keeping the target's own root=, boot= and rootflags= boilerplate intact." }, { "phase": "2 — /etc/modules merge", "detail": "_rs_hyd_modules appends modules from manifest.kernel_params.modules_loaded_at_boot that are in the whitelist (vfio, vfio_pci, vfio_iommu_type1, vfio_virqfd, kvm, kvm_intel, kvm_amd, nvidia, nvidia_drm, nvidia_modeset, nvidia_uvm, i915, xe) AND not already present in the target's /etc/modules." }, - { "phase": "3 — Whitelisted files copy", "detail": "_rs_hyd_files copies operator-authored files from the staging rootfs to the live target when the content differs. Whitelist covers VFIO/nvidia/blacklist files under /etc/modprobe.d, /etc/modules-load.d, and the ProxMenux VFIO bind rule + nvidia udev rules under /etc/udev/rules.d. Distro-owned files (pve-blacklist.conf, mdadm.conf, nvme.conf) are intentionally excluded — their contents evolve between releases." }, + { "phase": "3 — Whitelisted files copy", "detail": "_rs_hyd_files copies user-authored files from the staging rootfs to the live target when the content differs. Whitelist covers VFIO/nvidia/blacklist files under /etc/modprobe.d, /etc/modules-load.d, and the ProxMenux VFIO bind rule + nvidia udev rules under /etc/udev/rules.d. Distro-owned files (pve-blacklist.conf, mdadm.conf, nvme.conf) 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, HB_HYDRATION_APPLIED=1 propagates through plan.env to apply_pending_restore.sh, which forces NEEDS_INITRAMFS=1 and NEEDS_GRUB=1 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 _rs_apply_bk_older_hydration in plan mode: it computes exactly what would be merged, populates RS_HYDRATION_SUMMARY 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 commit mode — same phases, same logic, but this time each phase writes to the live target. Cancelling the confirmation dialog leaves the target untouched." }, "flowDiagram": { diff --git a/web/messages/en/docs/backup-restore/glossary.json b/web/messages/en/docs/backup-restore/glossary.json index 831d941d..e750271d 100644 --- a/web/messages/en/docs/backup-restore/glossary.json +++ b/web/messages/en/docs/backup-restore/glossary.json @@ -23,7 +23,7 @@ { "id": "passphrase-recuperacion", "term": "Recovery passphrase", - "def": "Text password the operator picks to protect the recovery envelope. 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. It is not the passphrase that unlocks the keyfile itself, 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 recovery envelope. 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. It is not the passphrase that unlocks the keyfile itself, 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 backup group. ProxMenux defaults to hostcfg-HOSTNAME and lets the operator edit it before upload; unsupported characters are stripped automatically." + "def": "Name of the backup group. ProxMenux defaults to hostcfg-HOSTNAME and lets the user edit it before upload; unsupported characters are stripped automatically." }, { "id": "pxar", diff --git a/web/messages/en/docs/backup-restore/how-it-works.json b/web/messages/en/docs/backup-restore/how-it-works.json index dee5eb9b..917e6b18 100644 --- a/web/messages/en/docs/backup-restore/how-it-works.json +++ b/web/messages/en/docs/backup-restore/how-it-works.json @@ -20,11 +20,11 @@ "heading": "Archive layout", "intro": "Every archive follows the same tree layout regardless of destination. The metadata/ subdirectory holds the structured payloads; the rootfs/ 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 rootfs/ tree is a plain filesystem copy produced by rsync from the source host. It contains a curated default profile of paths that matter for a Proxmox restore, plus any custom paths added by the operator to the backup job or interactive session. The set is deliberately narrow: only paths that either hold configuration or hold state that Proxmox cannot regenerate on its own.", + "intro": "The rootfs/ tree is a plain filesystem copy produced by rsync from the source host. It contains a curated default profile of paths that matter for a Proxmox restore, plus any custom paths added by the user to the backup job or interactive session. The set is deliberately narrow: only paths that either hold configuration or hold state that Proxmox cannot regenerate on its own.", "defaultProfileTitle": "The default profile", "defaultProfileBody": "The default profile is defined by hb_default_profile_paths in lib_host_backup_common.sh. 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 (config.db) is captured as a consistent snapshot via sqlite3 .backup without stopping the cluster, and the .db-wal / .db-shm sidecars are excluded from the rsync. When sqlite3 is not available, a config.db.raw-fallback 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 .link files under /etc/systemd/network 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 /usr/local/share/proxmenux/backup-extra-paths.txt holds a list of absolute paths the operator has marked as \"always include\" on this host. When a backup runs in Default 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; # comments are allowed.", + "customExtrasBody": "A text file at /usr/local/share/proxmenux/backup-extra-paths.txt holds a list of absolute paths the user has marked as \"always include\" on this host. When a backup runs in Default 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; # comments are allowed.", "customModeTitle": "2. Custom mode (per-run)", - "customModeBody": "Launching a backup in Custom 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 [+] and pre-checked). The operator ticks or unticks entries for that specific run, and can also press Add custom path to enter a new path — which is then persisted to the extras file for future backups.", + "customModeBody": "Launching a backup in Custom 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 [+] and pre-checked). The user ticks or unticks entries for that specific run, and can also press Add custom path 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 metadata/missing_paths.txt 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 /etc/wireguard or /etc/prometheus 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 metadata/missing_paths.txt 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 /etc/wireguard or /etc/prometheus 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 /dev/disk/by-id/* for portability), LVM volume groups + thin pools, physical disks with SMART capability, PVE storage.cfg entries, external mounts." + "content": "ZFS pools (with pool type + member disks resolved to /dev/disk/by-id/*, /dev/disk/by-partuuid/* or raw /dev/sdX paths depending on how each pool was originally created), LVM volume groups + thin pools, physical disks with SMART capability, PVE storage.cfg 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": "_rs_prepare_pending_restore stages risky paths under /var/lib/proxmenux/pending-restore/, writes plan.env, apply-on-boot.list and rs-skip-paths.txt, and enables proxmenux-restore-onboot.service to fire on the next boot." }, + { + "stage": "3b", + "name": "Data-pool import", + "reads": "manifest.storage_inventory + target ZFS state", + "action": "_rs_import_data_pools imports every non-root ZFS pool whose full set of disks is present on the target. Pools carrying a foreign hostid are retried with zpool import -f. Pools missing any disk are skipped with a warning. The per-pool outcome is recorded in /var/log/proxmenux/restore-datapools-<timestamp>.log." + }, { "stage": "4", "name": "Package install", diff --git a/web/messages/en/docs/backup-restore/index.json b/web/messages/en/docs/backup-restore/index.json index 9b73824a..1c10d79c 100644 --- a/web/messages/en/docs/backup-restore/index.json +++ b/web/messages/en/docs/backup-restore/index.json @@ -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 target's current kernel, 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 hydration 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 target's current kernel, 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 hydration 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", diff --git a/web/messages/en/docs/backup-restore/restoring.json b/web/messages/en/docs/backup-restore/restoring.json index c66817d5..51c2ce22 100644 --- a/web/messages/en/docs/backup-restore/restoring.json +++ b/web/messages/en/docs/backup-restore/restoring.json @@ -14,7 +14,7 @@ }, "intro": { "title": "The restore reproduces the source, not just the files", - "body": "Restoring a ProxMenux backup is not an extraction. Files are only the first step: after the filesystem is in place, the restore reads the manifest to detect differences between source and target, filters paths that would break the target's boot, hydrates operator-authored tuning that cannot be copied verbatim, and — after the mandatory reboot — reinstalls every component the source had (NVIDIA driver, Coral TPU, GPU tools) against the target's own kernel. What the operator picks from the menu is a single archive; what actually happens involves the compatibility check, the path classification, the on-boot dispatcher and the post-boot pass working together to produce a host that behaves like the source." + "body": "Restoring a ProxMenux backup is not an extraction. Files are only the first step: after the filesystem is in place, the restore reads the manifest to detect differences between source and target, filters paths that would break the target's boot, hydrates user-authored tuning that cannot be copied verbatim, and — after the mandatory reboot — reinstalls every component the source had (NVIDIA driver, Coral TPU, GPU tools) against the target's own kernel. What the user picks from the menu is a single archive; what actually happens involves the compatibility check, the path classification, the on-boot dispatcher and the post-boot pass working together to produce a host that behaves like the source." }, "threeActions": { "heading": "Three actions on a backup", @@ -52,10 +52,10 @@ }, { "output": "Rollback plan", - "detail": "Computed by compute_rollback_plan.sh. Lists VMs, LXCs and components present on the target but not in the backup. The operator opts in during the confirmation dialog to have these removed as part of the restore, so the target ends up matching the backup exactly." + "detail": "Computed by compute_rollback_plan.sh. Lists VMs, LXCs and components present on the target but not in the backup. The user opts in during the confirmation dialog to have these removed as part of the restore, so the target ends up matching the backup exactly." } ], - "reportBody": "The check also emits a structured report (HB_COMPAT_RESULTS) categorised as PASS / INFO / WARN / FAIL. WARN and FAIL entries surface in the pre-restore panel; the restore refuses to proceed only when a FAIL is present that the operator cannot resolve by clicking Continue." + "reportBody": "The check also emits a structured report (HB_COMPAT_RESULTS) categorised as PASS / INFO / WARN / FAIL. WARN and FAIL entries surface in the pre-restore panel; the restore refuses to proceed only when a FAIL is present that the user cannot resolve by clicking Continue." }, "twoModes": { "heading": "Full restore vs Custom restore", @@ -67,7 +67,7 @@ }, { "mode": "Custom restore", - "detail": "Opens a checklist showing every path the archive carries. The operator ticks a subset. Paths blocked by the cross-kernel filter appear greyed out and cannot be selected. Package install and component auto-reinstall are skipped by default in Custom mode — the operator is signalling that they want partial application, not a full reproduction." + "detail": "Opens a checklist showing every path the archive carries. The user ticks a subset. Paths blocked by the cross-kernel filter appear greyed out and cannot be selected. Package install and component auto-reinstall are skipped by default in Custom mode — the user is signalling that they want partial application, not a full reproduction." } ] }, @@ -92,7 +92,7 @@ "fullFlow": { "heading": "The full restore workflow", "intro": "The complete pipeline from archive selection to a working restored host. Each stage feeds the next; the state written by earlier stages is consumed by later ones.", - "diagram": "┌─────────────────────────────────────────────────────────────────┐\n│ Archive → staging_root/ │\n│ manifest.json metadata/ rootfs/ │\n└──────────────────────────────┬──────────────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 1. hb_compat_check │\n │ · hardware drift → RS_SKIP_PATHS │\n │ · kernel direction → same / bk_newer / bk_older │\n │ · NIC remap plan │\n │ · hydration plan (bk_older only) │\n │ · rollback plan │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 2. Confirmation dialog │\n │ Shows: hot count, pending count, drift, hydration, │\n │ NIC remap, cross-kernel note, reinstalls preview │\n └──────────────────────────────┬──────────────────────────┘\n │ (accepted)\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 3. Apply hot paths → LIVE │\n │ _rs_apply rsyncs each hot path │\n │ from staging_root/rootfs to / │\n │ (skips paths in RS_SKIP_PATHS) │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 4. _rs_prepare_pending_restore │\n │ /var/lib/proxmenux/pending-restore/ │\n │ ├── apply-on-boot.list │\n │ ├── plan.env │\n │ ├── rs-skip-paths.txt │\n │ └── rootfs/ (deferred paths) │\n │ Enable proxmenux-restore-onboot.service │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 5. _rs_run_complete_extras │\n │ packages.manual.list │\n │ → cascade-safe filter │\n │ → apt-get install │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 6. REBOOT │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 7. apply_pending_restore.sh (on early boot) │\n │ Reads plan.env │\n │ Applies apply-on-boot.list (filtered) │\n │ Installs the postboot systemd unit │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 8. apply_cluster_postboot.sh (~10 minutes) │\n │ Runs after pve-cluster is up │\n │ · Applies /etc/pve entries to pmxcfs │\n │ · update-initramfs -u -k all │\n │ · update-grub / proxmox-boot-tool refresh │\n │ · component --auto-reinstall (nvidia, coral, ...) │\n │ · Boot sanity check │\n │ · Notification: \"Host restore finished\" │\n └─────────────────────────────────────────────────────────┘" + "diagram": "┌─────────────────────────────────────────────────────────────────┐\n│ Archive → staging_root/ │\n│ manifest.json metadata/ rootfs/ │\n└──────────────────────────────┬──────────────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 1. hb_compat_check │\n │ · hardware drift → RS_SKIP_PATHS │\n │ · kernel direction → same / bk_newer / bk_older │\n │ · NIC remap plan │\n │ · hydration plan (bk_older only) │\n │ · rollback plan │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 2. Confirmation dialog │\n │ Shows: hot count, pending count, drift, hydration, │\n │ NIC remap, cross-kernel note, reinstalls preview │\n └──────────────────────────────┬──────────────────────────┘\n │ (accepted)\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 3. Apply hot paths → LIVE │\n │ _rs_apply rsyncs each hot path │\n │ from staging_root/rootfs to / │\n │ (skips paths in RS_SKIP_PATHS) │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 4. _rs_prepare_pending_restore │\n │ /var/lib/proxmenux/pending-restore/ │\n │ ├── apply-on-boot.list │\n │ ├── plan.env │\n │ ├── rs-skip-paths.txt │\n │ └── rootfs/ (deferred paths) │\n │ Enable proxmenux-restore-onboot.service │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 4b. _rs_import_data_pools │\n │ Imports non-root ZFS pools whose disks are all │\n │ present; retries with -f on foreign hostid; │\n │ skips with a warning when disks are missing. │\n │ Log: /var/log/proxmenux/restore-datapools-*.log │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 5. _rs_run_complete_extras │\n │ packages.manual.list │\n │ → cascade-safe filter │\n │ → apt-get install │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 6. REBOOT │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 7. apply_pending_restore.sh (on early boot) │\n │ Reads plan.env │\n │ Applies apply-on-boot.list (filtered) │\n │ Installs the postboot systemd unit │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 8. apply_cluster_postboot.sh (~10 minutes) │\n │ Runs after pve-cluster is up │\n │ · Applies /etc/pve entries to pmxcfs │\n │ · update-initramfs -u -k all │\n │ · update-grub / proxmox-boot-tool refresh │\n │ · component --auto-reinstall (nvidia, coral, ...) │\n │ · Boot sanity check │\n │ · Notification: \"Host restore finished\" │\n └─────────────────────────────────────────────────────────┘" }, "pendingMachinery": { "heading": "The pending-restore machinery", @@ -100,7 +100,7 @@ "rows": [ { "file": "apply-on-boot.list", - "content": "One relative path per line — the exact set of paths apply_pending_restore.sh will apply from the staged rootfs. Reading this file tells the operator exactly what will change on the next boot." + "content": "One relative path per line — the exact set of paths apply_pending_restore.sh will apply from the staged rootfs. Reading this file tells the user exactly what will change on the next boot." }, { "file": "plan.env", @@ -123,7 +123,7 @@ "tasks": [ { "task": "Apply /etc/pve", - "detail": "/etc/pve is a live pmxcfs FUSE mount — it cannot be written to on early boot. The dispatcher copies files from the pending rootfs to the live /etc/pve once the cluster filesystem is up, one file at a time, without restarting pve-cluster." + "detail": "/etc/pve is a live pmxcfs FUSE mount — it cannot be written to on early boot. The dispatcher copies files from the pending rootfs to the live /etc/pve once the cluster filesystem is up, one file at a time, without restarting pve-cluster. The captured /var/lib/pve-cluster/config.db from the backup (or the promoted config.db.raw-fallback) is materialised with the systemctl stop pve-cluster → atomic file copy → systemctl start pve-cluster pattern, so pmxcfs re-reads the restored state on the way back up." }, { "task": "Rebuild initramfs and bootloader", @@ -159,6 +159,10 @@ "name": "Component list", "detail": "One row per driver being reinstalled (NVIDIA, Intel GPU tools, AMD tools, Coral TPU) with installing / ok / failed plus a link to the per-component log at /var/log/proxmenux/component-*.log." }, + { + "name": "Data-pool import", + "detail": "The data_pools_import block shows five colour-coded rows — Imported, Forced (imported with -f after a foreign hostid), Skipped partial (pools with only some disks present), Skipped missing (no disks present) and Failed. The state is persisted in /var/lib/proxmenux/restore-state.json, and the raw per-run log lives at /var/log/proxmenux/restore-datapools-<timestamp>.log." + }, { "name": "Boot sanity warnings", "detail": "The dispatcher checks for missing /lib/modules/<kernel>, absent ESP configuration and dangling /vmlinuz symlinks. Any warning surfaces here as a coloured banner." @@ -221,14 +225,14 @@ { "stage": "Sanity check + notification", "time": "~2 s", - "detail": "Cheap. The operator learns the run completed the moment the notification fires." + "detail": "Cheap. The user learns the run completed the moment the notification fires." } ], - "outroBody": "The window matters because the operator can log in during this time and see missing tools (nvidia-smi not found, coral not detected). This is expected — the reinstall is still running in the background. The completion notification signals when everything is ready." + "outroBody": "The window matters because the user can log in during this time and see missing tools (nvidia-smi not found, coral not detected). This is expected — the reinstall is still running in the background. The completion notification signals when everything is ready." }, "destructiveRollback": { "heading": "The optional destructive rollback", - "body": "A restore is additive by default: if the target has VMs, LXCs or components that are not in the backup, they survive. This preserves work but can leave stale hostpci entries, orphan components or VMs the operator no longer wants. When drift is detected between target and backup, the confirmation dialog offers a second yes/no: Execute destructive rollback?. Accepting it triggers _rs_execute_rollback, which deletes the VMs, LXCs and components that exist on the target but not in the backup. The rollback runs BEFORE the reboot so that the pending restore machinery sees a clean state. This is opt-in — the default is No, and the operator sees exactly what would be removed listed by name (VMID and CTID) before deciding." + "body": "A restore is additive by default: if the target has VMs, LXCs or components that are not in the backup, they survive. This preserves work but can leave stale hostpci entries, orphan components or VMs the user no longer wants. When drift is detected between target and backup, the confirmation dialog offers a second yes/no: Execute destructive rollback?. Accepting it triggers _rs_execute_rollback, which deletes the VMs, LXCs and components that exist on the target but not in the backup. The rollback runs BEFORE the reboot so that the pending restore machinery sees a clean state. This is opt-in — the default is No, and the user sees exactly what would be removed listed by name (VMID and CTID) before deciding." }, "logs": { "heading": "Where the logs live", @@ -247,7 +251,11 @@ }, { "log": "/var/log/proxmenux/component--YYYYMMDD_HHMMSS.log", - "detail": "One log per component installer (nvidia, coral, etc.) that ran in the post-boot pass. Useful when a specific component's reinstall failed and the operator needs the tool's own output." + "detail": "One log per component installer (nvidia, coral, etc.) that ran in the post-boot pass. Useful when a specific component's reinstall failed and the user needs the tool's own output." + }, + { + "log": "/var/log/proxmenux/restore-datapools-YYYYMMDD_HHMMSS.log", + "detail": "Written by _rs_import_data_pools. Contains: enumeration of non-root pools from the manifest, per-pool outcome (imported cleanly, imported with -f for foreign hostid, skipped due to missing disks, or failed), and the raw output of each zpool import call." } ] }, diff --git a/web/messages/en/docs/backup-restore/scheduled-jobs.json b/web/messages/en/docs/backup-restore/scheduled-jobs.json index a6cc2612..5357ec6a 100644 --- a/web/messages/en/docs/backup-restore/scheduled-jobs.json +++ b/web/messages/en/docs/backup-restore/scheduled-jobs.json @@ -39,7 +39,7 @@ "heading": "How attach mode works", "intro": "PVE writes vzdump tasks to /etc/pve/jobs.cfg — 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 hb_pve_list_vzdump_jobs_for_backend. The operator picks one." }, + { "step": "1", "detail": "During job creation, ProxMenux lists compatible parent PVE tasks via hb_pve_list_vzdump_jobs_for_backend. The user picks one." }, { "step": "2", "detail": "The job's .env is written with PVE_PARENT_JOB, PVE_STORAGE and the inherited KEEP_* values; no systemd timer is created." }, { "step": "3", "detail": "hb_install_vzdump_hook registers a script-hook in /etc/vzdump.conf. When PVE runs any vzdump task, the hook script fires; if the $STOREID passed to it matches an attached job's PVE_STORAGE, the runner is invoked for that job." }, { "step": "4", "detail": "The archive lands in the same storage the vzdump dumps just wrote to: path/dump/ 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 hb_notify_lifecycle events as the interactive flow (start, complete, fail). 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 hb_notify_lifecycle events as the interactive flow (start, complete, fail). 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", diff --git a/web/messages/en/docs/help-info/update-commands.json b/web/messages/en/docs/help-info/update-commands.json index 6cd05a52..06c13f20 100644 --- a/web/messages/en/docs/help-info/update-commands.json +++ b/web/messages/en/docs/help-info/update-commands.json @@ -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", diff --git a/web/messages/en/docs/installation.json b/web/messages/en/docs/installation.json index a781b98c..6d90772f 100644 --- a/web/messages/en/docs/installation.json +++ b/web/messages/en/docs/installation.json @@ -57,7 +57,7 @@ }, "updating": { "heading": "Updating", - "body": "ProxMenux self-updates. When a new version is available, you're prompted on the next menu 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 menu 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 menu 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 (bash -c \"$(wget -qLO - https://raw.githubusercontent.com/MacRimi/ProxMenux/main/install_proxmenux.sh)\" for stable, or the equivalent install_proxmenux_beta.sh URL for beta) to run from SSH or console." }, "uninstall": { "heading": "Uninstalling", diff --git a/web/messages/en/docs/monitor/dashboard/terminal.json b/web/messages/en/docs/monitor/dashboard/terminal.json index 684e0757..fa380971 100644 --- a/web/messages/en/docs/monitor/dashboard/terminal.json +++ b/web/messages/en/docs/monitor/dashboard/terminal.json @@ -10,7 +10,7 @@ }, "intro": { "title": "A real PTY in the browser", - "body": "The terminal allocates a server-side PTY through flask_terminal_routes, pipes it over a WebSocket to xterm.js in the browser, and runs as root (the systemd unit's user). Anything you can do in ssh root@<host> works here — including vim, tmux, ncurses tools and Proxmox CLIs (qm, pct, pvesh, pvecm)." + "body": "The terminal allocates a server-side PTY through flask_terminal_routes, pipes it over a WebSocket to xterm.js in the browser, and runs as root (the systemd unit's user). Anything you can do in ssh root@<host> works here — including vim, tmux, ncurses tools and Proxmox CLIs (qm, pct, pvesh, pvecm). Every PTY started from this tab exports the environment variable PROXMENUX_TERMINAL=monitor, which is inherited by every child bash process — including a subsequent menu 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 (1 / 4 terminals), + New, Search, Clear and Close. The mobile keyboard helpers appear under the terminal on touch devices.", @@ -19,6 +19,11 @@ "body1": "The Terminal tab opens a shell on the Proxmox host itself — the same login you would get over SSH. Each tab opens a brand-new host terminal.", "body2": "To reach an LXC container from the browser, use the dedicated Console button on every running CT card in the VMs & LXCs tab. It opens a modal that runs pct enter <vmid> and reuses the same mobile-friendly toolbar described below." }, + "menuGuard": { + "heading": "Self-update guard when menu runs inside this terminal", + "body1": "ProxMenux's menu 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 check_updates_stable, check_updates_beta and apply_release_channel read PROXMENUX_TERMINAL 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 — bash -c \"$(wget -qLO - https://raw.githubusercontent.com/MacRimi/ProxMenux/main/install_proxmenux.sh)\" for stable, and the equivalent install_proxmenux_beta.sh 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:", diff --git a/web/messages/en/docs/monitor/health-monitor.json b/web/messages/en/docs/monitor/health-monitor.json index 22646817..04b994e4 100644 --- a/web/messages/en/docs/monitor/health-monitor.json +++ b/web/messages/en/docs/monitor/health-monitor.json @@ -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 Update Now button that runs the full ProxMenux update flow inline; on close it re-hits the health endpoint with ?refresh=1 so the badge count reflects the new state without waiting for the next 5-min cycle." }, { "category": "Security & Certificates", @@ -151,11 +151,14 @@ "Dismissed — 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 Active." ], "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 System Updates category reports one or more pending updates, the Overview header renders an Update Now button next to the health pill. The button opens a modal that runs the same host-side flow as ProxMenux → Settings post-install Proxmox → Proxmox System Update: repo hygiene, apt full-upgrade via the safe worker update-pve-safe.sh, 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 GET /api/health/details?refresh=1, 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 System Updates: pending updates available 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 System Updates: pending updates available stays true until you patch the host, and you don't want a notification every five minutes for a week. The Update Now 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": "Per-event Dismiss action in the modal. The Dismiss button opens a small dropdown with three options — 24 hours, 7 days or Permanently — letting you choose how long this specific alert stays silenced regardless of the category's default. Picking one calls POST /api/health/acknowledge with the error_key and the chosen suppression_hours (-1 for permanent). The event moves to the Dismissed list with a timestamped acknowledged_at.", "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 Permanent badge in the Dismissed list and never re-fire.", diff --git a/web/messages/en/docs/post-install/automated.json b/web/messages/en/docs/post-install/automated.json index 7b9c92c6..c0975a30 100644 --- a/web/messages/en/docs/post-install/automated.json +++ b/web/messages/en/docs/post-install/automated.json @@ -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-.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" } diff --git a/web/messages/en/docs/post-install/network.json b/web/messages/en/docs/post-install/network.json index f8bf647c..9fa85418 100644 --- a/web/messages/en/docs/post-install/network.json +++ b/web/messages/en/docs/post-install/network.json @@ -54,6 +54,8 @@ } ], "sourceOutro": "It also adds source /etc/network/interfaces.d/* to /etc/network/interfaces 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 /usr/local/sbin/proxmenux-fwbr-tune that applies rp_filter=0 and log_martians=0 to the fwbr* / fwln* / fwpr* / tap* interfaces Proxmox creates around VMs and containers. The helper is invoked by the proxmenux-fwbr-tune.service one-shot unit at boot, and by the /etc/udev/rules.d/99-proxmenux-fwbr-tune.rules rule on every net add 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 different 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. rp_filter=2 (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 hotplug 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 /etc/systemd/network/10-<iface>.link:", - "writtenOutro": "Any pre-existing .link files in that directory are copied to /etc/systemd/network/backup-<timestamp>/ before touching anything.", + "writtenIntro": "One file per physical NIC, at /etc/systemd/network/10-proxmenux-<iface>.link. Each file starts with the header # Managed by ProxMenux — do not edit so ProxMenux can distinguish its own files from user- or package-provided .link files.", + "writtenOutro": ".link 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 MACAddress= from every 10-proxmenux-*.link and drops any whose MAC no longer appears under /sys/class/net/. Files written by an earlier ProxMenux release in the old 10-<iface>.link format are migrated automatically the first time.", "pveTitle": "PVE 9 vs PVE 8", "pveBody": "On Proxmox VE 9 (systemd-networkd native), the script reloads udev rules after writing the .link files so new hotplug NICs pick up the correct name without a reboot. On PVE 8 (ifupdown2), 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 /etc/network/interfaces that references NIC names generated by the kernel's default scheme, pinning today's 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": "Uninstall Optimizations deletes every .link file from /etc/systemd/network/, 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": "Uninstall Optimizations deletes only the 10-proxmenux-*.link files carrying the ProxMenux header from /etc/systemd/network/. Any .link 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", diff --git a/web/messages/en/docs/post-install/optional.json b/web/messages/en/docs/post-install/optional.json index b2585266..5bfdf029 100644 --- a/web/messages/en/docs/post-install/optional.json +++ b/web/messages/en/docs/post-install/optional.json @@ -145,10 +145,11 @@ "doesLabel": "What ProxMenux does:", "doesItems": [ "Detects whether the root disk is SSD/NVMe by reading /sys/block/<dev>/queue/rotational. On a rotational disk the Automated flow asks first before installing.", - "Clones the upstream repository (azlux/log2ram) into /tmp/log2ram and runs its install.sh, then enables the log2ram systemd unit.", + "Clones the upstream repository (azlux/log2ram) into /tmp/log2ram and patches its install.sh, replacing rsync -aAXv with rsync -aXv --no-acls. That adjustment avoids the set_acl: Operation not supported exit 23 failure that the vanilla script triggers when moving /var/log on filesystems without ACL support. The log2ram systemd unit is enabled after install.", "Sizes the ramdisk based on host RAM: ≤ 8 GB → 128M, ≤ 16 GB → 256M, > 16 GB → 512M. Writes the value to SIZE= in /etc/log2ram.conf.", "Schedules a periodic disk sync via /etc/cron.d/log2ram: every 1h / 3h / 6h according to the same RAM tier.", - "Installs an auto-sync guard at /usr/local/bin/log2ram-check.sh, wired to /etc/cron.d/log2ram-auto-sync to run every 10 minutes. When /var/log reaches 80% of the ramdisk size the guard vacuums journald; at 92% it also truncates pveproxy access/error and pveam.log before syncing — the plain log2ram write command copies tmpfs to disk but does NOT shrink the tmpfs, so this guard prevents PVE from crashing with No space left on device when logs grow uncontrolled.", + "Installs an auto-sync guard at /usr/local/bin/log2ram-check.sh, wired to /etc/cron.d/log2ram-auto-sync to run every 10 minutes. When /var/log reaches 80% of the ramdisk size the guard vacuums journald; at 92% it first runs logrotate -f /etc/logrotate.d/proxmox-backup-api when that file exists (forcing rotation of the PBS API logs, typically the largest consumer on a host running PBS) and then truncates pveproxy access/error and pveam.log before syncing. The plain log2ram write command copies tmpfs to disk but does NOT shrink the tmpfs, so this guard prevents PVE from crashing with No space left on device when logs grow uncontrolled.", + "Detects whether proxmox-backup-server runs as a service on the host. When present, drops /etc/logrotate.d/proxmox-backup-api (20 MB × 3 rotation) and /etc/cron.hourly/proxmox-backup-logrotate, 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 (SystemMaxUse, RuntimeMaxUse) to fit within the ramdisk so a single burst cannot fill it.", "Registers itself in installed_tools.json so it can be reverted from Uninstall Optimizations." ], diff --git a/web/messages/en/docs/post-install/storage.json b/web/messages/en/docs/post-install/storage.json index 8ab79acb..1ac7960f 100644 --- a/web/messages/en/docs/post-install/storage.json +++ b/web/messages/en/docs/post-install/storage.json @@ -18,30 +18,26 @@ "intro": "The Adaptive Replacement Cache (ARC) 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 /etc/modprobe.d/99-zfsarc.conf 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 /etc/modprobe.d/99-zfsarc.conf contains a single directive — options zfs zfs_arc_max=…. Every other ZFS module parameter (zfs_arc_min, L2ARC prefetch/write throttle, TXG timeout) is left at its OpenZFS default. After writing the file, ProxMenux runs update-initramfs -u -k all and, when applicable, proxmox-boot-tool refresh, 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 zfs kernel module loads. They do not 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 zfs kernel module loads. To make the cap take effect on ZFS-on-root hosts, ProxMenux regenerates the initramfs with update-initramfs -u -k all and, when applicable, refreshes the boot loader with proxmox-boot-tool refresh. 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 zfs 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 zpool list 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": { diff --git a/web/messages/en/docs/post-install/uninstall.json b/web/messages/en/docs/post-install/uninstall.json index 7a34970a..d1bbe38a 100644 --- a/web/messages/en/docs/post-install/uninstall.json +++ b/web/messages/en/docs/post-install/uninstall.json @@ -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 Subscription Banner Removal reinstalls pve-manager, proxmox-widget-toolkit, libjs-extjs and libpve-http-server-perl with --force-confnew. 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 .link files (Persistent Interface Names) and reverting IOMMU/VFIO do not affect the running system — they only matter after a reboot. ProxMenux sets the reboot flag automatically for these.", + "rebootBody": "Removing ProxMenux's 10-proxmenux-*.link files (Persistent Interface Names) and reverting IOMMU/VFIO 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 Log2RAM. Nothing else is touched, and the registry is updated accordingly." }, diff --git a/web/messages/en/docs/settings/beta-program.json b/web/messages/en/docs/settings/beta-program.json index 4baa6bc5..112f7a58 100644 --- a/web/messages/en/docs/settings/beta-program.json +++ b/web/messages/en/docs/settings/beta-program.json @@ -53,7 +53,8 @@ "Your existing config, ProxMenux Monitor login (auth.json), 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 auth.json, 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": { diff --git a/web/messages/en/docs/utils/system-update.json b/web/messages/en/docs/utils/system-update.json index 9c087f7e..86c2dae4 100644 --- a/web/messages/en/docs/utils/system-update.json +++ b/web/messages/en/docs/utils/system-update.json @@ -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 (update-pve-safe.sh) 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 Update Now 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 exactly 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 scripts/utilities/proxmox_update.sh and the per-version worker scripts — nothing implied, every step is in the code:", + "intro": "This option runs exactly 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 scripts/utilities/proxmox_update.sh and the single safe worker scripts/global/update-pve-safe.sh — nothing implied, every step is in the code:", "items": [ - "Detects the PVE major version (pveversion | grep -oP ''pve-manager/\\K[0-9]+'') and dispatches to update-pve8.sh or update-pve9_2.sh so the right codename and repo URLs are used.", - "Cleans up repositories 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.", - "Runs the upgrade non-interactively with DEBIAN_FRONTEND=noninteractive and --force-confdef --force-confold — meaning if a configuration file you already modified also changed upstream, your version stays in place. No silent overwrites of custom configs.", - "Installs essential Proxmox packages if any are missing (zfsutils-linux, proxmox-backup-restore-image, chrony).", + "Detects the PVE major version from inside the worker (pveversion | grep -oP ''pve-manager/\\K[0-9]+'') 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.", + "Cleans up repositories in a conservative way. ensure_repositories 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. cleanup_duplicate_repos then removes exact URL + Suite + Component duplicates only against proxmox.sources / debian.sources; user-authored files (enterprise, Ceph, alternative NTP mirrors, custom download.proxmox.com/* entries or hand-written pve-*.list) are left untouched, and every file is backed up before being edited.", + "Runs the upgrade non-interactively with DEBIAN_FRONTEND=noninteractive and --force-confdef --force-confold — if a configuration file you already modified also changed upstream, your version stays in place. No silent overwrites of custom configs.", + "Skips forcing optional utilities. The safe worker does not push zfsutils-linux, chrony, ifupdown2 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.", "LVM metadata sanity check against stray PV headers from passthrough disks (warn-only, no automatic fix).", "Cleans up afterwards: apt-get autoremove -y + apt-get autoclean -y.", - "Reboot prompt only if the kernel actually changed (/var/run/reboot-required present or linux-image in the upgrade log)." + "DKMS rebuild before the reboot prompt. When the upgrade staged a new kernel, the wrapper calls pmx_rebuild_dkms_after_kernel 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 /var/run/reboot-required when needrestart is present, and falls back to a dpkg-query comparison between the running kernel and the newest installed proxmox-kernel-*-pve-signed / pve-kernel-*-pve package when it isn't — a signal that survives the many Proxmox hosts that ship without needrestart." ] }, "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 (scripts/global/update-pve8.sh for PVE 8 and scripts/global/update-pve9_2.sh for PVE 9) follow the same outline, with version-appropriate repo URLs and package names:", + "intro": "A single worker (scripts/global/update-pve-safe.sh) handles both PVE 8 and PVE 9. It detects the major version internally and uses the version-appropriate codename (bookworm or trixie) for its base sources. The stages are:", "items": [ - "Repo hygiene. Removes duplicate entries from /etc/apt/sources.list and /etc/apt/sources.list.d/. Comments out the enterprise repo if the host has no subscription and writes / enables the no-subscription source.", - "Apt update + full-upgrade. Pulls the latest package lists and applies all available upgrades for the current major version, running with DEBIAN_FRONTEND=noninteractive and --force-confdef --force-confold so any configuration file you customised keeps its current contents when upstream also changed it.", - "Essential packages check. Installs zfsutils-linux, chrony, ifupdown2 and a few others if the host is missing them.", - "LVM / storage sanity check. Repairs missing PV headers if detected.", - "Conflicting package removal. Drops packages known to clash on Proxmox (e.g. some time-sync daemons that fight chrony)." + "Sanity checks. Verifies at least ~1 GB free in /var/cache/apt/archives and pings download.proxmox.com. Aborts early with a clear message when either fails, so the run doesn't die in the middle of an apt transaction.", + "Repo bootstrap. ensure_repositories writes the base Proxmox / Debian sources only when they are missing (a fresh or hand-cleaned host); on a configured host it does nothing.", + "Apt update with GPG auto-recovery. On NO_PUBKEY for any repo (yours or a third-party one) the worker imports the missing key and retries automatically before failing.", + "Conservative duplicate cleanup. cleanup_duplicate_repos only removes exact URL + Suite + Component matches against proxmox.sources / debian.sources. User-authored files — enterprise, Ceph, alternative NTP mirrors, custom download.proxmox.com/* entries, hand-written pve-*.list — are left intact. Every file is backed up before modification.", + "Pending upgrades + security count. 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.", + "Confirmation dialog. The wrapper asks for an explicit yes before touching apt.", + "apt full-upgrade. Runs with DEBIAN_FRONTEND=noninteractive and --force-confdef --force-confold so any configuration file you customised keeps its current contents when upstream also changed it. Never overwrites operator-edited configs silently.", + "LVM sanity check. lvm_repair_check refreshes VG metadata when disks passed through to guest VMs (DSM, TrueNAS, storage appliances) come back with old PV headers.", + "DKMS rebuild for ProxMenux-managed drivers. When the upgrade staged a new kernel, pmx_rebuild_dkms_after_kernel reads components_status.json for the drivers ProxMenux installed (currently nvidia_driver → module nvidia, coral_driver → module gasket), installs the matching kernel headers (proxmox-headers-<newkver> when available, else pve-headers-<newkver>) and runs dkms autoinstall -k <newkver>. If dkms status then doesn't show the modules built against the new kernel, the worker falls back to re-running each installer with --auto-reinstall. Any failure is logged but does not abort the update — you land on the reboot prompt in every case.", + "Post-cleanup. apt-get autoremove + apt-get autoclean before returning control to the wrapper." ] }, "post": { @@ -91,14 +96,14 @@ "body": "Running on an old kernel after upgrading linux-image-* 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 apt-get update. 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 apt-get update. The safe worker deliberately does not 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) /etc/apt/sources.list.d/pve-enterprise.list (or the deb822 equivalent)", - "Writes /etc/apt/sources.list.d/pve-no-subscription.list (or the deb822 proxmox.sources for PVE 9) with the matching codename (bookworm for PVE 8, trixie for PVE 9)", - "Re-runs apt-get update" + "On a bare host with no base Proxmox / Debian sources at all, ensure_repositories writes the no-subscription source in the deb822 format (proxmox.sources) with the codename matching the detected major version (bookworm for PVE 8, trixie for PVE 9) and the matching Debian sources.", + "On a configured host, ensure_repositories is a no-op — whatever the operator chose (no-subscription, enterprise, or a mix) is preserved.", + "The enterprise pve-enterprise.sources / ceph.sources 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 pve-enterprise.sources 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 dkms status. If something is missing: dkms autoinstall." + "body": "The drivers ProxMenux installed via DKMS (currently nvidia_driver and coral_driver) are rebuilt automatically at the end of the upgrade against the incoming kernel version, using dkms autoinstall -k <newkver> and, when needed, a fallback to each installer with --auto-reinstall. Confirm with dkms status. Third-party out-of-tree modules that aren't in ProxMenux's components_status.json registry (custom NIC drivers, hand-installed DKMS packages, …) still need a manual dkms autoinstall — 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", diff --git a/web/messages/es/docs/backup-restore/creating-backups.json b/web/messages/es/docs/backup-restore/creating-backups.json index 71bbb8fe..f4b2188f 100644 --- a/web/messages/es/docs/backup-restore/creating-backups.json +++ b/web/messages/es/docs/backup-restore/creating-backups.json @@ -98,7 +98,7 @@ { "step": "1", "name": "Ensamblado del rootfs", - "detail": "Ejecuta rsync -a por cada ruta seleccionada hacia staging_root/rootfs/. Excluye subrutas volátiles (historial de bash, cachés, papelera) de /root/. Las rutas ausentes en el origen se registran en metadata/missing_paths.txt sin detener la copia." + "detail": "Ejecuta rsync -a por cada ruta seleccionada hacia staging_root/rootfs/. La base pmxcfs en /var/lib/pve-cluster/config.db se captura aparte con sqlite3 .backup para obtener un snapshot consistente sin parar el clúster, y los sidecars .db-wal / .db-shm se excluyen del rsync; si sqlite3 no está disponible, se deposita un config.db.raw-fallback que la restauración promociona al importar. Excluye subrutas volátiles (historial de bash, cachés, papelera) de /root/. Las rutas ausentes en el origen se registran en metadata/missing_paths.txt 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 .pxar 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", diff --git a/web/messages/es/docs/backup-restore/glossary.json b/web/messages/es/docs/backup-restore/glossary.json index 3a209fb3..c96e29a7 100644 --- a/web/messages/es/docs/backup-restore/glossary.json +++ b/web/messages/es/docs/backup-restore/glossary.json @@ -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 sobre de recuperación. 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. No sirve para desbloquear la clave del keyfile 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 sobre de recuperación. 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. No sirve para desbloquear la clave del keyfile ni se envía nunca a PBS. Nunca sale del host durante una copia normal." }, { "id": "clave-cifrado", diff --git a/web/messages/es/docs/backup-restore/how-it-works.json b/web/messages/es/docs/backup-restore/how-it-works.json index ba32ba70..f5c19b6c 100644 --- a/web/messages/es/docs/backup-restore/how-it-works.json +++ b/web/messages/es/docs/backup-restore/how-it-works.json @@ -20,7 +20,7 @@ "heading": "Layout del archivo", "intro": "Cada archivo respeta el mismo layout con independencia del destino. El subdirectorio metadata/ contiene los bloques estructurados; el subdirectorio rootfs/ 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 (config.db) se captura con un snapshot consistente vía sqlite3 .backup sin parar el clúster, y los sidecars .db-wal y .db-shm quedan excluidos del rsync. Si sqlite3 no está disponible, la copia deposita un config.db.raw-fallback 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 .link bajo /etc/systemd/network 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 /dev/disk/by-id/* para portabilidad), grupos de volúmenes LVM + thin pools, discos físicos con capacidad SMART, entradas de storage.cfg de PVE, puntos de montaje externos." + "content": "Pools ZFS (con tipo de pool y discos miembro resueltos a /dev/disk/by-id/*, /dev/disk/by-partuuid/* o rutas /dev/sdX 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 storage.cfg 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": "_rs_prepare_pending_restore deja las rutas de riesgo preparadas bajo /var/lib/proxmenux/pending-restore/, escribe plan.env, apply-on-boot.list y rs-skip-paths.txt, y habilita proxmenux-restore-onboot.service 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": "_rs_import_data_pools 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 zpool import -f. Los pools con discos faltantes se saltan con una advertencia. El resultado por pool queda registrado en /var/log/proxmenux/restore-datapools-<timestamp>.log." + }, { "stage": "4", "name": "Instalación de paquetes", diff --git a/web/messages/es/docs/backup-restore/index.json b/web/messages/es/docs/backup-restore/index.json index b8bcf9c0..7625af0c 100644 --- a/web/messages/es/docs/backup-restore/index.json +++ b/web/messages/es/docs/backup-restore/index.json @@ -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", diff --git a/web/messages/es/docs/backup-restore/restoring.json b/web/messages/es/docs/backup-restore/restoring.json index 7d22880d..44689048 100644 --- a/web/messages/es/docs/backup-restore/restoring.json +++ b/web/messages/es/docs/backup-restore/restoring.json @@ -92,7 +92,7 @@ "fullFlow": { "heading": "El flujo completo de restauración", "intro": "El pipeline completo desde la selección del archivo hasta un host restaurado y operativo. Cada etapa alimenta a la siguiente; el estado escrito por etapas anteriores lo consumen las posteriores.", - "diagram": "┌─────────────────────────────────────────────────────────────────┐\n│ Archivo → staging_root/ │\n│ manifest.json metadata/ rootfs/ │\n└──────────────────────────────┬──────────────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 1. hb_compat_check │\n │ · drift de hardware → RS_SKIP_PATHS │\n │ · dirección kernel → same / bk_newer / bk_older │\n │ · plan de remapeo NIC │\n │ · plan de hidratación (sólo bk_older) │\n │ · plan de rollback │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 2. Diálogo de confirmación │\n │ Muestra: rutas hot, rutas pending, drift, │\n │ hidratación, remapeo NIC, nota cross-kernel, │\n │ preview de reinstalaciones │\n └──────────────────────────────┬──────────────────────────┘\n │ (aceptado)\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 3. Aplicar rutas hot → EN VIVO │\n │ _rs_apply rsyncea cada ruta hot │\n │ desde staging_root/rootfs a / │\n │ (salta rutas en RS_SKIP_PATHS) │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 4. _rs_prepare_pending_restore │\n │ /var/lib/proxmenux/pending-restore/ │\n │ ├── apply-on-boot.list │\n │ ├── plan.env │\n │ ├── rs-skip-paths.txt │\n │ └── rootfs/ (rutas diferidas) │\n │ Habilita proxmenux-restore-onboot.service │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 5. _rs_run_complete_extras │\n │ packages.manual.list │\n │ → filtro cascade-safe │\n │ → apt-get install │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 6. REINICIO │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 7. apply_pending_restore.sh (arranque temprano) │\n │ Lee plan.env │\n │ Aplica apply-on-boot.list (filtrado) │\n │ Instala la unit systemd de postboot │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 8. apply_cluster_postboot.sh (~10 minutos) │\n │ Corre cuando pve-cluster está arriba │\n │ · Aplica entradas de /etc/pve a pmxcfs │\n │ · update-initramfs -u -k all │\n │ · update-grub / proxmox-boot-tool refresh │\n │ · component --auto-reinstall (nvidia, coral, ...) │\n │ · Comprobación de sanidad de arranque │\n │ · Notificación: \"Host restore finished\" │\n └─────────────────────────────────────────────────────────┘" + "diagram": "┌─────────────────────────────────────────────────────────────────┐\n│ Archivo → staging_root/ │\n│ manifest.json metadata/ rootfs/ │\n└──────────────────────────────┬──────────────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 1. hb_compat_check │\n │ · drift de hardware → RS_SKIP_PATHS │\n │ · dirección kernel → same / bk_newer / bk_older │\n │ · plan de remapeo NIC │\n │ · plan de hidratación (sólo bk_older) │\n │ · plan de rollback │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 2. Diálogo de confirmación │\n │ Muestra: rutas hot, rutas pending, drift, │\n │ hidratación, remapeo NIC, nota cross-kernel, │\n │ preview de reinstalaciones │\n └──────────────────────────────┬──────────────────────────┘\n │ (aceptado)\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 3. Aplicar rutas hot → EN VIVO │\n │ _rs_apply rsyncea cada ruta hot │\n │ desde staging_root/rootfs a / │\n │ (salta rutas en RS_SKIP_PATHS) │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 4. _rs_prepare_pending_restore │\n │ /var/lib/proxmenux/pending-restore/ │\n │ ├── apply-on-boot.list │\n │ ├── plan.env │\n │ ├── rs-skip-paths.txt │\n │ └── rootfs/ (rutas diferidas) │\n │ Habilita proxmenux-restore-onboot.service │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 4b. _rs_import_data_pools │\n │ Importa pools ZFS no-raíz cuyos discos │\n │ están todos presentes; retry con -f en │\n │ foreign hostid; skip con warning si faltan discos. │\n │ Log: /var/log/proxmenux/restore-datapools-*.log │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 5. _rs_run_complete_extras │\n │ packages.manual.list │\n │ → filtro cascade-safe │\n │ → apt-get install │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 6. REINICIO │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 7. apply_pending_restore.sh (arranque temprano) │\n │ Lee plan.env │\n │ Aplica apply-on-boot.list (filtrado) │\n │ Instala la unit systemd de postboot │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 8. apply_cluster_postboot.sh (~10 minutos) │\n │ Corre cuando pve-cluster está arriba │\n │ · Aplica entradas de /etc/pve a pmxcfs │\n │ · update-initramfs -u -k all │\n │ · update-grub / proxmox-boot-tool refresh │\n │ · component --auto-reinstall (nvidia, coral, ...) │\n │ · Comprobación de sanidad de arranque │\n │ · Notificación: \"Host restore finished\" │\n └─────────────────────────────────────────────────────────┘" }, "pendingMachinery": { "heading": "La maquinaria del pending-restore", @@ -123,7 +123,7 @@ "tasks": [ { "task": "Aplicar /etc/pve", - "detail": "/etc/pve es un montaje FUSE de pmxcfs en vivo — no se puede escribir en él en el arranque temprano. El dispatcher copia ficheros desde el rootfs pendiente al /etc/pve vivo una vez el filesystem del clúster está arriba, fichero por fichero, sin reiniciar pve-cluster." + "detail": "/etc/pve es un montaje FUSE de pmxcfs en vivo — no se puede escribir en él en el arranque temprano. El dispatcher copia ficheros desde el rootfs pendiente al /etc/pve vivo una vez el filesystem del clúster está arriba, fichero por fichero, sin reiniciar pve-cluster. La base /var/lib/pve-cluster/config.db del backup (o el config.db.raw-fallback promocionado) se materializa con el patrón systemctl stop pve-cluster → copia atómica del fichero → systemctl start pve-cluster, de modo que pmxcfs relee el estado restaurado al volver a arrancar." }, { "task": "Regenerar initramfs y bootloader", @@ -159,6 +159,10 @@ "name": "Lista de componentes", "detail": "Una fila por cada driver reinstalado (NVIDIA, Intel GPU tools, AMD tools, Coral TPU) con estado installing / ok / failed y un enlace al log por componente en /var/log/proxmenux/component-*.log." }, + { + "name": "Importación de pools de datos", + "detail": "Bloque data_pools_import con cinco filas coloreadas — Importados, Forzados (importados con -f por hostid ajeno), Omitidos parcial (pools con solo parte de sus discos presente), Omitidos ausentes (sin ningún disco presente) y Fallidos. El estado se persiste en /var/lib/proxmenux/restore-state.json y el log crudo por ejecución vive en /var/log/proxmenux/restore-datapools-<timestamp>.log." + }, { "name": "Avisos de arranque", "detail": "El dispatcher verifica si falta /lib/modules/<kernel>, si no hay ESP configurada y si hay un symlink /vmlinuz colgado. Cualquier aviso aparece aquí en un banner coloreado." @@ -248,6 +252,10 @@ { "log": "/var/log/proxmenux/component--YYYYMMDD_HHMMSS.log", "detail": "Un log por instalador de componente (nvidia, coral, etc.) que corrió en la pasada post-arranque. Útil cuando la reinstalación de un componente concreto ha fallado y el usuario necesita la salida de la propia herramienta." + }, + { + "log": "/var/log/proxmenux/restore-datapools-YYYYMMDD_HHMMSS.log", + "detail": "Lo escribe _rs_import_data_pools. Contiene: enumeración de pools no-raíz del manifiesto, resultado por pool (importado limpio, importado con -f por hostid ajeno, omitido por disco faltante, o fallado) y la salida cruda de cada llamada a zpool import." } ] }, diff --git a/web/messages/es/docs/help-info/update-commands.json b/web/messages/es/docs/help-info/update-commands.json index 8a59c2fd..a1069611 100644 --- a/web/messages/es/docs/help-info/update-commands.json +++ b/web/messages/es/docs/help-info/update-commands.json @@ -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", diff --git a/web/messages/es/docs/installation.json b/web/messages/es/docs/installation.json index 46d552f4..96cc7076 100644 --- a/web/messages/es/docs/installation.json +++ b/web/messages/es/docs/installation.json @@ -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 menu 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 menu 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 menu: 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 (bash -c \"$(wget -qLO - https://raw.githubusercontent.com/MacRimi/ProxMenux/main/install_proxmenux.sh)\" para el canal estable, o la URL equivalente de install_proxmenux_beta.sh para beta) para lanzarlo desde SSH o consola." }, "uninstall": { "heading": "Desinstalar", diff --git a/web/messages/es/docs/monitor/dashboard/terminal.json b/web/messages/es/docs/monitor/dashboard/terminal.json index fd3d92df..fad47110 100644 --- a/web/messages/es/docs/monitor/dashboard/terminal.json +++ b/web/messages/es/docs/monitor/dashboard/terminal.json @@ -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 flask_terminal_routes, la canaliza por un WebSocket hacia xterm.js en el navegador y se ejecuta como root (el usuario de la unidad systemd). Cualquier cosa que puedas hacer en ssh root@<host> funciona aquí — incluidos vim, tmux, herramientas ncurses y las CLIs de Proxmox (qm, pct, pvesh, pvecm)." + "body": "El terminal asigna una PTY del lado del servidor a través de flask_terminal_routes, la canaliza por un WebSocket hacia xterm.js en el navegador y se ejecuta como root (el usuario de la unidad systemd). Cualquier cosa que puedas hacer en ssh root@<host> funciona aquí — incluidos vim, tmux, herramientas ncurses y las CLIs de Proxmox (qm, pct, pvesh, pvecm). Cada PTY lanzada desde esta pestaña exporta la variable de entorno PROXMENUX_TERMINAL=monitor, que hereda todo proceso bash hijo — incluida una llamada posterior a menu — 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 (1 / 4 terminals), + New, Search, Clear y Close. 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 propio host Proxmox — el mismo login que obtendrías por SSH. Cada pestaña abre un terminal del host completamente nuevo.", "body2": "Para llegar a un contenedor LXC desde el navegador, usa el botón Console dedicado en cada tarjeta de CT en ejecución de la pestaña VMs y LXCs. Abre una modal que ejecuta pct enter <vmid> 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 menu 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 check_updates_stable, check_updates_beta y apply_release_channel leen PROXMENUX_TERMINAL 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 — bash -c \"$(wget -qLO - https://raw.githubusercontent.com/MacRimi/ProxMenux/main/install_proxmenux.sh)\" para el canal estable, y la URL equivalente de install_proxmenux_beta.sh 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\":", diff --git a/web/messages/es/docs/monitor/health-monitor.json b/web/messages/es/docs/monitor/health-monitor.json index 57bf0ed0..7d7e2690 100644 --- a/web/messages/es/docs/monitor/health-monitor.json +++ b/web/messages/es/docs/monitor/health-monitor.json @@ -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 Update Now que ejecuta el flujo completo de actualización de ProxMenux en línea; al cerrar vuelve a golpear el endpoint de health con ?refresh=1 para que el badge refleje el nuevo estado sin esperar al siguiente ciclo de 5 minutos." }, { "category": "Seguridad y Certificados", @@ -151,11 +151,14 @@ "Dismissed — 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 Active." ], "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 Actualizaciones del sistema reporta una o más actualizaciones pendientes, la cabecera de Overview renderiza un botón Update Now al lado de la pildora de salud. El botón abre una modal que ejecuta el mismo flujo del lado del host que ProxMenux → Settings post-install Proxmox → Proxmox System Update: higiene de repos, apt full-upgrade vía el worker seguro update-pve-safe.sh, 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 GET /api/health/details?refresh=1, 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 System Updates: actualizaciones pendientes disponibles 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 System Updates: actualizaciones pendientes disponibles permanece cierto hasta que parches el host, y no quieres una notificación cada cinco minutos durante una semana. El botón Update Now 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": "Acción Dismiss por evento en el modal. El botón Dismiss abre un dropdown con tres opciones — 24 horas, 7 días o Permanently — 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 POST /api/health/acknowledge con el error_key y el suppression_hours escogido (-1 para permanente). El evento se mueve a la lista Dismissed con un acknowledged_at 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 Permanent en la lista Dismissed y no se re-disparan.", diff --git a/web/messages/es/docs/post-install/automated.json b/web/messages/es/docs/post-install/automated.json index 3a220f49..926893c2 100644 --- a/web/messages/es/docs/post-install/automated.json +++ b/web/messages/es/docs/post-install/automated.json @@ -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-.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" } diff --git a/web/messages/es/docs/post-install/network.json b/web/messages/es/docs/post-install/network.json index e42dbc70..96ef4442 100644 --- a/web/messages/es/docs/post-install/network.json +++ b/web/messages/es/docs/post-install/network.json @@ -54,6 +54,8 @@ } ], "sourceOutro": "También añade source /etc/network/interfaces.d/* a /etc/network/interfaces 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 /usr/local/sbin/proxmenux-fwbr-tune que aplica rp_filter=0 y log_martians=0 a las interfaces fwbr* / fwln* / fwpr* / tap* que Proxmox crea alrededor de VMs y contenedores. El helper lo lanza la unit oneshot proxmenux-fwbr-tune.service al arranque y la regla /etc/udev/rules.d/99-proxmenux-fwbr-tune.rules en cada evento net add 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 distinta. 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. rp_filter=2 (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 hotplug 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 /etc/systemd/network/10-<iface>.link:", - "writtenOutro": "Cualquier archivo .link preexistente en ese directorio se copia a /etc/systemd/network/backup-<timestamp>/ antes de tocar nada.", + "writtenIntro": "Un archivo por NIC física, en /etc/systemd/network/10-proxmenux-<iface>.link. Cada archivo comienza con la cabecera # Managed by ProxMenux — do not edit, marca que permite a ProxMenux distinguir sus propios archivos de los .link provistos por el usuario o por otros paquetes.", + "writtenOutro": "Los archivos .link 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 MACAddress= de cada 10-proxmenux-*.link y elimina los cuya MAC ya no aparece en /sys/class/net/. Los archivos escritos por una versión antigua de ProxMenux en el formato 10-<iface>.link se migran automáticamente en la primera ejecución.", "pveTitle": "PVE 9 vs PVE 8", "pveBody": "En Proxmox VE 9 (systemd-networkd nativo), el script recarga las reglas udev tras escribir los archivos .link para que las nuevas NICs hotplug cojan el nombre correcto sin reiniciar. En PVE 8 (ifupdown2), 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 /etc/network/interfaces que referencia nombres de NIC generados por el esquema por defecto del kernel, fijar los nombres de hoy 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": "Uninstall Optimizations borra cada archivo .link de /etc/systemd/network/, 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": "Uninstall Optimizations solo borra los archivos 10-proxmenux-*.link que llevan la cabecera de ProxMenux en /etc/systemd/network/. Cualquier archivo .link 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", diff --git a/web/messages/es/docs/post-install/optional.json b/web/messages/es/docs/post-install/optional.json index ce5ea577..bf46e7a1 100644 --- a/web/messages/es/docs/post-install/optional.json +++ b/web/messages/es/docs/post-install/optional.json @@ -145,10 +145,11 @@ "doesLabel": "Qué hace ProxMenux:", "doesItems": [ "Detecta si el disco raíz es SSD/NVMe leyendo /sys/block/<dev>/queue/rotational. En un disco rotacional el flujo Automatizado pregunta antes de instalar.", - "Clona el repositorio oficial (azlux/log2ram) en /tmp/log2ram y ejecuta su install.sh, luego habilita la unit systemd log2ram.", + "Clona el repositorio oficial (azlux/log2ram) en /tmp/log2ram y aplica un patch a su install.sh, sustituyendo rsync -aAXv por rsync -aXv --no-acls. Ese ajuste evita el fallo set_acl: Operation not supported con exit 23 que el script vanilla produce al mover /var/log en sistemas de archivos sin soporte de ACLs. La unit systemd log2ram se habilita después de la instalación.", "Dimensiona la ramdisk según la RAM del host: ≤ 8 GB → 128M, ≤ 16 GB → 256M, > 16 GB → 512M. Escribe el valor en SIZE= de /etc/log2ram.conf.", "Programa una sincronización periódica a disco vía /etc/cron.d/log2ram: cada 1h / 3h / 6h según el mismo tramo de RAM.", - "Instala un guardián de auto-sync en /usr/local/bin/log2ram-check.sh, conectado a /etc/cron.d/log2ram-auto-sync para ejecutarse cada 10 minutos. Cuando /var/log alcanza el 80% del tamaño de la ramdisk el guardián compacta journald; al 92% además trunca pveproxy access/error y pveam.log antes de sincronizar — el comando log2ram write por sí solo copia tmpfs a disco pero NO reduce la tmpfs, así que este guardián evita que PVE caiga con No space left on device cuando los logs crecen sin control.", + "Instala un guardián de auto-sync en /usr/local/bin/log2ram-check.sh, conectado a /etc/cron.d/log2ram-auto-sync para ejecutarse cada 10 minutos. Cuando /var/log alcanza el 80% del tamaño de la ramdisk el guardián compacta journald; al 92% primero ejecuta logrotate -f /etc/logrotate.d/proxmox-backup-api 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 pveproxy access/error y pveam.log antes de sincronizar. El comando log2ram write por sí solo copia tmpfs a disco pero NO reduce la tmpfs, así que este guardián evita que PVE caiga con No space left on device cuando los logs crecen sin control.", + "Detecta si proxmox-backup-server corre como servicio en el host. Cuando está presente, deposita /etc/logrotate.d/proxmox-backup-api (rotación de 20 MB × 3) y /etc/cron.hourly/proxmox-backup-logrotate, 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 (SystemMaxUse, RuntimeMaxUse) para que quepan en la ramdisk y así una ráfaga puntual no la llene.", "Se registra en installed_tools.json para poder revertirlo desde Uninstall Optimizations." ], diff --git a/web/messages/es/docs/post-install/storage.json b/web/messages/es/docs/post-install/storage.json index 25944e0e..b08d24a8 100644 --- a/web/messages/es/docs/post-install/storage.json +++ b/web/messages/es/docs/post-install/storage.json @@ -18,30 +18,26 @@ "intro": "El Adaptive Replacement Cache (ARC) 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 /etc/modprobe.d/99-zfsarc.conf 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 /etc/modprobe.d/99-zfsarc.conf contiene una única directiva — options zfs zfs_arc_max=…. El resto de parámetros del módulo (zfs_arc_min, prefetch/write throttle de L2ARC, timeout de TXG) se dejan en los valores por defecto de OpenZFS. Tras escribir el archivo, ProxMenux ejecuta update-initramfs -u -k all y, cuando corresponde, proxmox-boot-tool refresh, 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 zfs. No 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 zfs. Para que el cap se aplique en hosts ZFS-on-root, ProxMenux regenera el initramfs con update-initramfs -u -k all y, cuando corresponde, refresca el cargador de arranque con proxmox-boot-tool refresh. 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 zfs 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 zpool list 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": { diff --git a/web/messages/es/docs/post-install/uninstall.json b/web/messages/es/docs/post-install/uninstall.json index 2711a8e9..44466634 100644 --- a/web/messages/es/docs/post-install/uninstall.json +++ b/web/messages/es/docs/post-install/uninstall.json @@ -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 Subscription Banner Removal reinstala pve-manager, proxmox-widget-toolkit, libjs-extjs y libpve-http-server-perl con --force-confnew. 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 .link (Persistent Interface Names) y revertir IOMMU/VFIO 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 10-proxmenux-*.link propios de ProxMenux (Persistent Interface Names) y revertir IOMMU/VFIO 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 Log2RAM. Nada más se toca, y el registro se actualiza en consecuencia." }, diff --git a/web/messages/es/docs/settings/beta-program.json b/web/messages/es/docs/settings/beta-program.json index 5b31d15b..1b5438d3 100644 --- a/web/messages/es/docs/settings/beta-program.json +++ b/web/messages/es/docs/settings/beta-program.json @@ -53,7 +53,8 @@ "Tu configuración existente, login de ProxMenux Monitor (auth.json), 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 auth.json, 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": { diff --git a/web/messages/es/docs/utils/system-update.json b/web/messages/es/docs/utils/system-update.json index c9d98a9a..0af6a1f2 100644 --- a/web/messages/es/docs/utils/system-update.json +++ b/web/messages/es/docs/utils/system-update.json @@ -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 (update-pve-safe.sh) 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 Update Now 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 exactamente 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 scripts/utilities/proxmox_update.sh y a los scripts worker por versión — nada implícito, cada paso está en el código:", + "intro": "Esta opción ejecuta exactamente 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 scripts/utilities/proxmox_update.sh y al único worker seguro scripts/global/update-pve-safe.sh — nada implícito, cada paso está en el código:", "items": [ - "Detecta la versión mayor de PVE (pveversion | grep -oP ''pve-manager/\\K[0-9]+'') y despacha a update-pve8.sh o update-pve9_2.sh para que se usen el codename y las URLs de repo correctas.", - "Limpia los repositorios 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.", - "Ejecuta el upgrade no interactivamente con DEBIAN_FRONTEND=noninteractive y --force-confdef --force-confold — 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.", - "Instala paquetes esenciales de Proxmox si falta alguno (zfsutils-linux, proxmox-backup-restore-image, chrony).", + "Detecta la versión mayor de PVE desde dentro del worker (pveversion | grep -oP ''pve-manager/\\K[0-9]+'') 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.", + "Limpia los repositorios de forma conservadora. ensure_repositories 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. cleanup_duplicate_repos elimina después duplicados exactos por URL + Suite + Component solo contra proxmox.sources / debian.sources; los archivos propios del usuario (enterprise, Ceph, mirrors NTP alternativos, entradas custom de download.proxmox.com/* o pve-*.list escritos a mano) no se tocan, y cada archivo se respalda antes de editarse.", + "Ejecuta el upgrade no interactivamente con DEBIAN_FRONTEND=noninteractive y --force-confdef --force-confold — 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.", + "No fuerza utilidades opcionales. El worker seguro no empuja zfsutils-linux, chrony, ifupdown2 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.", "Comprobación de sanity de metadatos LVM contra cabeceras PV sueltas de discos en passthrough (solo aviso, sin arreglo automático).", "Limpia después: apt-get autoremove -y + apt-get autoclean -y.", - "Preguntar antes de reiniciar solo si el kernel realmente cambió (presencia de /var/run/reboot-required o linux-image en el log del upgrade)." + "Recompilación DKMS antes del prompt de reinicio. Cuando el upgrade dejó staged un kernel nuevo, el wrapper llama a pmx_rebuild_dkms_after_kernel 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 /var/run/reboot-required cuando needrestart está presente, y como fallback una comparación por dpkg-query entre el kernel en ejecución y el paquete proxmox-kernel-*-pve-signed / pve-kernel-*-pve más nuevo instalado cuando no lo está — señal que sobrevive a los muchos hosts Proxmox que se entregan sin needrestart." ] }, "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 (scripts/global/update-pve8.sh para PVE 8 y scripts/global/update-pve9_2.sh para PVE 9) siguen el mismo esquema, con URLs de repo y nombres de paquete propios de cada versión:", + "intro": "Un único worker (scripts/global/update-pve-safe.sh) maneja tanto PVE 8 como PVE 9. Detecta la versión mayor internamente y usa el codename propio de cada versión (bookworm o trixie) para sus fuentes base. Las etapas son:", "items": [ - "Higiene de repos. Elimina entradas duplicadas de /etc/apt/sources.list y /etc/apt/sources.list.d/. Comenta el repo enterprise si el host no tiene suscripción y escribe / habilita la fuente sin suscripción.", - "Apt update + full-upgrade. Trae las últimas listas de paquetes y aplica todas las actualizaciones disponibles para la versión mayor actual, ejecutándose con DEBIAN_FRONTEND=noninteractive y --force-confdef --force-confold para que cualquier archivo de configuración que personalizaste mantenga su contenido actual cuando upstream también lo cambió.", - "Comprobación de paquetes esenciales. Instala zfsutils-linux, chrony, ifupdown2 y unos pocos más si el host no los tiene.", - "Comprobación de sanity de LVM / almacenamiento. Repara cabeceras PV faltantes si se detectan.", - "Eliminación de paquetes conflictivos. Quita paquetes conocidos por chocar en Proxmox (p. ej. algunos demonios de sincronización horaria que pelean con chrony)." + "Comprobaciones previas. Verifica al menos ~1 GB libres en /var/cache/apt/archives y hace ping a download.proxmox.com. 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.", + "Bootstrap de repos. ensure_repositories escribe las fuentes base Proxmox / Debian solo cuando faltan (un host fresco o limpiado a mano); en un host configurado no hace nada.", + "Apt update con auto-recuperación de GPG. Ante un NO_PUBKEY 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.", + "Limpieza conservadora de duplicados. cleanup_duplicate_repos elimina solo coincidencias exactas por URL + Suite + Component contra proxmox.sources / debian.sources. Los archivos propios del usuario — enterprise, Ceph, mirrors NTP alternativos, entradas custom de download.proxmox.com/*, pve-*.list escritos a mano — se dejan intactos. Cada archivo se respalda antes de modificarse.", + "Actualizaciones pendientes + conteo de seguridad. 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.", + "Diálogo de confirmación. El wrapper pide un sí explícito antes de tocar apt.", + "apt full-upgrade. Se ejecuta con DEBIAN_FRONTEND=noninteractive y --force-confdef --force-confold 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.", + "Comprobación de sanity LVM. lvm_repair_check refresca los metadatos de VG cuando discos en passthrough a VMs guest (DSM, TrueNAS, appliances de almacenamiento) vuelven con cabeceras PV antiguas.", + "Recompilación DKMS de los drivers gestionados por ProxMenux. Cuando el upgrade dejó staged un kernel nuevo, pmx_rebuild_dkms_after_kernel lee components_status.json para los drivers que ProxMenux instaló (actualmente nvidia_driver → módulo nvidia, coral_driver → módulo gasket), instala los headers de kernel correspondientes (proxmox-headers-<newkver> cuando está disponible, si no pve-headers-<newkver>) y ejecuta dkms autoinstall -k <newkver>. Si dkms status no muestra los módulos construidos contra el kernel nuevo, el worker cae a reejecutar cada instalador con --auto-reinstall. Cualquier fallo se registra pero no aborta la actualización — llegas al prompt de reinicio en todos los casos.", + "Post-limpieza. apt-get autoremove + apt-get autoclean antes de devolver el control al wrapper." ] }, "post": { @@ -91,14 +96,14 @@ "body": "Correr en un kernel antiguo tras actualizar linux-image-* 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 apt-get update. 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 apt-get update. El worker seguro deliberadamente no 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) /etc/apt/sources.list.d/pve-enterprise.list (o el equivalente deb822)", - "Escribe /etc/apt/sources.list.d/pve-no-subscription.list (o el deb822 proxmox.sources para PVE 9) con el codename correspondiente (bookworm para PVE 8, trixie para PVE 9)", - "Vuelve a ejecutar apt-get update" + "En un host bare sin fuentes base de Proxmox / Debian, ensure_repositories escribe la fuente sin suscripción en formato deb822 (proxmox.sources) con el codename correspondiente a la versión mayor detectada (bookworm para PVE 8, trixie para PVE 9) y las fuentes Debian correspondientes.", + "En un host configurado, ensure_repositories es un no-op — lo que el usuario haya elegido (sin suscripción, enterprise o una mezcla) se preserva.", + "Los archivos enterprise pve-enterprise.sources / ceph.sources 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 pve-enterprise.sources 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 dkms status. Si falta algo: dkms autoinstall." + "body": "Los drivers que ProxMenux instaló vía DKMS (actualmente nvidia_driver y coral_driver) se recompilan automáticamente al final del upgrade contra la versión del kernel entrante, usando dkms autoinstall -k <newkver> y, si hace falta, un fallback a cada instalador con --auto-reinstall. Confirma con dkms status. Los módulos fuera del árbol de terceros que no están en el registro components_status.json de ProxMenux (drivers de NIC propios, paquetes DKMS instalados a mano, …) siguen necesitando un dkms autoinstall 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",