mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-07-26 18:38:30 +00:00
Update 1.2.4
This commit is contained in:
@@ -1,21 +1,21 @@
|
||||
"use client"
|
||||
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { Plus, Share, X } from "lucide-react"
|
||||
import { Download, Plus, Share, X } from "lucide-react"
|
||||
|
||||
// ==========================================================
|
||||
// PwaInstallPrompt
|
||||
// ==========================================================
|
||||
// Bottom-sheet shown on mobile when the Monitor is opened in
|
||||
// a browser (not launched as an installed PWA). Two variants:
|
||||
// iOS Safari → manual 3-step instructions
|
||||
// Android → generic "browser menu → Add to Home Screen"
|
||||
// iOS Safari → manual 3-step instructions (no browser API)
|
||||
// Android → native install prompt when the browser
|
||||
// fires `beforeinstallprompt` (Chromium 89+
|
||||
// delivers it based on manifest validity alone,
|
||||
// no Service Worker required), with the manual
|
||||
// "browser menu → Add to Home Screen" steps
|
||||
// always shown below as a fallback.
|
||||
//
|
||||
// No `beforeinstallprompt` handling: the Monitor no longer
|
||||
// registers a Service Worker (see `pwa-register.tsx`), so
|
||||
// Chromium never fires it. Installation is always manual.
|
||||
//
|
||||
// Never shown on desktop, or when already running standalone.
|
||||
// Dismissal options:
|
||||
// "Not now" → temporary, hidden for 30 days
|
||||
// "Don't show again" → permanent (no expiry)
|
||||
@@ -27,6 +27,13 @@ const DISMISSED_FOREVER_KEY = "proxmenux-install-dismissed"
|
||||
const DISMISSED_UNTIL_KEY = "proxmenux-install-dismissed-until"
|
||||
const NOT_NOW_DAYS = 30
|
||||
|
||||
// `BeforeInstallPromptEvent` isn't in the TS DOM lib yet.
|
||||
interface BeforeInstallPromptEvent extends Event {
|
||||
readonly platforms: string[]
|
||||
readonly userChoice: Promise<{ outcome: "accepted" | "dismissed"; platform: string }>
|
||||
prompt(): Promise<void>
|
||||
}
|
||||
|
||||
function isMobileDevice(): boolean {
|
||||
if (typeof window === "undefined") return false
|
||||
// Prefer feature detection (coarse pointer + touch) over UA sniffing,
|
||||
@@ -57,6 +64,7 @@ function isIOS(): boolean {
|
||||
export function PwaInstallPrompt() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [platform, setPlatform] = useState<"ios" | "android" | null>(null)
|
||||
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return
|
||||
@@ -79,6 +87,28 @@ export function PwaInstallPrompt() {
|
||||
setOpen(true)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return
|
||||
const onBeforeInstall = (e: Event) => {
|
||||
// Suppress the browser's own mini-infobar so the Install button
|
||||
// in the bottom sheet is the primary path.
|
||||
e.preventDefault()
|
||||
setDeferredPrompt(e as BeforeInstallPromptEvent)
|
||||
}
|
||||
const onInstalled = () => {
|
||||
// The browser confirms the install landed on the home screen —
|
||||
// close the sheet and drop the deferred event.
|
||||
setDeferredPrompt(null)
|
||||
setOpen(false)
|
||||
}
|
||||
window.addEventListener("beforeinstallprompt", onBeforeInstall)
|
||||
window.addEventListener("appinstalled", onInstalled)
|
||||
return () => {
|
||||
window.removeEventListener("beforeinstallprompt", onBeforeInstall)
|
||||
window.removeEventListener("appinstalled", onInstalled)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleNotNow = useCallback(() => {
|
||||
try {
|
||||
const until = Date.now() + NOT_NOW_DAYS * 24 * 60 * 60 * 1000
|
||||
@@ -107,6 +137,25 @@ export function PwaInstallPrompt() {
|
||||
setOpen(false)
|
||||
}, [])
|
||||
|
||||
const handleInstall = useCallback(async () => {
|
||||
if (!deferredPrompt) return
|
||||
try {
|
||||
await deferredPrompt.prompt()
|
||||
const { outcome } = await deferredPrompt.userChoice
|
||||
// A `beforeinstallprompt` event can only be used once; drop the
|
||||
// reference either way so the button hides.
|
||||
setDeferredPrompt(null)
|
||||
if (outcome === "accepted") {
|
||||
setOpen(false)
|
||||
}
|
||||
} catch {
|
||||
// Browser refused to run the prompt (already handled, race with
|
||||
// appinstalled, etc.) — leave the sheet open so the operator can
|
||||
// fall back to the manual steps rendered below.
|
||||
setDeferredPrompt(null)
|
||||
}
|
||||
}, [deferredPrompt])
|
||||
|
||||
if (!open || !platform) return null
|
||||
|
||||
return (
|
||||
@@ -187,11 +236,33 @@ export function PwaInstallPrompt() {
|
||||
</li>
|
||||
</ol>
|
||||
) : (
|
||||
<div className="mb-4 rounded-lg border border-border bg-muted/50 px-3.5 py-3 text-[13px] leading-relaxed text-muted-foreground">
|
||||
Open the browser menu <b className="text-foreground">⋮</b> →{" "}
|
||||
<b className="text-foreground">Add to Home Screen</b> → confirm by tapping{" "}
|
||||
<b className="text-foreground">Install</b>.
|
||||
</div>
|
||||
<>
|
||||
{deferredPrompt && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleInstall}
|
||||
className="mb-3 flex w-full items-center justify-center gap-2 rounded-xl bg-primary px-4 py-3 text-[14px] font-semibold text-primary-foreground shadow-sm hover:opacity-90 active:opacity-80 transition-opacity"
|
||||
>
|
||||
<Download className="h-4 w-4" aria-hidden="true" />
|
||||
Install
|
||||
</button>
|
||||
)}
|
||||
<div className="mb-4 rounded-lg border border-border bg-muted/50 px-3.5 py-3 text-[13px] leading-relaxed text-muted-foreground">
|
||||
{deferredPrompt ? (
|
||||
<>
|
||||
Or install manually: browser menu <b className="text-foreground">⋮</b> →{" "}
|
||||
<b className="text-foreground">Add to Home Screen</b> → confirm by tapping{" "}
|
||||
<b className="text-foreground">Install</b>.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Open the browser menu <b className="text-foreground">⋮</b> →{" "}
|
||||
<b className="text-foreground">Add to Home Screen</b> → confirm by tapping{" "}
|
||||
<b className="text-foreground">Install</b>.
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="mt-1 flex flex-col gap-1 border-t border-border pt-3">
|
||||
|
||||
@@ -2,13 +2,9 @@
|
||||
|
||||
import { useEffect } from "react"
|
||||
|
||||
// Unregisters any Service Worker registered against this origin on
|
||||
// mount. Having a SW active on the Monitor origin caused a mobile
|
||||
// dashboard-freeze bug over HTTPS + reverse proxy (setInterval polls
|
||||
// stopped firing real fetches under battery throttling). Confirmed
|
||||
// as the sole cause by the affected reporter on Brave and Firefox
|
||||
// Android. `sw.js` is kept in the tree so PWA offline support can
|
||||
// be revisited later without archaeology.
|
||||
// Unregister any Service Worker on this origin at mount. A SW here
|
||||
// interacts badly with mobile battery throttling behind reverse
|
||||
// proxies. `sw.js` is kept for a future PWA-offline revisit.
|
||||
export function PwaRegister() {
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return
|
||||
@@ -19,10 +15,7 @@ export function PwaRegister() {
|
||||
if (regs.length === 0) return
|
||||
return Promise.all(regs.map((r) => r.unregister()))
|
||||
})
|
||||
.catch(() => {
|
||||
// Best-effort — private browsing or a locked-down browser
|
||||
// may reject the API; nothing to do.
|
||||
})
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -405,14 +405,8 @@ const RestoreDetailModal: React.FC<{
|
||||
)
|
||||
}
|
||||
|
||||
// ── Data-pools auto-import block ──────────────────────────────
|
||||
// Rendered inside the RestoreDetailModal (and the compact header
|
||||
// badge on the card) so the operator sees which ZFS data pools
|
||||
// were imported automatically during the restore, which ones ZFS
|
||||
// took with -f because the on-disk hostid didn't match the fresh
|
||||
// install, and which ones were skipped and need manual attention.
|
||||
// The section is populated by backup_host.sh:_rs_persist_datapool_import
|
||||
// and survives the ScriptTerminalModal being closed.
|
||||
// Rendered inside RestoreDetailModal — one row per outcome category
|
||||
// (imported / forced / partial skip / missing skip / failed).
|
||||
const DataPoolsBlock: React.FC<{ section: DataPoolsImport }> = ({ section }) => {
|
||||
const total =
|
||||
section.ok.length +
|
||||
|
||||
@@ -262,6 +262,10 @@ def terminal_websocket(ws):
|
||||
_term_env.setdefault('COLORTERM', 'truecolor')
|
||||
_term_env.setdefault('LANG', 'C.UTF-8')
|
||||
_term_env.setdefault('LC_ALL', 'C.UTF-8')
|
||||
# Inherited by every child of this shell (including `menu`), so the
|
||||
# update path can tell it's running inside a WebSocket-backed session
|
||||
# that would be cut mid-install if the Monitor service restarted.
|
||||
_term_env['PROXMENUX_TERMINAL'] = 'monitor'
|
||||
_term_env.pop('PS1', None)
|
||||
_home = _term_env.get('HOME') or os.path.expanduser('~') or '/root'
|
||||
|
||||
|
||||
104
CHANGELOG.md
104
CHANGELOG.md
@@ -3,7 +3,7 @@
|
||||
|
||||
### New version ProxMenux v1.2.4
|
||||
|
||||
This release adds two in-dashboard improvements — a one-click Proxmox update trigger from the Health Monitor and a mobile PWA install prompt — and closes seven notification content/delivery fixes, the mobile dashboard freeze over HTTPS + reverse proxy that slipped into 1.2.3, and two Health panel bugs reported within hours of that release.
|
||||
This release adds two in-dashboard improvements — a one-click Proxmox update trigger from the Health Monitor and a mobile PWA install prompt — extends the Backups restore flow with automatic ZFS data-pool import, sharpens Log2RAM behaviour on hosts running Proxmox Backup Server as a service, hardens firewall bridge sysctl tuning across VM lifecycle events, narrows the ZFS ARC optimization to its own scope, makes persistent NIC naming idempotent across reruns, rebuilds DKMS drivers automatically when a new kernel is staged, keeps the Monitor terminal session intact when a ProxMenux update is available, and reinforces five notification templates plus three Health panel checks.
|
||||
|
||||
---
|
||||
|
||||
@@ -20,49 +20,113 @@ This release adds two in-dashboard improvements — a one-click Proxmox update t
|
||||
|
||||
## 📱 In-app Install prompt for mobile
|
||||
|
||||
- First-time visitors on **Android** (Chrome / Brave) and **iOS Safari** now see a bottom-sheet with clear instructions for adding the Monitor as a PWA to their home screen.
|
||||
- First-time visitors on **Android** (Chrome / Brave / Edge / Samsung Internet) and **iOS Safari** now see a bottom-sheet with clear instructions for adding the Monitor as a PWA to their home screen.
|
||||
- On Android, when the browser deems the Monitor installable and fires `beforeinstallprompt` (Chromium 89+ delivers it on manifest validity alone, no Service Worker required), an **Install** button in the sheet drives the native install flow with a single tap; the browser's own in-page install banner is suppressed so the sheet is the primary path. The manual "browser menu → Add to Home Screen" steps are still shown as a fallback for browsers that don't fire the event (Firefox Android) or cases where the operator prefers the manual path.
|
||||
- iOS Safari keeps the manual 3-step instructions — no equivalent JS API is available on iOS.
|
||||
- The sheet closes automatically when the browser fires `appinstalled` — no lingering prompt after the install lands on the home screen.
|
||||
- Two dismissal levels: **Not now** (temporary, reappears in 30 days) and **Don't show again** (permanent, stored in `localStorage`).
|
||||
- Never shown on desktop, or once the Monitor is already running standalone.
|
||||
|
||||
---
|
||||
|
||||
## 🔔 Notification content — five rendering fixes
|
||||
## 🔔 Notification content — five rendering refinements
|
||||
|
||||
- **Backup destination now visible** — VM/CT backup emails and Telegram messages include the storage / PBS target in both the title and body row, so operators with multiple backup destinations can tell at a glance which one succeeded or failed.
|
||||
- **Migration bodies no longer render `migrated to node .`** — the actual target node is pulled from the PVE task log for `qmigrate` / `vzmigrate` events.
|
||||
- **Snapshot bodies no longer render `Snapshot "" created`** — the real snapshot name is pulled from the PVE task log for `qmsnapshot` / `vzsnapshot`.
|
||||
- **Generic `system_problem` notifications carry the real reason** — PVE messages that fall into the generic bucket include the original payload body, replacing the useless *"A system-level problem has been detected."*
|
||||
- **NVIDIA / Coral driver update emails show the new version** — the *New Version* row in the HTML body used to render empty due to a template/renderer field-name mismatch (`latest_version` vs `new_version`). Fixed.
|
||||
- **Backup destination in title and body** — VM/CT backup emails and Telegram messages carry the storage / PBS target, so operators with several backup destinations can tell at a glance which one produced the event.
|
||||
- **Migration bodies carry the real target node** — pulled from the PVE task log for `qmigrate` / `vzmigrate` events.
|
||||
- **Snapshot bodies carry the real snapshot name** — pulled from the PVE task log for `qmsnapshot` / `vzsnapshot` events.
|
||||
- **Generic `system_problem` notifications include the real reason** — PVE payload messages are surfaced as the notification body.
|
||||
- **NVIDIA / Coral driver update emails render the *New Version* row correctly** — the template placeholder is now aligned with the field the renderer reads.
|
||||
|
||||
---
|
||||
|
||||
## 🩹 Health panel fixes
|
||||
## 🩹 Health panel — three checks reinforced
|
||||
|
||||
- **Dismiss silently failed on storage alerts** — clicking Dismiss on a `storage_unavailable`, `mount_stale`, `mount_readonly`, `lxc_disk_low`, `lxc_mount_low`, `pve_storage_full` or `zfs_pool_full` error persisted the ack in the database but never invalidated the Monitor's cache because the event was tagged with category `general` instead of `storage`. The error stayed visible after dismiss. Both the category inference and the cache invalidation map fixed.
|
||||
- **VMs & Containers stuck on `UNKNOWN` with `'NoneType' object has no attribute 'get'`** (#255) — any persisted error with a `NULL` `details` column crashed the entire VM/CT check on every scan. The category stayed UNKNOWN permanently and Dismiss couldn't silence it because there was no specific `error_key` to acknowledge. Fixed by explicit `error.get('details') or {}` coalescing in both persistence loops.
|
||||
- **Duplicate `system_startup` notifications after every polling tick** — `_check_startup_aggregation` emitted the boot summary but never marked aggregation as done, so the next polling iteration re-emitted the same event over and over until the service restarted. On a host that stays up for hours after boot this produced up to one duplicate every polling interval. Now the aggregated flag is set right after the notification is queued.
|
||||
- **Dismiss now silences storage alerts.** The acknowledge flow includes `storage_unavailable`, `mount_stale`, `mount_readonly`, `lxc_disk_low`, `lxc_mount_low`, `pve_storage_full` and `zfs_pool_full` under the `storage` category, and the storage cache is invalidated on dismiss so the panel refreshes immediately.
|
||||
- **VMs & Containers check tolerates persisted errors with a NULL `details` column** (#255). `_check_vms_cts_with_persistence` coalesces missing `details` to an empty dict before reading nested keys, so a single sparse row no longer takes the whole VM/CT check offline.
|
||||
- **`system_startup` notification fires once per boot.** `_check_startup_aggregation` marks aggregation as done right after queuing the event, so the boot summary lands one time regardless of how many polling ticks fit inside the session.
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Mobile & webhook fixes
|
||||
## 🛠 Mobile & webhook
|
||||
|
||||
- **Mobile dashboard freeze over HTTPS + reverse proxy** — the Service Worker introduced in v1.2.3 (`sw.js`, needed for PWA installability) interacted with mobile browsers' battery-saving background throttling and caused polling fetches to stop firing after a page refresh. Dashboard values (CPU / RAM / temperature) froze at their last reading until a hard cache wipe. Fixed by auto-cleaning any registered Service Worker on load. PWA installability is now driven entirely by the new in-app install prompt above.
|
||||
- **Webhook auth extended to VPN / CGNAT interface IPs** — the internal webhook (`/api/notifications/webhook`) previously trusted only requests from `127.0.0.1` / `::1`. Users with a Tailscale, WireGuard, LAN or IPv6 FQDN pointing at the host saw PVE Test buttons return `401 missing_timestamp`. The webhook now treats any of the host's own interface IPs as trusted local-loopback, including IPv4-mapped-in-IPv6 form (`::ffff:100.x.x.x`) which Flask reports on dual-stack binds.
|
||||
- **Mobile dashboard polling stays live on HTTPS + reverse proxy setups.** `pwa-register.tsx` auto-unregisters any Service Worker on load so mobile-browser background throttling stops interfering with the polling fetches, and PWA installability is now driven by the new in-app install prompt above.
|
||||
- **Webhook auth trusts every host-local IP.** The internal webhook (`/api/notifications/webhook`) accepts requests from any interface IP the host owns (Tailscale, WireGuard, LAN, IPv6, plus IPv4-mapped-in-IPv6 form `::ffff:x.x.x.x` that Flask emits on dual-stack binds), so PVE Test buttons work through any of them.
|
||||
|
||||
---
|
||||
|
||||
## 🛡 Update flow — ProxMenux update prompt is now Monitor-terminal-aware
|
||||
|
||||
- **The Monitor's WebSocket terminal now exposes `PROXMENUX_TERMINAL=monitor`** in the environment of every shell it opens, and every child inherits it. This gives `menu` (and anything else that cares) a reliable, deterministic way to tell that the current session lives inside the Monitor process — a session that would be cut mid-install if the Monitor service was restarted.
|
||||
- **When `menu` starts and detects that a new ProxMenux version is available AND the session is running inside the Monitor terminal, the classic yes/no update prompt is replaced by an informational msgbox.** The msgbox names the new version and shows the one-line command to run the update from SSH or the Proxmox host console. Because the flow has already decided the in-terminal update path is unsafe (see the [msgbox-ack rule](memory/feedback_whiptail_msgbox_ack.md)), there's a single OK button — no yes/no that could trigger the destructive update by accident.
|
||||
- **After OK, `menu` continues normally**. The operator keeps using ProxMenux from the same terminal without restrictions; only the update itself is routed elsewhere. There is no lockdown and no forced action.
|
||||
- **SSH sessions, the Proxmox host console, and any environment where `PROXMENUX_TERMINAL` isn't `monitor` keep the previous yes/no prompt** and can update as always. The change only affects the case where updating in place would break the running session.
|
||||
- **Bootstrap note**: because `PROXMENUX_TERMINAL=monitor` is added by the AppImage this release ships, the guard only starts protecting sessions once the host is on 1.2.4 or newer. The very first update to 1.2.4, if triggered from the Monitor terminal, can still hit the old behaviour — from 1.2.4 forward the guard is in place.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Update flow — DKMS drivers rebuilt when a new kernel lands
|
||||
|
||||
- **After `apt full-upgrade` stages a kernel newer than the one currently running, `update-pve-safe.sh` now rebuilds ProxMenux-installed DKMS drivers against the new kernel.** The Update Now button in the Health Monitor and the `utilities/proxmox_update.sh` CLI both delegate to `update-pve-safe.sh`, so both routes gain the behaviour. The step reads `components_status.json`, cross-references the DKMS-managed components ProxMenux tracks (`nvidia_driver`, `coral_driver`), installs the matching kernel headers (`proxmox-headers-<newkver>` or `pve-headers-<newkver>`) if they aren't already present, and runs `dkms autoinstall -k <newkver>`. Then it verifies via `dkms status` that each expected module (`gasket` for Coral, `nvidia` for the NVIDIA driver) actually reached `installed` state for the new kernel — if any module didn't, it falls back to each installer's `--auto-reinstall` path.
|
||||
- **A whiptail msgbox announces the rebuild before it runs.** Single OK button — no yes/no. Names the incoming kernel version and lists the DKMS components that are going to be rebuilt, so the operator sees exactly what's about to happen. Because leaving DKMS drivers unbuilt would leave the system with a working kernel but non-functional TPU / GPU at boot, this is transparency, not a decision — pressing OK acknowledges the follow-up work and the flow proceeds. Non-interactive invocations (cron, headless batch, missing whiptail) skip the msgbox and log the same information.
|
||||
- **Only components already registered as `installed` in `components_status.json` are considered.** A host with no ProxMenux-managed DKMS drivers sees no msgbox and no rebuild step. Hosts that never ran the Coral or NVIDIA installer are unaffected.
|
||||
- **Failure to rebuild does not abort the update.** If a DKMS module can't be rebuilt against the new kernel (upstream API break, missing dependency), the update flow completes normally, the specific components that failed are named in the summary, and the operator can re-run their installer manually after reboot. The step is best-effort by design — a kernel/driver mismatch is an upstream problem, not something the update flow should fail on.
|
||||
- Shared helper `pmx_rebuild_dkms_after_kernel` lives in `scripts/global/utils-install-functions.sh`, so future updaters or CLI utilities can pick it up with a one-line call.
|
||||
|
||||
---
|
||||
|
||||
## 🔌 Post-install — Persistent NIC naming becomes idempotent
|
||||
|
||||
- **ProxMenux-owned `.link` files now carry a distinctive filename prefix and internal marker.** Files are written as `10-proxmenux-<iface>.link` and the first line of every file is `# Managed by ProxMenux — do not edit`. Both are checked by the reconciliation and uninstall paths before touching a file, so anything the operator wrote by hand or that came from another package is safe.
|
||||
- **Reruns of `setup_persistent_network` reconcile ProxMenux entries.** Every invocation walks the existing `10-proxmenux-*.link` files, extracts the `MACAddress=` value, compares it against the MACs currently present under `/sys/class/net/`, and removes only the ProxMenux-owned entries whose MAC is no longer there. Hardware replacements, NIC swaps and hardware migrations stop leaving orphan mappings behind on every rerun.
|
||||
- **Legacy 1.0-format files (`10-<iface>.link` written by the previous revision) are migrated on the first run of the new function.** If the file matches the exact template the 1.0 code used to write (two sections, `MACAddress=` + `Name=`, nothing else), it's removed and replaced with the new `10-proxmenux-<iface>.link` in one step. Any file that doesn't match the template exactly is left alone.
|
||||
- **The uninstall path (`uninstall_persistent_network`) now only removes files that carry both the `10-proxmenux-` filename prefix and the marker on the first line.** The previous `rm -f /etc/systemd/network/*.link` blanket sweep is gone — user-authored `.link` files stay in place regardless of their filename.
|
||||
- **Single shared implementation.** The three duplicated `setup_persistent_network` bodies (`auto_post_install.sh`, `customizable_post_install.sh`, `network_menu.sh`) plus the uninstall path now all delegate to `pmx_setup_persistent_network` / `pmx_uninstall_persistent_network` in `scripts/global/utils-install-functions.sh`. Future fixes can't miss a copy.
|
||||
- `FUNC_VERSION` bumped 1.0 → 1.1 on all three call sites so the ProxMenux update detector re-runs the function on hosts that already had the 1.0 build. That first re-run performs the legacy migration + reconciliation in one shot.
|
||||
|
||||
---
|
||||
|
||||
## 🧮 Post-install — ZFS ARC optimization narrowed to its scope
|
||||
|
||||
- **`optimize_zfs_arc` now sets only `zfs_arc_max`.** The function writes a single line to `/etc/modprobe.d/99-zfsarc.conf`: `options zfs zfs_arc_max=<cap>`. `zfs_arc_min` stays at the OpenZFS default (auto-calculated as the larger of 32 MiB and ~1/32 of RAM), and L2ARC (`l2arc_noprefetch`, `l2arc_write_max`) and TXG (`zfs_txg_timeout`) tunables — which are outside the scope of an ARC optimization — are left at their OpenZFS defaults unless the operator configures them elsewhere.
|
||||
- **The initramfs is now regenerated after writing the config.** On ZFS-on-root systems the ZFS module loads from the initramfs before the running system reads `/etc/modprobe.d/`, so a plain reboot wasn't enough for the new cap to take effect. `update-initramfs -u -k all` runs right after the file is written, plus `proxmox-boot-tool refresh` on systemd-boot hosts, so the value is picked up at the next boot instead of being shadowed by the initramfs's stale copy.
|
||||
- **The function is guarded on the presence of a live ZFS pool** (`zpool list` check) so it becomes a no-op on hosts that don't use ZFS.
|
||||
- **Cap values use clean binary sizes**: 512 MiB up to 16 GB RAM, 1 GiB up to 32 GB, RAM/8 above that — with a floor of 512 MiB so a bad memory reading never leaves an unusably small ARC.
|
||||
- `FUNC_VERSION` bumped 1.0 → 1.1 so the ProxMenux update detector re-runs the function on hosts that already had the 1.0 build. Because the write is a full rewrite of `99-zfsarc.conf`, running the updated function once replaces the whole file cleanly. The uninstall path now also runs `update-initramfs` + `proxmox-boot-tool refresh` after restoring or removing the config, so the revert propagates to the initramfs the same way.
|
||||
|
||||
---
|
||||
|
||||
## 🔥 Post-install — Firewall bridge sysctl tuning hardened
|
||||
|
||||
- **The `rp_filter=0` and `log_martians=0` tuning for `fwbr*`, `fwln*`, `fwpr*` and `tap*` interfaces now also applies to interfaces Proxmox spins up when a VM starts, stops, reboots or migrates.** A new `/etc/udev/rules.d/99-proxmenux-fwbr-tune.rules` fires a helper on every `net`/`add` event matching those prefixes, so each fresh interface picks up the correct value immediately — no reboot and no rerun of the post-install needed.
|
||||
- **The tuning logic is now in a standalone helper** at `/usr/local/sbin/proxmenux-fwbr-tune`, shared by the initial sweep (`proxmenux-fwbr-tune.service`, oneshot) and by the udev rule. An explicit invocation at install time ensures the current session sees the change without waiting for the next VM cycle.
|
||||
- **The customizable post-install flow (`customizable_post_install.sh`) now installs the same helper + oneshot service + udev rule + initial sweep as the automatic flow**, so both variants leave the system in the same end state.
|
||||
- Both `apply_network_optimizations` functions bumped `FUNC_VERSION` 1.0 → 1.1, so the ProxMenux update detector re-runs the function on hosts that already had the 1.0 build. The uninstall path (`uninstall_network_optimization`) is extended to remove the new helper and udev rule, and reload udev.
|
||||
|
||||
---
|
||||
|
||||
## 🧰 Post-install — Log2RAM + PBS
|
||||
|
||||
- **PBS API log rotation applied automatically when `proxmox-backup-server` runs as a service on the host.** Both Log2RAM installers (`install_log2ram_auto` and the customizable `configure_log2ram`) detect PBS via `dpkg-query` and drop `/etc/logrotate.d/proxmox-backup-api` with a 20MB × 3 rotation rule plus `/etc/cron.hourly/proxmox-backup-logrotate`. On a PVE host that also runs PBS as a service, `pvestatd`'s local-datastore poll writes to `/var/log/proxmox-backup/api/access.log` and `auth.log` every few seconds — the upstream PBS package ships no logrotate rule for those files, and this rule keeps them bounded so a tmpfs-backed `/var/log` stays comfortably under budget. No-op on hosts without PBS as a service.
|
||||
- **Upstream `log2ram` script patched to `rsync -aXv --no-acls` right after `install.sh`.** Both installers rewrite the call in place with a `sed` guarded by `grep -q` (backup at `.proxmenux.bak`, no-op if a future upstream release already dropped `-A`). Extended attributes (`-X`) are preserved. Result: `log2ram write` finishes cleanly on `/var/log.hdd` filesystems that don't accept POSIX ACLs (ZFS with `acltype=off`, ext4 mounted without the `acl` option) — no more `set_acl: Operation not supported` / exit 23 messages.
|
||||
- **Emergency block of `log2ram-check.sh` rotates PBS logs before truncating.** When `/var/log` crosses the 92% threshold, the auto-sync script now runs `logrotate -f /etc/logrotate.d/proxmox-backup-api` (only if the rule file exists) *before* truncating `pveproxy/access.log`, `pveproxy/error.log` and `pveam.log`. Recent PBS access/auth history is preserved in the rotated `.gz` files instead of being lost. Both `install_log2ram_auto` and `configure_log2ram` bumped `FUNC_VERSION` 1.2 → 1.3, and the embedded `log2ram-check.sh` header comment bumped v1.2 → v1.3.
|
||||
|
||||
## 🗄 Backup restore — ZFS data pools imported automatically
|
||||
|
||||
- **Separate ZFS data pools listed in the backup are now imported automatically at restore time.** Previously the restore flow correctly protected the root pool (rpool) from a stale `zpool.cache`, but any other ZFS pool a user had (for VMs, LXCs or plain data) was left up to `zfs-import-scan.service` to import at next boot — which fails when the fresh install has a different `hostid` than the one written on the pool's on-disk label. The pool would show as `FAILED Failed to start zfs-import-scan.service` and stay unavailable until the user ran `zpool import -f` manually. Now the restore iterates the backup's `storage_inventory.zfs_pools[]`, skips the root pool (already imported by the system), and runs `zpool import <name>` for every non-root pool whose disks are all present on `/dev/disk/by-id/`. If ZFS rejects the import as *foreign*, it retries with `-f` and reports the pool as forced so the operator knows the new hostid was grabbed onto the label. Pools with any missing disks are skipped with a clear warning instead of a silent import that would rebuild the pool without the missing vdev.
|
||||
- **Separate ZFS data pools listed in the backup are now imported automatically at restore time.** The new `_rs_import_data_pools` step runs after config apply, walks `storage_inventory.zfs_pools[]`, skips the root pool (already mounted by the system), and issues `zpool import <name>` for every non-root pool whose disks are all present on this host. When ZFS rejects the import as *foreign* — the typical case after a fresh install regrabs the pool label with a new `hostid` — the step retries with `-f` and reports the pool as forced so the operator has trace. Pools missing any disk are skipped with a clear warning rather than imported degraded. Together this closes the common case where `zfs-import-scan.service` failed at boot after a fresh install and left the separate data pool unavailable until `zpool import -f` was run manually.
|
||||
- **The auto-import result persists to the post-restore progress card.** The step writes a `data_pools_import` section into `/var/lib/proxmenux/restore-state.json` (the same JSON the Backups-tab card polls) and a raw log at `/var/log/proxmenux/restore-datapools-<timestamp>.log`. The Backups tab card renders a dedicated block inside Details with five color-coded rows (Imported / Forced / Skipped partial / Skipped missing / Failed) so the summary stays consultable after the restore terminal is closed, and the entry is preserved in the run's history for later review.
|
||||
- **ZFS pools created with `by-partuuid` or raw `/dev/sdX` are recognised by the disk-presence check.** The auto-import step and `validate_storage.sh` treat `devices_by_id` entries that start with `/` as absolute paths and only prepend `/dev/disk/by-id/` to bare basenames, so pools built against partition UUIDs or a raw block device are detected as present when their disks are on the host.
|
||||
- **`/etc/systemd/network` added to the default backup paths.** That directory holds systemd `.link` files that pin NIC names to their MAC across kernel updates and reinstalls — `setup_persistent_network` in the post_install writes them for every physical interface, and users can drop their own to rename a NIC to something meaningful. Preserving them across a fresh-install restore keeps the source host's NIC naming policy intact on the target, so `/etc/network/interfaces` entries that reference custom NIC names continue to resolve after the restore.
|
||||
|
||||
---
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- **@pepenai** — reported the mobile dashboard freeze on his HTTPS + reverse proxy setup that led to the Service Worker cleanup path.
|
||||
- **Pepo** — reported the `401 missing_timestamp` on his PVE Test button that led to the webhook self-IP allowlist and the v4-mapped fix.
|
||||
- **@ash34** (#255) — reported the `VM/CT check unavailable: 'NoneType' object has no attribute 'get'` that led to the defensive coalescing of the `details` column.
|
||||
- **Juan C.** — reported the post-restore `zfs-import-scan.service FAILED` on a fresh install with a separate data pool, which led to the auto-import step above.
|
||||
- **@pepenai** — mobile dashboard on HTTPS + reverse proxy.
|
||||
- **Pepo** — webhook auth from a Tailscale FQDN.
|
||||
- **@ash34** (#255) — VM/CT check with a NULL `details` row.
|
||||
- **@f3rs3n** (#256, #257, #258) — firewall bridge sysctl tuning, ZFS ARC optimization scope, and persistent NIC naming reconciliation.
|
||||
- **Juan C.** — ZFS data pool auto-import after a fresh install.
|
||||
- **David Barbero (@sikete)** — DKMS driver rebuild on kernel upgrade.
|
||||
|
||||
|
||||
## 2026-07-14
|
||||
|
||||
32
menu
32
menu
@@ -149,6 +149,24 @@ check_updates_stable() {
|
||||
PROMPT_AVAIL="$(translate 'New version available')"
|
||||
PROMPT_ASK="$(translate 'Do you want to update now?')"
|
||||
|
||||
# Running inside the Monitor's WebSocket terminal: the installer will
|
||||
# restart the Monitor service, which kills this shell mid-install and
|
||||
# leaves the update broken. Inform and route to SSH / host console.
|
||||
if [[ "${PROXMENUX_TERMINAL:-}" == "monitor" ]]; then
|
||||
local WS_INFO
|
||||
WS_INFO="$(translate 'A new ProxMenux version is available:') ${REMOTE_VERSION}
|
||||
|
||||
$(translate 'This session is running in the Monitor terminal. Updating from here would restart the Monitor service and cut the connection mid-install, leaving the update in a broken state.')
|
||||
|
||||
$(translate 'Run the update from an SSH session or the Proxmox host console with:')
|
||||
|
||||
bash <(curl -fsSL ${INSTALL_URL}) --update
|
||||
|
||||
$(translate 'You can keep using ProxMenux from this terminal.')"
|
||||
whiptail --title "$PROMPT_TITLE" --msgbox "$WS_INFO" 20 78
|
||||
return 0
|
||||
fi
|
||||
|
||||
if whiptail --title "$PROMPT_TITLE" \
|
||||
--yesno "$PROMPT_AVAIL ($REMOTE_VERSION)\n\n$PROMPT_ASK" \
|
||||
10 60 --defaultno; then
|
||||
@@ -180,6 +198,20 @@ check_updates_beta() {
|
||||
[[ -z "$REMOTE_BETA" || -z "$LOCAL_BETA" || "$LOCAL_BETA" = "$REMOTE_BETA" ]] && return 0
|
||||
[[ "$(printf '%s\n%s\n' "$LOCAL_BETA" "$REMOTE_BETA" | sort -V | tail -1)" = "$REMOTE_BETA" ]] || return 0
|
||||
|
||||
if [[ "${PROXMENUX_TERMINAL:-}" == "monitor" ]]; then
|
||||
whiptail --title "Beta Update Available" --msgbox "\
|
||||
A new beta build is available: $REMOTE_BETA
|
||||
|
||||
This session is running in the Monitor terminal. Updating from here would restart the Monitor service and cut the connection mid-install, leaving the update in a broken state.
|
||||
|
||||
Run the update from an SSH session or the Proxmox host console with:
|
||||
|
||||
bash <(curl -fsSL $REPO_DEVELOP/install_proxmenux_beta.sh) --update
|
||||
|
||||
You can keep using ProxMenux from this terminal." 20 78
|
||||
return 0
|
||||
fi
|
||||
|
||||
if whiptail --title "Beta Update Available" \
|
||||
--yesno "A new beta build is available!\n\nInstalled beta : $LOCAL_BETA\nNew beta build : $REMOTE_BETA\n\nDo you want to update now?" \
|
||||
12 64 --defaultno; then
|
||||
|
||||
@@ -3398,15 +3398,8 @@ _rs_run_custom_restore() {
|
||||
# user-installed packages is a prerequisite for the restored
|
||||
# systemd units and config files to actually do anything.
|
||||
|
||||
# ─── _rs_import_data_pools ─────────────────────────────────────
|
||||
# Imports the ZFS data pools that were present in the backup but
|
||||
# aren't yet imported on this host — typical scenario is a fresh
|
||||
# PVE install onto new boot disks while the data-pool disks stay
|
||||
# in place. Never touches the root pool (already imported by the
|
||||
# system), never forces an import that's missing disks. Retries
|
||||
# with -f when the pool imports as foreign (hostid mismatch after
|
||||
# a fresh install regrabs the label) and reports which pools were
|
||||
# forced so the operator has traceability.
|
||||
# Import non-root ZFS pools from the backup manifest whose disks are
|
||||
# all present on this host. Retry with -f on foreign hostid.
|
||||
_rs_import_data_pools() {
|
||||
local staging_root="$1"
|
||||
local manifest="$staging_root/manifest.json"
|
||||
@@ -3414,7 +3407,6 @@ _rs_import_data_pools() {
|
||||
command -v zpool >/dev/null 2>&1 || return 0
|
||||
command -v jq >/dev/null 2>&1 || return 0
|
||||
|
||||
# Root pool → skip (mounted at / by the system).
|
||||
local root_pool=""
|
||||
if command -v zfs >/dev/null 2>&1; then
|
||||
local root_dataset
|
||||
@@ -3423,7 +3415,6 @@ _rs_import_data_pools() {
|
||||
root_pool="${root_dataset%%/*}"
|
||||
fi
|
||||
|
||||
# Live pools already imported.
|
||||
local live_pools
|
||||
live_pools=$(zpool list -H -o name 2>/dev/null || true)
|
||||
|
||||
@@ -3437,15 +3428,21 @@ _rs_import_data_pools() {
|
||||
continue
|
||||
fi
|
||||
|
||||
local devs_all devs_total=0 devs_present=0 devs_missing=0 dev
|
||||
local devs_all devs_total=0 devs_present=0 devs_missing=0 dev dev_path
|
||||
devs_all=$(jq -r --arg n "$pool_name" \
|
||||
'.storage_inventory.zfs_pools[]? | select(.name==$n) | .devices_by_id[]?' \
|
||||
"$manifest" 2>/dev/null)
|
||||
[[ -z "$devs_all" ]] && continue
|
||||
# devices_by_id entries can be a by-id basename or an absolute path.
|
||||
while IFS= read -r dev; do
|
||||
[[ -z "$dev" ]] && continue
|
||||
((devs_total++))
|
||||
if [[ -e "/dev/disk/by-id/$dev" ]]; then
|
||||
if [[ "$dev" == /* ]]; then
|
||||
dev_path="$dev"
|
||||
else
|
||||
dev_path="/dev/disk/by-id/$dev"
|
||||
fi
|
||||
if [[ -e "$dev_path" ]]; then
|
||||
((devs_present++))
|
||||
else
|
||||
((devs_missing++))
|
||||
@@ -3483,7 +3480,6 @@ _rs_import_data_pools() {
|
||||
fi
|
||||
if (( ${#forced[@]} > 0 )); then
|
||||
msg_ok "$(translate 'Imported (foreign hostid, forced):') ${forced[*]}"
|
||||
echo -e "${TAB}${BL}$(translate "New hostid grabbed onto the pool label — next boot imports clean.")${CL}"
|
||||
fi
|
||||
if (( ${#partial[@]} > 0 )); then
|
||||
msg_warn "$(translate 'Skipped (some disks missing):') ${partial[*]}"
|
||||
@@ -3495,22 +3491,15 @@ _rs_import_data_pools() {
|
||||
msg_error "$(translate 'Import failed — inspect with `zpool import`:') ${failed[*]}"
|
||||
fi
|
||||
|
||||
# Persist so the post-restore card can show the summary after the
|
||||
# reboot. The Monitor's ScriptTerminalModal loses its buffer on
|
||||
# close, and monitor_apply.sh doesn't tee to disk — without this
|
||||
# the operator has no trace once they dismiss the terminal.
|
||||
_rs_persist_datapool_import "${ok[@]}" "|FORCED|" "${forced[@]}" \
|
||||
"|PARTIAL|" "${partial[@]}" "|MISSING|" "${missing[@]}" \
|
||||
"|FAILED|" "${failed[@]}"
|
||||
}
|
||||
|
||||
# Serialise the auto-import result to restore-state.json (Monitor UI)
|
||||
# and to a raw log under /var/log/proxmenux. The state file is the
|
||||
# same one apply_cluster_postboot.sh manages — jq `*` merge preserves
|
||||
# our section when postboot later seeds its own fields. If the file
|
||||
# doesn't exist yet (operator hasn't rebooted, or restore ran without
|
||||
# a pending-reboot step) we seed placeholder fields so the UI can
|
||||
# still render just our section without crashing on undefined keys.
|
||||
# Write the data_pools_import section into restore-state.json (which
|
||||
# the Backups tab polls) and an append-only log under /var/log/proxmenux.
|
||||
# Seeds the state file with placeholder fields when it doesn't exist yet
|
||||
# so the UI can render just this section without crashing on undefined keys.
|
||||
_rs_persist_datapool_import() {
|
||||
command -v jq >/dev/null 2>&1 || return 0
|
||||
local mode="OK"
|
||||
@@ -3540,7 +3529,6 @@ _rs_persist_datapool_import() {
|
||||
local log_file="$log_dir/restore-datapools-$(date +%Y%m%d_%H%M%S).log"
|
||||
mkdir -p "$state_dir" "$log_dir" 2>/dev/null || true
|
||||
|
||||
# Raw log — appended one line per pool with the resolved action.
|
||||
{
|
||||
echo "=== ProxMenux data-pool auto-import at $(date -Iseconds) ==="
|
||||
local p
|
||||
@@ -3551,9 +3539,8 @@ _rs_persist_datapool_import() {
|
||||
for p in "${failed[@]}"; do echo "FAILED $p (zpool import failed — inspect manually)"; done
|
||||
} >>"$log_file" 2>/dev/null || true
|
||||
|
||||
# State-file JSON section. `jq -sR` reads each array from a
|
||||
# newline-separated stdin so pool names with spaces (partial
|
||||
# entries include a count fragment) round-trip intact.
|
||||
# jq -sR reads each array from newline-separated stdin so entries
|
||||
# containing spaces (partial pools carry a count fragment) round-trip.
|
||||
local ok_json forced_json partial_json missing_json failed_json section
|
||||
ok_json=$(printf '%s\n' "${ok[@]:-}" | jq -Rsc 'split("\n") | map(select(length>0))')
|
||||
forced_json=$(printf '%s\n' "${forced[@]:-}" | jq -Rsc 'split("\n") | map(select(length>0))')
|
||||
@@ -3578,16 +3565,12 @@ _rs_persist_datapool_import() {
|
||||
local tmp
|
||||
tmp=$(mktemp "${state_file}.XXXXXX") || return 0
|
||||
if [[ -f "$state_file" ]]; then
|
||||
# Merge into whatever's already there.
|
||||
if jq -c ". * $section" "$state_file" > "$tmp" 2>/dev/null; then
|
||||
mv -f "$tmp" "$state_file"
|
||||
else
|
||||
rm -f "$tmp"
|
||||
fi
|
||||
else
|
||||
# Seed with placeholders so the card renders without crashing
|
||||
# if the operator never triggers the postboot flow (hot-only
|
||||
# restore or a restore path where no reboot is scheduled).
|
||||
local seed
|
||||
seed=$(jq -n \
|
||||
--arg started "$(date -Iseconds)" \
|
||||
@@ -3832,14 +3815,9 @@ _rs_run_complete_extras() {
|
||||
fi
|
||||
|
||||
# ─ Data ZFS pools — auto-import ───────────────────────────
|
||||
# Import ZFS pools that were present in the backup and whose
|
||||
# disks are still on this host. Skips the root pool (already
|
||||
# imported by the system) and any pool missing disks. When a
|
||||
# pool imports but ZFS marks it foreign (hostid mismatch from
|
||||
# a fresh install), retries with -f and flags it for the
|
||||
# operator. Field-reported by Juan C.: fresh PVE install →
|
||||
# restore → separate data pool stayed unavailable because
|
||||
# zfs-import-scan.service failed on hostid mismatch.
|
||||
# Skips the root pool and any pool whose disks aren't all present.
|
||||
# Retries with -f when ZFS rejects the import as foreign and flags
|
||||
# the pool as forced in the report.
|
||||
_rs_import_data_pools "$staging_root"
|
||||
|
||||
# ─ Guest configs — only in full strategies ────────────────
|
||||
|
||||
@@ -107,6 +107,7 @@ hb_default_profile_paths() {
|
||||
# ── Common Proxmox tooling (skipped if not present) ──
|
||||
"/etc/systemd/system" # custom units (including log2ram.service if installed)
|
||||
"/etc/systemd/journald.conf" # journal retention tuning from post-install
|
||||
"/etc/systemd/network" # .link files that pin NIC names to MAC
|
||||
"/etc/log2ram.conf"
|
||||
"/etc/logrotate.conf"
|
||||
"/etc/logrotate.d" # post-install drops log2ram + custom logrotate here
|
||||
|
||||
@@ -30,9 +30,16 @@ while IFS= read -r pool_json; do
|
||||
needed_devs="$(printf '%s' "$pool_json" | jq -r '.devices_by_id[]?')"
|
||||
present=()
|
||||
missing=()
|
||||
# devices_by_id entries can be a by-id basename or an absolute path.
|
||||
dev_path=""
|
||||
while IFS= read -r dev; do
|
||||
[[ -z "$dev" ]] && continue
|
||||
if [[ -e "/dev/disk/by-id/$dev" ]]; then
|
||||
if [[ "$dev" == /* ]]; then
|
||||
dev_path="$dev"
|
||||
else
|
||||
dev_path="/dev/disk/by-id/$dev"
|
||||
fi
|
||||
if [[ -e "$dev_path" ]]; then
|
||||
present+=("$dev")
|
||||
else
|
||||
missing+=("$dev")
|
||||
|
||||
@@ -241,7 +241,12 @@ update_pve_safe() {
|
||||
lvm_repair_check
|
||||
fi
|
||||
|
||||
# ── 9. Final cleanup ──
|
||||
# ── 9. DKMS driver rebuild if a new kernel was staged ──
|
||||
if declare -f pmx_rebuild_dkms_after_kernel >/dev/null 2>&1; then
|
||||
pmx_rebuild_dkms_after_kernel
|
||||
fi
|
||||
|
||||
# ── 10. Final cleanup ──
|
||||
apt-get -y autoremove >/dev/null 2>&1 || true
|
||||
apt-get -y autoclean >/dev/null 2>&1 || true
|
||||
msg_ok "$(translate "Cleanup finished")"
|
||||
|
||||
@@ -153,3 +153,306 @@ install_single_package() {
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# ==========================================================
|
||||
# Persistent NIC naming — shared helpers
|
||||
# ==========================================================
|
||||
# ProxMenux-owned .link files carry two identifying marks:
|
||||
# - filename prefix: 10-proxmenux-<iface>.link
|
||||
# - first-line marker: `# Managed by ProxMenux — do not edit`
|
||||
# Both are required for pmx_uninstall_persistent_network to remove a
|
||||
# file, so user-authored .link files in /etc/systemd/network/ are
|
||||
# never touched.
|
||||
|
||||
readonly PMX_NIC_LINK_DIR="/etc/systemd/network"
|
||||
readonly PMX_NIC_LINK_MARKER="# Managed by ProxMenux — do not edit"
|
||||
|
||||
# Print the MAC address recorded in a .link file, uppercased, or empty
|
||||
# if the file has no MACAddress= line.
|
||||
_pmx_link_mac() {
|
||||
awk -F= '
|
||||
/^[[:space:]]*MACAddress=/ {
|
||||
gsub(/[[:space:]]/, "", $2)
|
||||
print toupper($2)
|
||||
exit
|
||||
}
|
||||
' "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
# Returns 0 if the file exists AND its first line matches the marker.
|
||||
_pmx_link_is_managed() {
|
||||
local first
|
||||
IFS= read -r first < "$1" 2>/dev/null || return 1
|
||||
[[ "$first" == "$PMX_NIC_LINK_MARKER" ]]
|
||||
}
|
||||
|
||||
# Returns 0 if the file matches the exact template ProxMenux 1.0 used
|
||||
# to write (no marker, no extra fields). Used once at 1.1 upgrade time
|
||||
# to reclaim files created by an earlier version and replace them with
|
||||
# the marked format.
|
||||
_pmx_link_matches_legacy_10() {
|
||||
local file="$1" iface mac
|
||||
iface=$(basename "$file" .link)
|
||||
iface="${iface#10-}"
|
||||
mac=$(_pmx_link_mac "$file")
|
||||
[[ -z "$iface" || -z "$mac" ]] && return 1
|
||||
local expected
|
||||
expected="[Match]
|
||||
MACAddress=$mac
|
||||
|
||||
[Link]
|
||||
Name=$iface"
|
||||
[[ "$(cat "$file" 2>/dev/null)" == "$expected" ]]
|
||||
}
|
||||
|
||||
# Detect physical NICs and generate 10-proxmenux-<iface>.link for
|
||||
# each. Idempotent — reruns replace stale ProxMenux entries whose MAC
|
||||
# is no longer present, migrate 1.0-format files, and leave every
|
||||
# other .link in the directory alone.
|
||||
#
|
||||
# Outputs (via `printf`):
|
||||
# line "COUNT=<n>" — number of managed files after the run
|
||||
# line "REMOVED_STALE=<n>" — reconciled entries for missing MACs
|
||||
# line "REMOVED_LEGACY=<n>" — 1.0-format files replaced
|
||||
pmx_setup_persistent_network() {
|
||||
mkdir -p "$PMX_NIC_LINK_DIR"
|
||||
|
||||
declare -A current_macs=()
|
||||
local dev_path iface mac
|
||||
for dev_path in /sys/class/net/*; do
|
||||
iface=$(basename "$dev_path")
|
||||
case "$iface" in
|
||||
lo|docker*|veth*|br-*|vmbr*|tap*|fwpr*|fwln*|virbr*|bond*|cilium*|zt*|wg*)
|
||||
continue ;;
|
||||
esac
|
||||
if [[ -e "$dev_path/device" || -e "$dev_path/phy80211" ]]; then
|
||||
mac=$(cat "$dev_path/address" 2>/dev/null | tr '[:lower:]' '[:upper:]')
|
||||
[[ "$mac" =~ ^([A-F0-9]{2}:){5}[A-F0-9]{2}$ ]] && current_macs["$mac"]=1
|
||||
fi
|
||||
done
|
||||
|
||||
if compgen -G "$PMX_NIC_LINK_DIR"/*.link >/dev/null; then
|
||||
local backup_dir
|
||||
backup_dir="$PMX_NIC_LINK_DIR/backup-$(date +%Y%m%d-%H%M%S)"
|
||||
mkdir -p "$backup_dir"
|
||||
cp "$PMX_NIC_LINK_DIR"/*.link "$backup_dir"/ 2>/dev/null || true
|
||||
fi
|
||||
|
||||
local removed_stale=0 removed_legacy=0
|
||||
local link_file
|
||||
for link_file in "$PMX_NIC_LINK_DIR"/10-proxmenux-*.link; do
|
||||
[[ -f "$link_file" ]] || continue
|
||||
_pmx_link_is_managed "$link_file" || continue
|
||||
mac=$(_pmx_link_mac "$link_file")
|
||||
[[ -z "$mac" ]] && continue
|
||||
if [[ -z "${current_macs[$mac]+x}" ]]; then
|
||||
rm -f -- "$link_file"
|
||||
removed_stale=$((removed_stale + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
for link_file in "$PMX_NIC_LINK_DIR"/10-*.link; do
|
||||
[[ -f "$link_file" ]] || continue
|
||||
[[ "$(basename "$link_file")" == 10-proxmenux-*.link ]] && continue
|
||||
if _pmx_link_matches_legacy_10 "$link_file"; then
|
||||
rm -f -- "$link_file"
|
||||
removed_legacy=$((removed_legacy + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
local count=0
|
||||
for dev_path in /sys/class/net/*; do
|
||||
iface=$(basename "$dev_path")
|
||||
case "$iface" in
|
||||
lo|docker*|veth*|br-*|vmbr*|tap*|fwpr*|fwln*|virbr*|bond*|cilium*|zt*|wg*)
|
||||
continue ;;
|
||||
esac
|
||||
[[ -e "$dev_path/device" || -e "$dev_path/phy80211" ]] || continue
|
||||
mac=$(cat "$dev_path/address" 2>/dev/null | tr '[:lower:]' '[:upper:]')
|
||||
[[ "$mac" =~ ^([A-F0-9]{2}:){5}[A-F0-9]{2}$ ]] || continue
|
||||
|
||||
local link_file="$PMX_NIC_LINK_DIR/10-proxmenux-$iface.link"
|
||||
cat > "$link_file" <<EOF
|
||||
$PMX_NIC_LINK_MARKER
|
||||
|
||||
[Match]
|
||||
MACAddress=$mac
|
||||
|
||||
[Link]
|
||||
Name=$iface
|
||||
EOF
|
||||
chmod 644 "$link_file"
|
||||
count=$((count + 1))
|
||||
done
|
||||
|
||||
printf 'COUNT=%d\n' "$count"
|
||||
printf 'REMOVED_STALE=%d\n' "$removed_stale"
|
||||
printf 'REMOVED_LEGACY=%d\n' "$removed_legacy"
|
||||
}
|
||||
|
||||
# Remove only files that carry BOTH the ProxMenux filename prefix AND
|
||||
# the marker on the first line. User-authored .link files are left
|
||||
# intact regardless of their name.
|
||||
pmx_uninstall_persistent_network() {
|
||||
local removed=0 link_file
|
||||
for link_file in "$PMX_NIC_LINK_DIR"/10-proxmenux-*.link; do
|
||||
[[ -f "$link_file" ]] || continue
|
||||
_pmx_link_is_managed "$link_file" || continue
|
||||
rm -f -- "$link_file"
|
||||
removed=$((removed + 1))
|
||||
done
|
||||
printf 'REMOVED=%d\n' "$removed"
|
||||
}
|
||||
|
||||
|
||||
# ==========================================================
|
||||
# DKMS driver rebuild after a kernel upgrade
|
||||
# ==========================================================
|
||||
# Called from update-pve-safe.sh and proxmox_update.sh after
|
||||
# apt full-upgrade succeeds. Detects whether a new kernel is
|
||||
# staged for the next boot and, if so, ensures matching headers
|
||||
# are installed and rebuilds every DKMS module registered by
|
||||
# ProxMenux components against that kernel — so drivers keep
|
||||
# working after the reboot without operator intervention.
|
||||
|
||||
readonly PMX_COMPONENTS_STATUS="/usr/local/share/proxmenux/components_status.json"
|
||||
|
||||
# component_key : dkms_module_name (empty module = not DKMS)
|
||||
readonly -a PMX_DKMS_COMPONENTS=(
|
||||
"nvidia_driver:nvidia"
|
||||
"coral_driver:gasket"
|
||||
)
|
||||
|
||||
# Print the newest installed pve/proxmox kernel version, or empty.
|
||||
_pmx_newest_installed_kernel() {
|
||||
dpkg-query -W -f='${Status}\t${Package}\n' \
|
||||
'proxmox-kernel-*-pve-signed' 'pve-kernel-*-pve' 2>/dev/null \
|
||||
| awk -F'\t' '/^install ok installed\t/ { print $2 }' \
|
||||
| sed -E 's/^(proxmox|pve)-kernel-//; s/-signed$//' \
|
||||
| sort -V | tail -1
|
||||
}
|
||||
|
||||
# Return the header package name matching a kernel version.
|
||||
_pmx_header_pkg_for_kernel() {
|
||||
local kver="$1"
|
||||
if apt-cache show "proxmox-headers-$kver" >/dev/null 2>&1; then
|
||||
printf 'proxmox-headers-%s\n' "$kver"
|
||||
else
|
||||
printf 'pve-headers-%s\n' "$kver"
|
||||
fi
|
||||
}
|
||||
|
||||
# Return 0 if the DKMS module is 'installed' for the given kernel.
|
||||
_pmx_dkms_module_installed_for_kernel() {
|
||||
local module="$1" kver="$2"
|
||||
command -v dkms >/dev/null 2>&1 || return 1
|
||||
dkms status 2>/dev/null | awk -v m="$module" -v k="$kver" '
|
||||
BEGIN { FS = "[,:]" }
|
||||
{
|
||||
gsub(/[[:space:]]/, "")
|
||||
if (index($0, m "/") == 1 && index($0, k) > 0 && $0 ~ /installed/) {
|
||||
found = 1; exit
|
||||
}
|
||||
}
|
||||
END { exit found ? 0 : 1 }
|
||||
'
|
||||
}
|
||||
|
||||
# Best-effort rebuild. Never returns non-zero — the update flow that
|
||||
# calls this must always complete regardless of driver state.
|
||||
pmx_rebuild_dkms_after_kernel() {
|
||||
command -v dkms >/dev/null 2>&1 || return 0
|
||||
[[ -f "$PMX_COMPONENTS_STATUS" ]] || return 0
|
||||
command -v jq >/dev/null 2>&1 || return 0
|
||||
|
||||
local running_kernel newest_kernel
|
||||
running_kernel=$(uname -r)
|
||||
newest_kernel=$(_pmx_newest_installed_kernel)
|
||||
[[ -z "$newest_kernel" || "$newest_kernel" == "$running_kernel" ]] && return 0
|
||||
|
||||
local -a pending_components=() pending_modules=()
|
||||
local entry key module status
|
||||
for entry in "${PMX_DKMS_COMPONENTS[@]}"; do
|
||||
key="${entry%%:*}"
|
||||
module="${entry##*:}"
|
||||
status=$(jq -r --arg k "$key" '.[$k].status // ""' "$PMX_COMPONENTS_STATUS" 2>/dev/null)
|
||||
[[ "$status" == "installed" ]] || continue
|
||||
pending_components+=("$key")
|
||||
pending_modules+=("$module")
|
||||
done
|
||||
(( ${#pending_components[@]} == 0 )) && return 0
|
||||
|
||||
local msg
|
||||
msg="$(translate 'A new kernel is staged for the next boot:')"$'\n'
|
||||
msg+=" ${newest_kernel}"$'\n\n'
|
||||
msg+="$(translate 'The following DKMS-managed drivers will now be rebuilt against it so they keep working after reboot:')"$'\n'
|
||||
local c
|
||||
for c in "${pending_components[@]}"; do
|
||||
msg+=" • $c"$'\n'
|
||||
done
|
||||
msg+=$'\n'"$(translate 'This may take a few minutes. Press OK to proceed.')"
|
||||
|
||||
if [[ -t 0 ]] && command -v whiptail >/dev/null 2>&1; then
|
||||
whiptail --title "$(translate 'DKMS driver rebuild')" --msgbox "$msg" 18 78
|
||||
else
|
||||
msg_info "$(translate 'New kernel staged; rebuilding DKMS drivers:') ${pending_components[*]}"
|
||||
fi
|
||||
|
||||
local header_pkg
|
||||
header_pkg=$(_pmx_header_pkg_for_kernel "$newest_kernel")
|
||||
if ! dpkg-query -W -f='${Status}' "$header_pkg" 2>/dev/null | grep -q 'install ok installed'; then
|
||||
msg_info "$(translate 'Installing kernel headers:') $header_pkg"
|
||||
if DEBIAN_FRONTEND=noninteractive apt-get install -y "$header_pkg" >/dev/null 2>&1; then
|
||||
msg_ok "$(translate 'Kernel headers installed')"
|
||||
else
|
||||
msg_warn "$(translate 'Kernel headers install failed — DKMS rebuild will likely fail:') $header_pkg"
|
||||
fi
|
||||
fi
|
||||
|
||||
msg_info "$(translate 'Running dkms autoinstall for kernel') ${newest_kernel}..."
|
||||
dkms autoinstall -k "$newest_kernel" >/dev/null 2>&1 || true
|
||||
|
||||
local -a failed_components=() failed_modules=()
|
||||
local i
|
||||
for i in "${!pending_components[@]}"; do
|
||||
if ! _pmx_dkms_module_installed_for_kernel "${pending_modules[$i]}" "$newest_kernel"; then
|
||||
failed_components+=("${pending_components[$i]}")
|
||||
failed_modules+=("${pending_modules[$i]}")
|
||||
fi
|
||||
done
|
||||
|
||||
if (( ${#failed_components[@]} == 0 )); then
|
||||
msg_ok "$(translate 'DKMS drivers rebuilt for kernel') ${newest_kernel}: ${pending_components[*]}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
msg_warn "$(translate 'dkms autoinstall did not activate:') ${failed_components[*]}"
|
||||
msg_info "$(translate 'Falling back to each installer with --auto-reinstall...')"
|
||||
|
||||
declare -A installer_for=(
|
||||
[nvidia_driver]="/usr/local/share/proxmenux/scripts/gpu_tpu/nvidia_installer.sh"
|
||||
[coral_driver]="/usr/local/share/proxmenux/scripts/gpu_tpu/install_coral.sh"
|
||||
)
|
||||
local comp installer still_failing=()
|
||||
for comp in "${failed_components[@]}"; do
|
||||
installer="${installer_for[$comp]:-}"
|
||||
[[ -n "$installer" && -x "$installer" ]] || {
|
||||
still_failing+=("$comp")
|
||||
continue
|
||||
}
|
||||
msg_info "$(translate 'Reinstalling') $comp..."
|
||||
if bash "$installer" --auto-reinstall >/dev/null 2>&1; then
|
||||
msg_ok "$(translate 'Reinstalled') $comp"
|
||||
else
|
||||
still_failing+=("$comp")
|
||||
fi
|
||||
done
|
||||
|
||||
if (( ${#still_failing[@]} > 0 )); then
|
||||
msg_warn "$(translate 'The following drivers could not be rebuilt for the new kernel — run their installer manually after reboot:') ${still_failing[*]}"
|
||||
else
|
||||
msg_ok "$(translate 'DKMS drivers reinstalled for kernel') ${newest_kernel}"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -751,57 +751,45 @@ guided_configuration_cleanup() {
|
||||
|
||||
|
||||
setup_persistent_network() {
|
||||
local LINK_DIR="/etc/systemd/network"
|
||||
local BACKUP_DIR="/etc/systemd/network/backup-$(date +%Y%m%d-%H%M%S)"
|
||||
|
||||
if ! dialog --title "$(translate "Network Interface Setup")" \
|
||||
--yesno "\n$(translate "Create persistent network interface names?")" 8 60; then
|
||||
return 1
|
||||
fi
|
||||
show_proxmenux_logo
|
||||
show_proxmenux_logo
|
||||
msg_info "$(translate "Setting up persistent network interfaces")"
|
||||
sleep 2
|
||||
# Create directory
|
||||
mkdir -p "$LINK_DIR"
|
||||
|
||||
# Backup existing files
|
||||
if ls "$LINK_DIR"/*.link >/dev/null 2>&1; then
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
cp "$LINK_DIR"/*.link "$BACKUP_DIR"/ 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Process physical interfaces
|
||||
local count=0
|
||||
for iface in $(ls /sys/class/net/ | grep -vE "lo|docker|veth|br-|vmbr|tap|fwpr|fwln|virbr|bond|cilium|zt|wg"); do
|
||||
if [[ -e "/sys/class/net/$iface/device" ]] || [[ -e "/sys/class/net/$iface/phy80211" ]]; then
|
||||
local MAC=$(cat /sys/class/net/$iface/address 2>/dev/null)
|
||||
|
||||
if [[ "$MAC" =~ ^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$ ]]; then
|
||||
local LINK_FILE="$LINK_DIR/10-$iface.link"
|
||||
|
||||
cat > "$LINK_FILE" <<EOF
|
||||
[Match]
|
||||
MACAddress=$MAC
|
||||
|
||||
[Link]
|
||||
Name=$iface
|
||||
EOF
|
||||
chmod 644 "$LINK_FILE"
|
||||
((count++))
|
||||
fi
|
||||
if ! type pmx_setup_persistent_network &>/dev/null; then
|
||||
if [[ -f "$LOCAL_SCRIPTS/global/utils-install-functions.sh" ]]; then
|
||||
source "$LOCAL_SCRIPTS/global/utils-install-functions.sh"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $count -gt 0 ]]; then
|
||||
fi
|
||||
|
||||
local count=0 removed_stale=0 removed_legacy=0
|
||||
while IFS='=' read -r key value; do
|
||||
case "$key" in
|
||||
COUNT) count="$value" ;;
|
||||
REMOVED_STALE) removed_stale="$value" ;;
|
||||
REMOVED_LEGACY) removed_legacy="$value" ;;
|
||||
esac
|
||||
done < <(pmx_setup_persistent_network)
|
||||
|
||||
if (( removed_legacy > 0 )); then
|
||||
msg_ok "$(translate "Migrated") $removed_legacy $(translate "legacy .link file(s) to the ProxMenux-managed format")"
|
||||
fi
|
||||
if (( removed_stale > 0 )); then
|
||||
msg_ok "$(translate "Reconciled") $removed_stale $(translate "stale entry/entries for interfaces no longer present")"
|
||||
fi
|
||||
if (( count > 0 )); then
|
||||
msg_ok "$(translate "Created persistent names for") $count $(translate "interfaces")"
|
||||
msg_ok "$(translate "Changes will apply after reboot.")"
|
||||
else
|
||||
msg_warn "$(translate "No physical interfaces found")"
|
||||
fi
|
||||
register_tool "persistent_network" true
|
||||
echo -e
|
||||
msg_success "$(translate "Press ENTER to continue...")"
|
||||
read -r
|
||||
register_tool "persistent_network" true
|
||||
echo -e
|
||||
msg_success "$(translate "Press ENTER to continue...")"
|
||||
read -r
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -497,7 +497,7 @@ force_apt_ipv4() {
|
||||
# ==========================================================
|
||||
|
||||
apply_network_optimizations() {
|
||||
local FUNC_VERSION="1.0"
|
||||
local FUNC_VERSION="1.1"
|
||||
# description: Tune TCP buffers, somaxconn, IPv4 hardening and disable rp_filter on fw bridges (PVE 9 compatible).
|
||||
msg_info "$(translate "Optimizing network settings...")"
|
||||
NECESSARY_REBOOT=1
|
||||
@@ -548,8 +548,38 @@ EOF
|
||||
|
||||
sysctl --system > /dev/null 2>&1
|
||||
|
||||
cat > /usr/local/sbin/proxmenux-fwbr-tune <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
# Set rp_filter=0 and log_martians=0 on Proxmox fw bridge interfaces.
|
||||
# No arg → sweep every interface currently under /proc/sys/net/ipv4/conf/.
|
||||
# One arg → tune only that interface (used by the udev rule).
|
||||
set -u
|
||||
|
||||
cat >/etc/systemd/system/proxmenux-fwbr-tune.service <<'EOF'
|
||||
tune_interface() {
|
||||
local iface="$1"
|
||||
local sysctl_path="/proc/sys/net/ipv4/conf/${iface}"
|
||||
case "$iface" in
|
||||
fwbr*|fwln*|fwpr*|tap*)
|
||||
[[ -d "$sysctl_path" ]] || return 0
|
||||
[[ -w "$sysctl_path/rp_filter" ]] && printf '0\n' > "$sysctl_path/rp_filter"
|
||||
[[ -w "$sysctl_path/log_martians" ]] && printf '0\n' > "$sysctl_path/log_martians"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [[ $# -gt 0 ]]; then
|
||||
tune_interface "$1"
|
||||
else
|
||||
for sysctl_path in /proc/sys/net/ipv4/conf/*; do
|
||||
[[ -d "$sysctl_path" ]] || continue
|
||||
tune_interface "${sysctl_path##*/}"
|
||||
done
|
||||
fi
|
||||
EOF
|
||||
chmod 0755 /usr/local/sbin/proxmenux-fwbr-tune
|
||||
chown root:root /usr/local/sbin/proxmenux-fwbr-tune
|
||||
|
||||
cat > /etc/systemd/system/proxmenux-fwbr-tune.service <<'EOF'
|
||||
[Unit]
|
||||
Description=ProxMenux - Tune rp_filter/log_martians on virtual fw bridges
|
||||
After=network-online.target
|
||||
@@ -557,14 +587,26 @@ Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/bash -c 'for i in /proc/sys/net/ipv4/conf/*; do n=${i##*/}; case "$n" in fwbr*|fwln*|fwpr*|tap*) echo 0 > /proc/sys/net/ipv4/conf/$n/rp_filter; echo 0 > /proc/sys/net/ipv4/conf/$n/log_martians; esac; done'
|
||||
ExecStart=/usr/local/sbin/proxmenux-fwbr-tune
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
cat > /etc/udev/rules.d/99-proxmenux-fwbr-tune.rules <<'EOF'
|
||||
ACTION=="add", SUBSYSTEM=="net", KERNEL=="fwbr*", RUN+="/usr/local/sbin/proxmenux-fwbr-tune %k"
|
||||
ACTION=="add", SUBSYSTEM=="net", KERNEL=="fwln*", RUN+="/usr/local/sbin/proxmenux-fwbr-tune %k"
|
||||
ACTION=="add", SUBSYSTEM=="net", KERNEL=="fwpr*", RUN+="/usr/local/sbin/proxmenux-fwbr-tune %k"
|
||||
ACTION=="add", SUBSYSTEM=="net", KERNEL=="tap*", RUN+="/usr/local/sbin/proxmenux-fwbr-tune %k"
|
||||
EOF
|
||||
chmod 0644 /etc/udev/rules.d/99-proxmenux-fwbr-tune.rules
|
||||
chown root:root /etc/udev/rules.d/99-proxmenux-fwbr-tune.rules
|
||||
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
udevadm control --reload-rules >/dev/null 2>&1 || true
|
||||
systemctl enable --now proxmenux-fwbr-tune.service >/dev/null 2>&1 || true
|
||||
/usr/local/sbin/proxmenux-fwbr-tune >/dev/null 2>&1 || true
|
||||
|
||||
|
||||
local interfaces_file="/etc/network/interfaces"
|
||||
@@ -638,7 +680,7 @@ EOF
|
||||
|
||||
|
||||
install_log2ram_auto() {
|
||||
local FUNC_VERSION="1.2"
|
||||
local FUNC_VERSION="1.3"
|
||||
|
||||
# description: Install Log2RAM with size auto-tuned to host RAM (128M/256M/512M); SSD/M.2 detection skips on rotational disks.
|
||||
|
||||
@@ -727,6 +769,52 @@ install_log2ram_auto() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Drop ACL preservation from the upstream rsync call: some
|
||||
# /var/log.hdd filesystems reject POSIX ACLs and log2ram write
|
||||
# exits 23 with `set_acl: Operation not supported`. xattrs stay.
|
||||
local _l2r_bin
|
||||
for _l2r_bin in \
|
||||
"$(command -v log2ram 2>/dev/null)" \
|
||||
/usr/local/bin/log2ram \
|
||||
/usr/sbin/log2ram \
|
||||
/usr/bin/log2ram
|
||||
do
|
||||
[[ -n "$_l2r_bin" && -f "$_l2r_bin" ]] || continue
|
||||
if grep -q 'rsync -aAXv ' "$_l2r_bin" 2>/dev/null; then
|
||||
cp -a "$_l2r_bin" "${_l2r_bin}.proxmenux.bak"
|
||||
sed -i 's/rsync -aAXv /rsync -aXv --no-acls /g' "$_l2r_bin"
|
||||
fi
|
||||
break
|
||||
done
|
||||
|
||||
# Size-based rotation for the PBS API logs — the upstream package
|
||||
# ships no logrotate rule and pvestatd's local-datastore poll fills
|
||||
# them fast enough to saturate a tmpfs /var/log.
|
||||
if dpkg-query -W -f='${Status}' proxmox-backup-server 2>/dev/null \
|
||||
| grep -q 'install ok installed'; then
|
||||
mkdir -p /var/log/proxmox-backup/api 2>/dev/null || true
|
||||
cat > /etc/logrotate.d/proxmox-backup-api <<'EOF'
|
||||
/var/log/proxmox-backup/api/access.log /var/log/proxmox-backup/api/auth.log {
|
||||
size 20M
|
||||
rotate 3
|
||||
compress
|
||||
delaycompress
|
||||
missingok
|
||||
notifempty
|
||||
copytruncate
|
||||
}
|
||||
EOF
|
||||
chmod 0644 /etc/logrotate.d/proxmox-backup-api
|
||||
chown root:root /etc/logrotate.d/proxmox-backup-api
|
||||
cat > /etc/cron.hourly/proxmox-backup-logrotate <<'EOF'
|
||||
#!/bin/sh
|
||||
/usr/sbin/logrotate /etc/logrotate.d/proxmox-backup-api >/dev/null 2>&1
|
||||
EOF
|
||||
chmod 0755 /etc/cron.hourly/proxmox-backup-logrotate
|
||||
chown root:root /etc/cron.hourly/proxmox-backup-logrotate
|
||||
msg_ok "$(translate "PBS API log rotation configured (hourly, size-based)")"
|
||||
fi
|
||||
|
||||
systemctl enable --now log2ram >/dev/null 2>&1 || true
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
|
||||
@@ -766,13 +854,10 @@ EOF
|
||||
|
||||
cat > /usr/local/bin/log2ram-check.sh <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
# v1.2 — `log2ram write` only copies tmpfs→disk; it does NOT shrink
|
||||
# the tmpfs. When journald or pveproxy/access.log grow past their
|
||||
# limits the tmpfs hit 100% and PVE crashed with "No space left on
|
||||
# device" on Shell open (community-reported: JC Miñarro, Nicolás P.
|
||||
# de A., 17-18/05). We now vacuum the journal and truncate the
|
||||
# non-rotating logs that actually consume the tmpfs before calling
|
||||
# `log2ram write`.
|
||||
# Watch /var/log usage on Log2RAM's tmpfs and act at two thresholds:
|
||||
# > 80% → vacuum journald down to ~30% of SIZE, then log2ram write
|
||||
# > 92% → aggressive: journal to ~5%, rotate PBS API logs if present,
|
||||
# truncate pveproxy/pveam, then log2ram write
|
||||
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
|
||||
CONF_FILE="/etc/log2ram.conf"
|
||||
@@ -793,15 +878,13 @@ LOCK="/run/log2ram-check.lock"
|
||||
exec 9>"$LOCK" 2>/dev/null || exit 0
|
||||
flock -n 9 || exit 0
|
||||
|
||||
# `log2ram write` alone leaves the tmpfs full. Real recovery requires:
|
||||
# (a) journal vacuum — journald respects --vacuum-size unconditionally,
|
||||
# unlike SystemMaxUse which only enforces on rotation boundaries;
|
||||
# (b) truncating logs that aren't rotated by logrotate (pveproxy, pveam);
|
||||
# (c) THEN syncing to disk so the persistent copy reflects reality.
|
||||
if (( USED_BYTES > EMERGENCY_BYTES )); then
|
||||
SAFE_JOURNAL_MB=$(( SIZE_MiB * 5 / 100 ))
|
||||
[[ "$SAFE_JOURNAL_MB" -lt 16 ]] && SAFE_JOURNAL_MB=16
|
||||
journalctl --vacuum-size="${SAFE_JOURNAL_MB}M" >/dev/null 2>&1 || true
|
||||
if [[ -x /usr/sbin/logrotate && -f /etc/logrotate.d/proxmox-backup-api ]]; then
|
||||
/usr/sbin/logrotate -f /etc/logrotate.d/proxmox-backup-api >/dev/null 2>&1 || true
|
||||
fi
|
||||
: > /var/log/pveproxy/access.log 2>/dev/null || true
|
||||
: > /var/log/pveproxy/error.log 2>/dev/null || true
|
||||
: > /var/log/pveam.log 2>/dev/null || true
|
||||
@@ -820,8 +903,7 @@ EOF
|
||||
SHELL=/bin/bash
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
MAILTO=""
|
||||
# Runs every 10 min starting at :03 to avoid overlap with debian-sa1 (:00/:10/:20...)
|
||||
# nice -n 19 + ionice -c 3 ensures minimum CPU/IO priority (no visible spikes)
|
||||
# nice/ionice keep the check off the priority queue for scheduled tasks.
|
||||
3-59/10 * * * * root nice -n 19 ionice -c 3 /usr/local/bin/log2ram-check.sh >/dev/null 2>&1
|
||||
EOF
|
||||
chmod 0644 /etc/cron.d/log2ram-auto-sync
|
||||
@@ -1011,57 +1093,37 @@ enable_zfs_autotrim() {
|
||||
|
||||
|
||||
setup_persistent_network() {
|
||||
local FUNC_VERSION="1.0"
|
||||
local FUNC_VERSION="1.1"
|
||||
# description: Pin NIC names to MAC addresses via systemd .link files so kernel updates don't shuffle interface names.
|
||||
local LINK_DIR="/etc/systemd/network"
|
||||
local BACKUP_DIR="/etc/systemd/network/backup-$(date +%Y%m%d-%H%M%S)"
|
||||
local pve_version
|
||||
pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+' | head -1)
|
||||
|
||||
msg_info "$(translate "Setting up persistent network interfaces")"
|
||||
sleep 2
|
||||
|
||||
# Same legacy-conflict warning as in customizable_post_install.sh.
|
||||
if [[ -f /etc/network/interfaces ]]; then
|
||||
if grep -qE '^[[:space:]]*allow-hotplug[[:space:]]' /etc/network/interfaces 2>/dev/null; then
|
||||
msg_warn "$(translate '/etc/network/interfaces uses allow-hotplug. Renaming interfaces via systemd .link can break that flow — review the file after reboot.')"
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$LINK_DIR"
|
||||
local count=0 removed_stale=0 removed_legacy=0
|
||||
while IFS='=' read -r key value; do
|
||||
case "$key" in
|
||||
COUNT) count="$value" ;;
|
||||
REMOVED_STALE) removed_stale="$value" ;;
|
||||
REMOVED_LEGACY) removed_legacy="$value" ;;
|
||||
esac
|
||||
done < <(pmx_setup_persistent_network)
|
||||
|
||||
if ls "$LINK_DIR"/*.link >/dev/null 2>&1; then
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
cp "$LINK_DIR"/*.link "$BACKUP_DIR"/ 2>/dev/null || true
|
||||
if (( removed_legacy > 0 )); then
|
||||
msg_ok "$(translate "Migrated") $removed_legacy $(translate "legacy .link file(s) to the ProxMenux-managed format")"
|
||||
fi
|
||||
|
||||
local count=0
|
||||
for iface in $(ls /sys/class/net/ | grep -vE "lo|docker|veth|br-|vmbr|tap|fwpr|fwln|virbr|bond|cilium|zt|wg"); do
|
||||
if [[ -e "/sys/class/net/$iface/device" ]] || [[ -e "/sys/class/net/$iface/phy80211" ]]; then
|
||||
local MAC=$(cat /sys/class/net/$iface/address 2>/dev/null)
|
||||
|
||||
if [[ "$MAC" =~ ^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$ ]]; then
|
||||
local LINK_FILE="$LINK_DIR/10-$iface.link"
|
||||
|
||||
cat > "$LINK_FILE" <<EOF
|
||||
[Match]
|
||||
MACAddress=$MAC
|
||||
|
||||
[Link]
|
||||
Name=$iface
|
||||
EOF
|
||||
chmod 644 "$LINK_FILE"
|
||||
((count++))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $count -gt 0 ]]; then
|
||||
if (( removed_stale > 0 )); then
|
||||
msg_ok "$(translate "Reconciled") $removed_stale $(translate "stale entry/entries for interfaces no longer present")"
|
||||
fi
|
||||
if (( count > 0 )); then
|
||||
msg_ok "$(translate "Created persistent names for") $count $(translate "interfaces")"
|
||||
# In PVE9, systemd-networkd is the native network backend and udev processes
|
||||
# .link files directly. Reloading udev rules makes the new .link files effective
|
||||
# immediately for any interface added later (hotplug, new NICs) without waiting
|
||||
# for a full reboot. On PVE8 (ifupdown2), names are resolved at boot anyway.
|
||||
if [[ "$pve_version" -ge 9 ]]; then
|
||||
udevadm control --reload-rules 2>/dev/null || true
|
||||
msg_ok "$(translate "PVE9: udev rules reloaded — new interfaces will get correct names without reboot")"
|
||||
|
||||
@@ -778,7 +778,7 @@ force_apt_ipv4() {
|
||||
|
||||
|
||||
apply_network_optimizations() {
|
||||
local FUNC_VERSION="1.0"
|
||||
local FUNC_VERSION="1.1"
|
||||
# description: Tune TCP buffers, somaxconn, IPv4 hardening and disable rp_filter on fw bridges (PVE 9 compatible).
|
||||
msg_info "$(translate "Optimizing network settings...")"
|
||||
NECESSARY_REBOOT=1
|
||||
@@ -839,6 +839,66 @@ EOF
|
||||
|
||||
sysctl --system > /dev/null 2>&1
|
||||
|
||||
cat > /usr/local/sbin/proxmenux-fwbr-tune <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
# Set rp_filter=0 and log_martians=0 on Proxmox fw bridge interfaces.
|
||||
# No arg → sweep every interface currently under /proc/sys/net/ipv4/conf/.
|
||||
# One arg → tune only that interface (used by the udev rule).
|
||||
set -u
|
||||
|
||||
tune_interface() {
|
||||
local iface="$1"
|
||||
local sysctl_path="/proc/sys/net/ipv4/conf/${iface}"
|
||||
case "$iface" in
|
||||
fwbr*|fwln*|fwpr*|tap*)
|
||||
[[ -d "$sysctl_path" ]] || return 0
|
||||
[[ -w "$sysctl_path/rp_filter" ]] && printf '0\n' > "$sysctl_path/rp_filter"
|
||||
[[ -w "$sysctl_path/log_martians" ]] && printf '0\n' > "$sysctl_path/log_martians"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [[ $# -gt 0 ]]; then
|
||||
tune_interface "$1"
|
||||
else
|
||||
for sysctl_path in /proc/sys/net/ipv4/conf/*; do
|
||||
[[ -d "$sysctl_path" ]] || continue
|
||||
tune_interface "${sysctl_path##*/}"
|
||||
done
|
||||
fi
|
||||
EOF
|
||||
chmod 0755 /usr/local/sbin/proxmenux-fwbr-tune
|
||||
chown root:root /usr/local/sbin/proxmenux-fwbr-tune
|
||||
|
||||
cat > /etc/systemd/system/proxmenux-fwbr-tune.service <<'EOF'
|
||||
[Unit]
|
||||
Description=ProxMenux - Tune rp_filter/log_martians on virtual fw bridges
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/proxmenux-fwbr-tune
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
cat > /etc/udev/rules.d/99-proxmenux-fwbr-tune.rules <<'EOF'
|
||||
ACTION=="add", SUBSYSTEM=="net", KERNEL=="fwbr*", RUN+="/usr/local/sbin/proxmenux-fwbr-tune %k"
|
||||
ACTION=="add", SUBSYSTEM=="net", KERNEL=="fwln*", RUN+="/usr/local/sbin/proxmenux-fwbr-tune %k"
|
||||
ACTION=="add", SUBSYSTEM=="net", KERNEL=="fwpr*", RUN+="/usr/local/sbin/proxmenux-fwbr-tune %k"
|
||||
ACTION=="add", SUBSYSTEM=="net", KERNEL=="tap*", RUN+="/usr/local/sbin/proxmenux-fwbr-tune %k"
|
||||
EOF
|
||||
chmod 0644 /etc/udev/rules.d/99-proxmenux-fwbr-tune.rules
|
||||
chown root:root /etc/udev/rules.d/99-proxmenux-fwbr-tune.rules
|
||||
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
udevadm control --reload-rules >/dev/null 2>&1 || true
|
||||
systemctl enable --now proxmenux-fwbr-tune.service >/dev/null 2>&1 || true
|
||||
/usr/local/sbin/proxmenux-fwbr-tune >/dev/null 2>&1 || true
|
||||
|
||||
|
||||
local interfaces_file="/etc/network/interfaces"
|
||||
if ! grep -q 'source /etc/network/interfaces.d/*' "$interfaces_file"; then
|
||||
@@ -1147,86 +1207,58 @@ EOF
|
||||
|
||||
|
||||
optimize_zfs_arc() {
|
||||
local FUNC_VERSION="1.0"
|
||||
# description: Cap ZFS ARC to a sensible fraction of host RAM so VMs don't fight the kernel for memory.
|
||||
msg_info2 "$(translate "Optimizing ZFS ARC size according to available memory...")"
|
||||
local FUNC_VERSION="1.1"
|
||||
# description: Cap ZFS ARC max to a sensible fraction of host RAM so VMs don't fight the kernel for memory. Only sets zfs_arc_max; other OpenZFS tunables stay at their defaults.
|
||||
local zfs_conf="/etc/modprobe.d/99-zfsarc.conf"
|
||||
local ram_bytes arc_max
|
||||
|
||||
# Check if ZFS is installed
|
||||
if ! command -v zfs > /dev/null; then
|
||||
msg_info2 "$(translate "Optimizing ZFS ARC maximum size...")"
|
||||
|
||||
if ! command -v zpool >/dev/null 2>&1; then
|
||||
msg_warn "$(translate "ZFS not detected. Skipping ZFS ARC optimization.")"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Ensure RAM_SIZE_GB is set
|
||||
if [ -z "$RAM_SIZE_GB" ]; then
|
||||
RAM_SIZE_GB=$(free -g | awk '/^Mem:/{print $2}')
|
||||
if [ -z "$RAM_SIZE_GB" ] || [ "$RAM_SIZE_GB" -eq 0 ]; then
|
||||
msg_warn "$(translate "Failed to detect RAM size. Using default value of 16GB for ZFS ARC optimization.")"
|
||||
RAM_SIZE_GB=16 # Default to 16GB if detection fails
|
||||
fi
|
||||
if ! zpool list -H -o name 2>/dev/null | grep -q .; then
|
||||
msg_warn "$(translate "No ZFS pools detected. Skipping ZFS ARC optimization.")"
|
||||
return 0
|
||||
fi
|
||||
|
||||
msg_ok "$(translate "Detected RAM size: ${RAM_SIZE_GB} GB")"
|
||||
ram_bytes=$(awk '/MemTotal:/ { print $2 * 1024 }' /proc/meminfo)
|
||||
if [[ -z "$ram_bytes" || "$ram_bytes" -le 0 ]]; then
|
||||
msg_error "$(translate "Unable to determine the installed memory.")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Calculate ZFS ARC sizes
|
||||
if [[ "$RAM_SIZE_GB" -le 16 ]]; then
|
||||
MY_ZFS_ARC_MIN=536870911 # 512MB
|
||||
MY_ZFS_ARC_MAX=536870912 # 512MB
|
||||
elif [[ "$RAM_SIZE_GB" -le 32 ]]; then
|
||||
MY_ZFS_ARC_MIN=1073741823 # 1GB
|
||||
MY_ZFS_ARC_MAX=1073741824 # 1GB
|
||||
if (( ram_bytes <= 16 * 1024 * 1024 * 1024 )); then
|
||||
arc_max=$((512 * 1024 * 1024))
|
||||
elif (( ram_bytes <= 32 * 1024 * 1024 * 1024 )); then
|
||||
arc_max=$((1024 * 1024 * 1024))
|
||||
else
|
||||
# Use 1/16 of RAM for min and 1/8 for max
|
||||
MY_ZFS_ARC_MIN=$((RAM_SIZE_GB * 1073741824 / 16))
|
||||
MY_ZFS_ARC_MAX=$((RAM_SIZE_GB * 1073741824 / 8))
|
||||
arc_max=$((ram_bytes / 8))
|
||||
fi
|
||||
(( arc_max < 512 * 1024 * 1024 )) && arc_max=$((512 * 1024 * 1024))
|
||||
|
||||
if [[ -f "$zfs_conf" && ! -f "${zfs_conf}.bak" ]]; then
|
||||
cp -p "$zfs_conf" "${zfs_conf}.bak"
|
||||
fi
|
||||
|
||||
# Enforce the minimum values
|
||||
MY_ZFS_ARC_MIN=$((MY_ZFS_ARC_MIN > 536870911 ? MY_ZFS_ARC_MIN : 536870911))
|
||||
MY_ZFS_ARC_MAX=$((MY_ZFS_ARC_MAX > 536870912 ? MY_ZFS_ARC_MAX : 536870912))
|
||||
|
||||
# Apply ZFS tuning parameters
|
||||
local zfs_conf="/etc/modprobe.d/99-zfsarc.conf"
|
||||
local config_changed=false
|
||||
|
||||
if [ -f "$zfs_conf" ]; then
|
||||
msg_info "$(translate "Checking existing ZFS ARC configuration...")"
|
||||
if ! grep -q "zfs_arc_min=$MY_ZFS_ARC_MIN" "$zfs_conf" || \
|
||||
! grep -q "zfs_arc_max=$MY_ZFS_ARC_MAX" "$zfs_conf"; then
|
||||
msg_ok "$(translate "Changes detected. Updating ZFS ARC configuration...")"
|
||||
cp "$zfs_conf" "${zfs_conf}.bak"
|
||||
config_changed=true
|
||||
else
|
||||
msg_ok "$(translate "ZFS ARC configuration is up to date")"
|
||||
fi
|
||||
else
|
||||
msg_info "$(translate "Creating new ZFS ARC configuration...")"
|
||||
config_changed=true
|
||||
fi
|
||||
|
||||
if $config_changed; then
|
||||
cat <<EOF > "$zfs_conf"
|
||||
# ZFS tuning
|
||||
# Use 1/8 RAM for MAX cache, 1/16 RAM for MIN cache, or 512MB/1GB for systems with <= 32GB RAM
|
||||
options zfs zfs_arc_min=$MY_ZFS_ARC_MIN
|
||||
options zfs zfs_arc_max=$MY_ZFS_ARC_MAX
|
||||
|
||||
# Enable prefetch method
|
||||
options zfs l2arc_noprefetch=0
|
||||
|
||||
# Set max write speed to L2ARC (500MB)
|
||||
options zfs l2arc_write_max=524288000
|
||||
options zfs zfs_txg_timeout=60
|
||||
cat > "$zfs_conf" <<EOF
|
||||
# ProxMenux ZFS ARC configuration
|
||||
# Only zfs_arc_max is set; zfs_arc_min stays at the OpenZFS default (auto).
|
||||
options zfs zfs_arc_max=$arc_max
|
||||
EOF
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
msg_ok "$(translate "ZFS ARC configuration file created/updated successfully")"
|
||||
NECESSARY_REBOOT=1
|
||||
else
|
||||
msg_error "$(translate "Failed to create/update ZFS ARC configuration file")"
|
||||
fi
|
||||
msg_info "$(translate "Updating initramfs so the ARC cap applies at next boot...")"
|
||||
if ! update-initramfs -u -k all >/dev/null 2>&1; then
|
||||
msg_error "$(translate "Failed to update initramfs.")"
|
||||
return 1
|
||||
fi
|
||||
if command -v proxmox-boot-tool >/dev/null 2>&1; then
|
||||
proxmox-boot-tool refresh >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
NECESSARY_REBOOT=1
|
||||
msg_ok "$(translate "ZFS ARC maximum configured:") $arc_max $(translate "bytes")"
|
||||
msg_success "$(translate "ZFS ARC optimization completed")"
|
||||
register_tool "zfs_arc" true "$FUNC_VERSION"
|
||||
}
|
||||
@@ -2493,7 +2525,7 @@ update_pve_appliance_manager() {
|
||||
|
||||
|
||||
configure_log2ram() {
|
||||
local FUNC_VERSION="1.2"
|
||||
local FUNC_VERSION="1.3"
|
||||
# description: Install Log2RAM with user-chosen RAM size; prompts for size and SSD/M.2 awareness before applying.
|
||||
msg_info2 "$(translate "Preparing Log2RAM configuration")"
|
||||
sleep 1
|
||||
@@ -2591,6 +2623,52 @@ configure_log2ram() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Drop ACL preservation from the upstream rsync call: some
|
||||
# /var/log.hdd filesystems reject POSIX ACLs and log2ram write
|
||||
# exits 23 with `set_acl: Operation not supported`. xattrs stay.
|
||||
local _l2r_bin
|
||||
for _l2r_bin in \
|
||||
"$(command -v log2ram 2>/dev/null)" \
|
||||
/usr/local/bin/log2ram \
|
||||
/usr/sbin/log2ram \
|
||||
/usr/bin/log2ram
|
||||
do
|
||||
[[ -n "$_l2r_bin" && -f "$_l2r_bin" ]] || continue
|
||||
if grep -q 'rsync -aAXv ' "$_l2r_bin" 2>/dev/null; then
|
||||
cp -a "$_l2r_bin" "${_l2r_bin}.proxmenux.bak"
|
||||
sed -i 's/rsync -aAXv /rsync -aXv --no-acls /g' "$_l2r_bin"
|
||||
fi
|
||||
break
|
||||
done
|
||||
|
||||
# Size-based rotation for the PBS API logs — the upstream package
|
||||
# ships no logrotate rule and pvestatd's local-datastore poll fills
|
||||
# them fast enough to saturate a tmpfs /var/log.
|
||||
if dpkg-query -W -f='${Status}' proxmox-backup-server 2>/dev/null \
|
||||
| grep -q 'install ok installed'; then
|
||||
mkdir -p /var/log/proxmox-backup/api 2>/dev/null || true
|
||||
cat > /etc/logrotate.d/proxmox-backup-api <<'EOF'
|
||||
/var/log/proxmox-backup/api/access.log /var/log/proxmox-backup/api/auth.log {
|
||||
size 20M
|
||||
rotate 3
|
||||
compress
|
||||
delaycompress
|
||||
missingok
|
||||
notifempty
|
||||
copytruncate
|
||||
}
|
||||
EOF
|
||||
chmod 0644 /etc/logrotate.d/proxmox-backup-api
|
||||
chown root:root /etc/logrotate.d/proxmox-backup-api
|
||||
cat > /etc/cron.hourly/proxmox-backup-logrotate <<'EOF'
|
||||
#!/bin/sh
|
||||
/usr/sbin/logrotate /etc/logrotate.d/proxmox-backup-api >/dev/null 2>&1
|
||||
EOF
|
||||
chmod 0755 /etc/cron.hourly/proxmox-backup-logrotate
|
||||
chown root:root /etc/cron.hourly/proxmox-backup-logrotate
|
||||
msg_ok "$(translate "PBS API log rotation configured (hourly, size-based)")"
|
||||
fi
|
||||
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
systemctl enable --now log2ram >/dev/null 2>&1 || true
|
||||
|
||||
@@ -2620,13 +2698,10 @@ EOF
|
||||
if [[ "$ENABLE_AUTOSYNC" == true ]]; then
|
||||
cat > /usr/local/bin/log2ram-check.sh <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
# v1.2 — `log2ram write` only copies tmpfs→disk; it does NOT shrink
|
||||
# the tmpfs. When journald or pveproxy/access.log grow past their
|
||||
# limits the tmpfs hit 100% and PVE crashed with "No space left on
|
||||
# device" on Shell open (community-reported: JC Miñarro, Nicolás P.
|
||||
# de A., 17-18/05). We now vacuum the journal and truncate the
|
||||
# non-rotating logs that actually consume the tmpfs before calling
|
||||
# `log2ram write`.
|
||||
# Watch /var/log usage on Log2RAM's tmpfs and act at two thresholds:
|
||||
# > 80% → vacuum journald down to ~30% of SIZE, then log2ram write
|
||||
# > 92% → aggressive: journal to ~5%, rotate PBS API logs if present,
|
||||
# truncate pveproxy/pveam, then log2ram write
|
||||
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
CONF_FILE="/etc/log2ram.conf"
|
||||
L2R_BIN="$(command -v log2ram || true)"
|
||||
@@ -2646,15 +2721,13 @@ LOCK="/run/log2ram-check.lock"
|
||||
exec 9>"$LOCK" 2>/dev/null || exit 0
|
||||
flock -n 9 || exit 0
|
||||
|
||||
# `log2ram write` alone leaves the tmpfs full. Real recovery requires:
|
||||
# (a) journal vacuum — journald respects --vacuum-size unconditionally,
|
||||
# unlike SystemMaxUse which only enforces on rotation boundaries;
|
||||
# (b) truncating logs that aren't rotated by logrotate (pveproxy, pveam);
|
||||
# (c) THEN syncing to disk so the persistent copy reflects reality.
|
||||
if (( USED_BYTES > EMERGENCY_BYTES )); then
|
||||
SAFE_JOURNAL_MB=$(( SIZE_MiB * 5 / 100 ))
|
||||
[[ "$SAFE_JOURNAL_MB" -lt 16 ]] && SAFE_JOURNAL_MB=16
|
||||
journalctl --vacuum-size="${SAFE_JOURNAL_MB}M" >/dev/null 2>&1 || true
|
||||
if [[ -x /usr/sbin/logrotate && -f /etc/logrotate.d/proxmox-backup-api ]]; then
|
||||
/usr/sbin/logrotate -f /etc/logrotate.d/proxmox-backup-api >/dev/null 2>&1 || true
|
||||
fi
|
||||
: > /var/log/pveproxy/access.log 2>/dev/null || true
|
||||
: > /var/log/pveproxy/error.log 2>/dev/null || true
|
||||
: > /var/log/pveam.log 2>/dev/null || true
|
||||
@@ -2763,64 +2836,37 @@ EOF
|
||||
|
||||
|
||||
setup_persistent_network() {
|
||||
local FUNC_VERSION="1.0"
|
||||
local FUNC_VERSION="1.1"
|
||||
# description: Pin NIC names to MAC addresses via systemd .link files so kernel updates don't shuffle interface names.
|
||||
local LINK_DIR="/etc/systemd/network"
|
||||
local BACKUP_DIR="/etc/systemd/network/backup-$(date +%Y%m%d-%H%M%S)"
|
||||
local pve_version
|
||||
pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+' | head -1)
|
||||
|
||||
msg_info "$(translate "Setting up persistent network interfaces")"
|
||||
sleep 2
|
||||
|
||||
# Detect legacy `/etc/network/interfaces` setups that depend on the
|
||||
# default udev naming. If the file references `allow-hotplug` rules
|
||||
# or uses physical interface names directly, the .link rules below
|
||||
# could rename interfaces and break network on reboot. Warn loudly so
|
||||
# the user can review before continuing. Audit Tier 6 —
|
||||
# `setup_persistent_network` no detecta conflicto con legacy.
|
||||
if [[ -f /etc/network/interfaces ]]; then
|
||||
if grep -qE '^[[:space:]]*allow-hotplug[[:space:]]' /etc/network/interfaces 2>/dev/null; then
|
||||
msg_warn "$(translate '/etc/network/interfaces uses allow-hotplug. Renaming interfaces via systemd .link can break that flow — review the file after reboot.')"
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$LINK_DIR"
|
||||
local count=0 removed_stale=0 removed_legacy=0
|
||||
while IFS='=' read -r key value; do
|
||||
case "$key" in
|
||||
COUNT) count="$value" ;;
|
||||
REMOVED_STALE) removed_stale="$value" ;;
|
||||
REMOVED_LEGACY) removed_legacy="$value" ;;
|
||||
esac
|
||||
done < <(pmx_setup_persistent_network)
|
||||
|
||||
|
||||
if ls "$LINK_DIR"/*.link >/dev/null 2>&1; then
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
cp "$LINK_DIR"/*.link "$BACKUP_DIR"/ 2>/dev/null || true
|
||||
if (( removed_legacy > 0 )); then
|
||||
msg_ok "$(translate "Migrated") $removed_legacy $(translate "legacy .link file(s) to the ProxMenux-managed format")"
|
||||
fi
|
||||
|
||||
# Process physical interfaces
|
||||
local count=0
|
||||
for iface in $(ls /sys/class/net/ | grep -vE "lo|docker|veth|br-|vmbr|tap|fwpr|fwln|virbr|bond|cilium|zt|wg"); do
|
||||
if [[ -e "/sys/class/net/$iface/device" ]] || [[ -e "/sys/class/net/$iface/phy80211" ]]; then
|
||||
local MAC=$(cat /sys/class/net/$iface/address 2>/dev/null)
|
||||
|
||||
if [[ "$MAC" =~ ^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$ ]]; then
|
||||
local LINK_FILE="$LINK_DIR/10-$iface.link"
|
||||
|
||||
cat > "$LINK_FILE" <<EOF
|
||||
[Match]
|
||||
MACAddress=$MAC
|
||||
|
||||
[Link]
|
||||
Name=$iface
|
||||
EOF
|
||||
chmod 644 "$LINK_FILE"
|
||||
((count++))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $count -gt 0 ]]; then
|
||||
if (( removed_stale > 0 )); then
|
||||
msg_ok "$(translate "Reconciled") $removed_stale $(translate "stale entry/entries for interfaces no longer present")"
|
||||
fi
|
||||
if (( count > 0 )); then
|
||||
msg_ok "$(translate "Created persistent names for") $count $(translate "interfaces")"
|
||||
# In PVE9, systemd-networkd is the native network backend and udev processes
|
||||
# .link files directly. Reloading udev rules makes the new .link files effective
|
||||
# immediately for any interface added later (hotplug, new NICs) without waiting
|
||||
# for a full reboot. On PVE8 (ifupdown2), names are resolved at boot anyway.
|
||||
if [[ "$pve_version" -ge 9 ]]; then
|
||||
udevadm control --reload-rules 2>/dev/null || true
|
||||
msg_ok "$(translate "PVE9: udev rules reloaded — new interfaces will get correct names without reboot")"
|
||||
|
||||
@@ -455,11 +455,14 @@ uninstall_network_optimization() {
|
||||
|
||||
systemctl disable --now proxmenux-fwbr-tune.service >/dev/null 2>&1 || true
|
||||
rm -f /etc/systemd/system/proxmenux-fwbr-tune.service
|
||||
rm -f /usr/local/sbin/proxmenux-fwbr-tune
|
||||
rm -f /etc/udev/rules.d/99-proxmenux-fwbr-tune.rules
|
||||
udevadm control --reload-rules >/dev/null 2>&1 || true
|
||||
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
sysctl --system >/dev/null 2>&1 || true
|
||||
|
||||
|
||||
|
||||
msg_ok "$(translate "Network optimizations removed")"
|
||||
register_tool "network_optimization" false
|
||||
}
|
||||
@@ -573,22 +576,28 @@ uninstall_log2ram() {
|
||||
################################################################
|
||||
|
||||
uninstall_persistent_network() {
|
||||
local LINK_DIR="/etc/systemd/network"
|
||||
|
||||
msg_info "$(translate "Removing all .link files from") $LINK_DIR"
|
||||
sleep 2
|
||||
|
||||
if ! ls "$LINK_DIR"/*.link >/dev/null 2>&1; then
|
||||
msg_warn "$(translate "No .link files found in") $LINK_DIR"
|
||||
return 0
|
||||
msg_info "$(translate "Removing ProxMenux persistent NIC .link files...")"
|
||||
sleep 1
|
||||
|
||||
if ! type pmx_uninstall_persistent_network &>/dev/null; then
|
||||
if [[ -f "$LOCAL_SCRIPTS/global/utils-install-functions.sh" ]]; then
|
||||
source "$LOCAL_SCRIPTS/global/utils-install-functions.sh"
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -f "$LINK_DIR"/*.link
|
||||
local removed=0
|
||||
while IFS='=' read -r key value; do
|
||||
[[ "$key" == "REMOVED" ]] && removed="$value"
|
||||
done < <(pmx_uninstall_persistent_network)
|
||||
|
||||
msg_ok "$(translate "Removed all .link files from") $LINK_DIR"
|
||||
msg_info "$(translate "Interface names will return to default systemd behavior.")"
|
||||
if (( removed > 0 )); then
|
||||
msg_ok "$(translate "Removed") $removed $(translate "ProxMenux-managed .link file(s). User-authored .link files were left in place.")"
|
||||
msg_info "$(translate "Interface names will return to default systemd behavior.")"
|
||||
NECESSARY_REBOOT=1
|
||||
else
|
||||
msg_warn "$(translate "No ProxMenux-managed .link files found — nothing to remove.")"
|
||||
fi
|
||||
register_tool "persistent_network" false
|
||||
NECESSARY_REBOOT=1
|
||||
}
|
||||
|
||||
|
||||
@@ -906,6 +915,10 @@ uninstall_zfs_arc() {
|
||||
rm -f /etc/modprobe.d/99-zfsarc.conf
|
||||
msg_ok "$(translate 'ZFS ARC config removed (kernel defaults will apply on reboot)')"
|
||||
fi
|
||||
update-initramfs -u -k all >/dev/null 2>&1 || true
|
||||
if command -v proxmox-boot-tool >/dev/null 2>&1; then
|
||||
proxmox-boot-tool refresh >/dev/null 2>&1 || true
|
||||
fi
|
||||
register_tool "zfs_arc" false
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Nueva versión ProxMenux v1.2.4
|
||||
|
||||
Esta versión suma dos mejoras visibles en el propio dashboard — un botón para lanzar la actualización de Proxmox desde el Monitor de Salud, y un prompt para invitar a los usuarios móviles a instalar el Monitor como PWA — y cierra siete correcciones de contenido y entrega de notificaciones, el bloqueo del dashboard móvil sobre HTTPS + reverse proxy.
|
||||
Esta versión suma dos mejoras visibles en el propio dashboard — un botón para lanzar la actualización de Proxmox desde el Monitor de Salud, y un prompt para invitar a los usuarios móviles a instalar el Monitor como PWA — extiende el flujo de restauración de Backups con importación automática de pools ZFS de datos, refina el comportamiento de Log2RAM en hosts que corren Proxmox Backup Server como servicio, refuerza el ajuste de sysctl de los bridges del firewall en todo el ciclo de vida de VMs, ajusta la optimización de ZFS ARC a su propio ámbito, hace idempotentes los nombres persistentes de NIC en re-ejecuciones, recompila automáticamente los drivers DKMS cuando entra un kernel nuevo, mantiene intacta la sesión de la terminal del Monitor cuando hay update de ProxMenux disponible, y afina cinco plantillas de notificaciones y tres chequeos del panel de salud.
|
||||
---
|
||||
|
||||
## 🩺 Botón Update Now en el Monitor de Salud
|
||||
@@ -19,49 +19,113 @@ Esta versión suma dos mejoras visibles en el propio dashboard — un botón par
|
||||
|
||||
## 📱 Prompt de instalación en la app para móvil
|
||||
|
||||
- Los visitantes por primera vez en **Android** (Chrome / Brave) e **iOS Safari** ven ahora una bottom-sheet con instrucciones claras para añadir el Monitor a su pantalla de inicio como PWA.
|
||||
- Los visitantes por primera vez en **Android** (Chrome / Brave / Edge / Samsung Internet) e **iOS Safari** ven ahora una bottom-sheet con instrucciones claras para añadir el Monitor a su pantalla de inicio como PWA.
|
||||
- En Android, cuando el navegador considera el Monitor instalable y dispara `beforeinstallprompt` (Chromium 89+ lo entrega solo con manifest válido, sin necesidad de Service Worker), un botón **Install** en la bottom-sheet dispara el flujo nativo de instalación con un solo tap; el propio banner de instalación in-page del navegador se suprime para que la bottom-sheet sea el camino principal. Las instrucciones manuales de "menú del navegador → Añadir a pantalla de inicio" siguen mostrándose como fallback para navegadores que no disparan el evento (Firefox Android) o casos en que el operador prefiere la ruta manual.
|
||||
- iOS Safari mantiene las instrucciones manuales de 3 pasos — no existe una API JS equivalente en iOS.
|
||||
- La bottom-sheet se cierra automáticamente cuando el navegador dispara `appinstalled` — no queda ningún prompt colgado tras la instalación en la pantalla de inicio.
|
||||
- Dos niveles de descarte: **Not now** (temporal, reaparece a los 30 días) y **Don't show again** (permanente, almacenado en `localStorage`).
|
||||
- No se muestra en escritorio, ni cuando el Monitor ya se está ejecutando como aplicación instalada.
|
||||
|
||||
---
|
||||
|
||||
## 🔔 Contenido de notificaciones — cinco correcciones de renderizado
|
||||
## 🔔 Contenido de notificaciones — cinco refinamientos de renderizado
|
||||
|
||||
- **Destino de backup ahora visible** — los correos y mensajes de Telegram de backup de VM/CT incluyen el storage / destino PBS tanto en el título como en la tabla del cuerpo, para que los operadores con varios destinos de backup puedan saber de un vistazo cuál funcionó o falló.
|
||||
- **Los cuerpos de migración ya no muestran `migrated to node .`** — el nodo destino real se extrae del log de la tarea PVE para eventos `qmigrate` / `vzmigrate`.
|
||||
- **Los cuerpos de snapshot ya no muestran `Snapshot "" created`** — el nombre real del snapshot se extrae del log de la tarea PVE para `qmsnapshot` / `vzsnapshot`.
|
||||
- **Las notificaciones genéricas `system_problem` llevan la razón real** — los mensajes PVE que caen en el bucket genérico incluyen el cuerpo original del payload, reemplazando el inútil *"Se ha detectado un problema a nivel del sistema."*
|
||||
- **Los correos de actualización de driver NVIDIA / Coral muestran la nueva versión** — la fila *New Version* en el cuerpo HTML aparecía vacía por un desajuste de nombres entre el template y el renderer (`latest_version` vs `new_version`). Corregido.
|
||||
- **Destino de backup en título y cuerpo.** Los correos y mensajes de Telegram de backup de VM/CT llevan el storage / destino PBS, de modo que en instalaciones con varios destinos se identifica de un vistazo qué backup produjo el evento.
|
||||
- **Los cuerpos de migración llevan el nodo destino real** — extraído del log de la tarea PVE para eventos `qmigrate` / `vzmigrate`.
|
||||
- **Los cuerpos de snapshot llevan el nombre real del snapshot** — extraído del log de la tarea PVE para eventos `qmsnapshot` / `vzsnapshot`.
|
||||
- **Las notificaciones genéricas `system_problem` incluyen la razón real** — el cuerpo del payload de PVE se surface como cuerpo de la notificación.
|
||||
- **Los correos de actualización de driver NVIDIA / Coral renderizan correctamente la fila *New Version*** — el placeholder del template está ahora alineado con el campo que lee el renderer.
|
||||
|
||||
---
|
||||
|
||||
## 🩹 Correcciones del panel de salud
|
||||
## 🩹 Panel de salud — tres chequeos reforzados
|
||||
|
||||
- **Dismiss silencioso sobre alertas de storage** — al pulsar Dismiss en un error tipo `storage_unavailable`, `mount_stale`, `mount_readonly`, `lxc_disk_low`, `lxc_mount_low`, `pve_storage_full` o `zfs_pool_full`, la acción se persistía en la DB pero el caché del Monitor no se invalidaba porque el evento se catalogaba como categoría `general` en lugar de `storage`. Resultado: el error seguía visible tras el descarte. Corregidos ambos, la inferencia de categoría y el mapa de invalidación de caché.
|
||||
- **VMs & Containers atascado en `UNKNOWN` con `'NoneType' object has no attribute 'get'`** (#255) — cualquier error persistido con la columna `details` a `NULL` en la DB provocaba que el chequeo completo de VM/CT lanzara excepción en cada scan. La categoría se quedaba UNKNOWN permanentemente y el Dismiss no podía silenciarla porque no había un `error_key` específico al que reaccionar. Corregido con coalescing explícito `error.get('details') or {}` en los dos bucles de persistencia.
|
||||
- **Notificaciones `system_startup` duplicadas en cada tick de polling** — `_check_startup_aggregation` emitía el resumen de arranque pero no marcaba la agregación como completada, así que en la siguiente iteración de polling el mismo evento se re-emitía indefinidamente hasta reiniciar el servicio. En un host que se mantiene arrancado durante horas tras el boot esto producía hasta un duplicado por cada intervalo de polling. Ahora el flag de agregación se marca justo después de encolar la notificación.
|
||||
- **Dismiss silencia ahora las alertas de storage.** El flujo de acknowledge cubre `storage_unavailable`, `mount_stale`, `mount_readonly`, `lxc_disk_low`, `lxc_mount_low`, `pve_storage_full` y `zfs_pool_full` bajo la categoría `storage`, y el caché de storage se invalida al pulsar Dismiss para que el panel se refresque inmediatamente.
|
||||
- **El chequeo de VMs & Containers tolera errores persistidos con la columna `details` a NULL** (#255). `_check_vms_cts_with_persistence` coalesciona un `details` ausente a un dict vacío antes de leer claves anidadas, de modo que una fila sparse suelta ya no deja el chequeo completo fuera de servicio.
|
||||
- **La notificación `system_startup` se dispara una sola vez por boot.** `_check_startup_aggregation` marca la agregación como completada justo después de encolar el evento, así que el resumen de arranque llega una única vez independientemente de cuántos ticks de polling entren en la sesión.
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Correcciones en móvil y webhook
|
||||
## 🛠 Móvil y webhook
|
||||
|
||||
- **Dashboard congelado en móvil sobre HTTPS + reverse proxy** — el Service Worker introducido en v1.2.3 (`sw.js`, necesario para la instalabilidad PWA) interactuaba con la limitación en segundo plano de los navegadores móviles para ahorrar batería y hacía que las peticiones de polling dejaran de dispararse tras un refresh. Los valores del dashboard (CPU / RAM / temperatura) se congelaban en la última lectura hasta que se limpiara la caché manualmente. Corregido auto-limpiando cualquier Service Worker registrado al cargar. La instalabilidad PWA se gestiona ahora íntegramente desde el nuevo prompt in-app descrito arriba.
|
||||
- **Autenticación de webhook extendida a IPs de VPN / CGNAT** — el webhook interno (`/api/notifications/webhook`) confiaba anteriormente solo en peticiones desde `127.0.0.1` / `::1`. Los usuarios con un FQDN de Tailscale, WireGuard, LAN o IPv6 apuntando al host veían botones Test de PVE devolver `401 missing_timestamp`. El webhook ahora trata cualquier IP de una interfaz local del host como local-loopback de confianza, incluyendo la forma IPv4 mapeada en IPv6 (`::ffff:100.x.x.x`) que Flask reporta en bindings dual-stack.
|
||||
- **El polling del dashboard en móvil se mantiene vivo sobre HTTPS + reverse proxy.** `pwa-register.tsx` auto-desregistra cualquier Service Worker al cargar, así que la limitación en segundo plano de los navegadores móviles deja de interferir con los fetch de polling, y la instalabilidad PWA se gestiona ahora desde el nuevo prompt in-app descrito arriba.
|
||||
- **La autenticación del webhook confía en todas las IPs locales del host.** El webhook interno (`/api/notifications/webhook`) acepta peticiones desde cualquier IP de una interfaz que el host posee (Tailscale, WireGuard, LAN, IPv6, más la forma IPv4 mapeada en IPv6 `::ffff:x.x.x.x` que Flask reporta en bindings dual-stack), de modo que los botones Test de PVE funcionan a través de cualquiera de ellas.
|
||||
|
||||
---
|
||||
|
||||
## 🛡 Flujo de actualización — El prompt de update de ProxMenux ahora conoce la terminal del Monitor
|
||||
|
||||
- **La terminal WebSocket del Monitor expone ahora `PROXMENUX_TERMINAL=monitor`** en el entorno de cada shell que abre, y cada proceso hijo lo hereda. Esto le da a `menu` (y a cualquier cosa que le importe) una vía fiable y determinista para saber que la sesión actual vive dentro del proceso del Monitor — una sesión que quedaría cortada a mitad de instalación si el servicio del Monitor se reiniciara.
|
||||
- **Cuando `menu` arranca y detecta que hay una nueva versión de ProxMenux Y la sesión corre dentro de la terminal del Monitor, el clásico prompt yes/no de update se sustituye por un msgbox informativo.** El msgbox nombra la nueva versión y muestra el comando de una línea para ejecutar el update desde SSH o la consola del host Proxmox. Como el flow ya ha decidido que el path de update in-terminal es inseguro, hay un solo botón OK — no hay yes/no que pudiera disparar el update destructivo por accidente.
|
||||
- **Tras pulsar OK, `menu` continúa con normalidad.** El operador sigue usando ProxMenux desde la misma terminal sin restricciones; solo el update en sí queda enrutado a otro sitio. No hay bloqueo ni acción forzada.
|
||||
- **Las sesiones SSH, la consola del host Proxmox y cualquier entorno donde `PROXMENUX_TERMINAL` no sea `monitor` mantienen el prompt yes/no anterior** y pueden actualizar como siempre. El cambio solo afecta al caso en el que actualizar en el sitio rompería la sesión activa.
|
||||
- **Nota de bootstrap**: como `PROXMENUX_TERMINAL=monitor` lo añade el AppImage que ship-ea esta release, el guard solo empieza a proteger sesiones una vez que el host está en 1.2.4 o posterior. La primera actualización a 1.2.4, si se dispara desde la terminal del Monitor, aún puede caer en el comportamiento antiguo — a partir de 1.2.4 el guard queda en su sitio.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Flujo de actualización — Drivers DKMS recompilados cuando entra un kernel nuevo
|
||||
|
||||
- **Cuando `apt full-upgrade` deja preparado un kernel más nuevo que el que está en marcha, `update-pve-safe.sh` recompila ahora los drivers DKMS instalados por ProxMenux contra ese nuevo kernel.** El botón Update Now del Monitor de Salud y la utilidad CLI `utilities/proxmox_update.sh` delegan ambos en `update-pve-safe.sh`, así que las dos vías ganan el comportamiento. El paso lee `components_status.json`, cruza los componentes DKMS que ProxMenux gestiona (`nvidia_driver`, `coral_driver`), instala las cabeceras de kernel correspondientes (`proxmox-headers-<newkver>` o `pve-headers-<newkver>`) si no están ya presentes, y ejecuta `dkms autoinstall -k <newkver>`. Después verifica vía `dkms status` que cada módulo esperado (`gasket` para Coral, `nvidia` para el driver NVIDIA) haya alcanzado el estado `installed` para el nuevo kernel — si alguno no lo hizo, cae al camino `--auto-reinstall` de cada instalador.
|
||||
- **Un whiptail msgbox anuncia la recompilación antes de ejecutarla.** Un solo botón OK — no hay yes/no. Nombra el kernel entrante y lista los componentes DKMS que se van a recompilar, de modo que el operador ve exactamente qué está a punto de suceder. Dado que dejar los drivers DKMS sin recompilar dejaría el sistema con un kernel funcional pero TPU / GPU no operativos al arrancar, se trata de transparencia, no de decisión — al pulsar OK se reconoce el trabajo posterior y el flujo procede. Las invocaciones no interactivas (cron, batch sin terminal, sin whiptail) omiten el msgbox y registran la misma información.
|
||||
- **Solo se consideran componentes registrados como `installed` en `components_status.json`.** Un host sin drivers DKMS gestionados por ProxMenux no ve msgbox ni paso de recompilación. Los hosts que nunca ejecutaron el instalador de Coral o NVIDIA no se ven afectados.
|
||||
- **Si la recompilación falla, no se aborta la actualización.** Si un módulo DKMS no puede recompilarse contra el nuevo kernel (ruptura de API upstream, dependencia faltante), el flujo de actualización termina normalmente, se nombran los componentes concretos que fallaron en el resumen, y el operador puede re-ejecutar su instalador a mano tras el reboot. El paso es best-effort por diseño — un desajuste kernel/driver es un problema upstream, no algo que el flujo de actualización deba tratar como fallo.
|
||||
- El helper compartido `pmx_rebuild_dkms_after_kernel` vive en `scripts/global/utils-install-functions.sh`, de modo que futuros actualizadores o utilidades CLI pueden reutilizarlo con una única llamada.
|
||||
|
||||
---
|
||||
|
||||
## 🔌 Post-install — Nombres persistentes de NIC ahora idempotentes
|
||||
|
||||
- **Los ficheros `.link` gestionados por ProxMenux llevan ahora un prefijo distintivo y un marcador interno.** Se escriben como `10-proxmenux-<iface>.link` y la primera línea de cada fichero es `# Managed by ProxMenux — do not edit`. Ambos se comprueban en los pasos de reconciliación y desinstalación antes de tocar un fichero, de modo que cualquier cosa que el operador haya escrito a mano o que venga de otro paquete queda a salvo.
|
||||
- **Cada re-ejecución de `setup_persistent_network` reconcilia las entradas de ProxMenux.** En cada invocación se recorren los ficheros `10-proxmenux-*.link` existentes, se extrae el valor `MACAddress=`, se compara con las MAC actualmente presentes en `/sys/class/net/`, y se eliminan únicamente las entradas propiedad de ProxMenux cuya MAC ya no está. Reemplazos de tarjetas, cambios de NIC y migraciones de hardware dejan de acumular mapeos huérfanos en cada re-ejecución.
|
||||
- **Los ficheros con formato 1.0 (`10-<iface>.link`, escritos por la versión anterior) se migran automáticamente en la primera ejecución de la nueva función.** Si el fichero coincide con la plantilla exacta que usaba el código 1.0 (dos secciones, `MACAddress=` + `Name=`, nada más), se elimina y se reemplaza por el `10-proxmenux-<iface>.link` en un solo paso. Cualquier fichero que no coincida con esa plantilla exacta se deja intacto.
|
||||
- **El desinstalador (`uninstall_persistent_network`) elimina ahora solamente ficheros que cumplan a la vez el prefijo `10-proxmenux-` y el marcador en la primera línea.** El anterior barrido `rm -f /etc/systemd/network/*.link` desaparece — los `.link` escritos por el usuario permanecen en su sitio con independencia de su nombre.
|
||||
- **Implementación única compartida.** Las tres copias de `setup_persistent_network` (`auto_post_install.sh`, `customizable_post_install.sh`, `network_menu.sh`) y el desinstalador delegan ahora en `pmx_setup_persistent_network` / `pmx_uninstall_persistent_network` en `scripts/global/utils-install-functions.sh`. Los futuros arreglos no pueden dejar una copia atrás.
|
||||
- Se sube `FUNC_VERSION` de 1.0 → 1.1 en las tres llamadas, de modo que el detector de actualizaciones de ProxMenux vuelve a ejecutar la función en hosts que ya tenían la 1.0. Esa primera re-ejecución hace la migración legacy y la reconciliación en un solo paso.
|
||||
|
||||
---
|
||||
|
||||
## 🧮 Post-install — Optimización de ZFS ARC ajustada a su ámbito
|
||||
|
||||
- **`optimize_zfs_arc` establece ahora únicamente `zfs_arc_max`.** La función escribe una sola línea en `/etc/modprobe.d/99-zfsarc.conf`: `options zfs zfs_arc_max=<cap>`. `zfs_arc_min` queda en el valor por defecto de OpenZFS (auto-calculado como el mayor entre 32 MiB y ~1/32 de la RAM), y los tunables de L2ARC (`l2arc_noprefetch`, `l2arc_write_max`) y de TXG (`zfs_txg_timeout`) — que quedan fuera del ámbito de una optimización del ARC — se dejan en los valores por defecto de OpenZFS salvo que el operador los configure explícitamente.
|
||||
- **Se regenera ahora el initramfs tras escribir el fichero.** En sistemas con ZFS-on-root el módulo ZFS se carga desde el initramfs antes de que el sistema en marcha lea `/etc/modprobe.d/`, así que un simple reboot no bastaba para que el nuevo cap surtiera efecto. `update-initramfs -u -k all` se ejecuta justo después de escribir el fichero, más `proxmox-boot-tool refresh` en hosts con systemd-boot, de modo que el valor se aplica en el siguiente arranque en lugar de quedar sombreado por la copia obsoleta del initramfs.
|
||||
- **La función se protege con la presencia de un pool ZFS vivo** (chequeo `zpool list`), de manera que se convierte en no-op en hosts que no usan ZFS.
|
||||
- **Los valores del cap usan tamaños binarios limpios**: 512 MiB hasta 16 GB de RAM, 1 GiB hasta 32 GB, RAM/8 por encima — con un suelo de 512 MiB para que una lectura defectuosa de memoria no deje un ARC inutilizablemente pequeño.
|
||||
- Se sube `FUNC_VERSION` de 1.0 → 1.1, de modo que el detector de actualizaciones de ProxMenux vuelve a ejecutar la función en hosts que ya tenían la 1.0. Como la escritura es una reescritura completa de `99-zfsarc.conf`, ejecutar la función actualizada una vez reemplaza el fichero entero limpiamente. El desinstalador ejecuta ahora también `update-initramfs` + `proxmox-boot-tool refresh` tras restaurar o eliminar la configuración, de modo que la reversión se propaga al initramfs de la misma manera.
|
||||
|
||||
---
|
||||
|
||||
## 🔥 Post-install — Ajuste de sysctl en los bridges del firewall reforzado
|
||||
|
||||
- **El ajuste de `rp_filter=0` y `log_martians=0` sobre las interfaces del firewall bridge (`fwbr*`, `fwln*`, `fwpr*`, `tap*`) se aplica ahora también a las interfaces que Proxmox crea al arrancar, parar, reiniciar o migrar una VM.** Se añade `/etc/udev/rules.d/99-proxmenux-fwbr-tune.rules`, que dispara un helper por cada evento `net`/`add` matcheando esos prefijos, de modo que cada interfaz nueva obtiene el valor correcto inmediatamente sin necesidad de reboot ni de reejecutar el post-install.
|
||||
- **La lógica de ajuste se ha reorganizado en un helper independiente** en `/usr/local/sbin/proxmenux-fwbr-tune`, compartido por el barrido inicial (servicio `proxmenux-fwbr-tune.service`, tipo oneshot) y por la regla udev. Al instalar se hace además un barrido explícito para que la sesión actual vea el cambio sin esperar al siguiente ciclo de VM.
|
||||
- **El flujo configurable (`customizable_post_install.sh`) incorpora ahora el mismo helper + servicio + regla udev + barrido inicial que el flujo automático**, para que ambos variantes de post-install dejen el sistema en el mismo estado.
|
||||
- Ambas funciones `apply_network_optimizations` suben `FUNC_VERSION` de 1.0 → 1.1, de modo que el detector de actualizaciones de ProxMenux vuelve a ejecutar la función en hosts que ya tenían la 1.0. El desinstalador (`uninstall_network_optimization`) se amplía para borrar el nuevo helper y la regla udev, y recargar udev.
|
||||
|
||||
---
|
||||
|
||||
## 🧰 Post-install — Log2RAM + PBS
|
||||
|
||||
- **Rotación automática de los logs de la API de PBS cuando `proxmox-backup-server` corre como servicio en el host.** Ambos instaladores de Log2RAM (`install_log2ram_auto` y el configurable `configure_log2ram`) detectan PBS mediante `dpkg-query` y dejan `/etc/logrotate.d/proxmox-backup-api` con una regla de rotación por tamaño (20MB × 3 copias) más `/etc/cron.hourly/proxmox-backup-logrotate`. En un host PVE que además ejecuta PBS como servicio, `pvestatd` sondea el datastore local cada pocos segundos y cada sondeo escribe en `/var/log/proxmox-backup/api/access.log` y `auth.log` — el paquete upstream de PBS no incluye regla de logrotate para esos ficheros, y esta regla los mantiene acotados de modo que un `/var/log` respaldado por tmpfs permanece cómodamente dentro del presupuesto. En hosts sin PBS como servicio no se crea nada.
|
||||
- **Script upstream `log2ram` parcheado a `rsync -aXv --no-acls` justo después de `install.sh`.** Ambos instaladores reescriben la llamada en el sitio con un `sed` guardado por `grep -q` (deja backup en `.proxmenux.bak`, no-op si una futura release upstream ya no incluye `-A`). Los atributos extendidos (`-X`) se preservan. Resultado: `log2ram write` finaliza limpio en sistemas de ficheros de `/var/log.hdd` que no aceptan ACLs POSIX (ZFS con `acltype=off`, ext4 montado sin la opción `acl`) — sin mensajes de `set_acl: Operation not supported` / salida 23.
|
||||
- **El bloque de emergencia de `log2ram-check.sh` rota los logs de PBS antes de truncar.** Cuando `/var/log` cruza el umbral del 92%, el script de auto-sync ejecuta ahora `logrotate -f /etc/logrotate.d/proxmox-backup-api` (solo si el fichero de regla existe) *antes* de truncar `pveproxy/access.log`, `pveproxy/error.log` y `pveam.log`. El historial reciente de accesos/autenticación de PBS se conserva en los `.gz` rotados en lugar de perderse. Se subió `FUNC_VERSION` de 1.2 → 1.3 en `install_log2ram_auto` y `configure_log2ram`, y el comentario de cabecera del `log2ram-check.sh` embebido de v1.2 → v1.3.
|
||||
|
||||
## 🗄 Restauración de backup — importación automática de pools ZFS de datos
|
||||
|
||||
- **Los pools ZFS de datos listados en el backup ahora se importan automáticamente durante la restauración.** Anteriormente el flujo protegía correctamente el pool raíz (rpool) de un `zpool.cache` obsoleto, pero cualquier otro pool ZFS que un usuario tuviera (para VMs, LXCs o simplemente datos) quedaba a merced de `zfs-import-scan.service` para importarse en el siguiente arranque — y ese servicio falla cuando la instalación fresh tiene un `hostid` distinto al que está grabado en la etiqueta on-disk del pool. El pool aparecía como `FAILED Failed to start zfs-import-scan.service` y quedaba indisponible hasta que el usuario ejecutara de manera manual `zpool import -f` Ahora la restauración itera el `storage_inventory.zfs_pools[]` del backup, excluye el pool raíz (ya montado por el sistema) y ejecuta `zpool import <nombre>` para cada pool no-raíz cuyos discos estén todos presentes en `/dev/disk/by-id/`. Si ZFS rechaza el import por *foreign*, se reintenta con `-f` y se reporta el pool como forzado para que el operador sepa que el nuevo hostid quedó grabado en la etiqueta. Los pools a los que les falta algún disco se omiten con un aviso claro, en lugar de un import silencioso que reconstruiría el pool sin el vdev ausente.
|
||||
- **Los pools ZFS de datos listados en el backup ahora se importan automáticamente durante la restauración.** El nuevo paso `_rs_import_data_pools` corre tras el apply de configs, recorre `storage_inventory.zfs_pools[]`, excluye el pool raíz (ya montado por el sistema) y lanza `zpool import <nombre>` para cada pool no-raíz cuyos discos estén todos presentes en este host. Cuando ZFS rechaza el import por *foreign* — el caso típico tras una instalación fresh que regraba la etiqueta on-disk con un `hostid` nuevo — el paso reintenta con `-f` y reporta el pool como forzado para dejar trazabilidad. Los pools a los que les falta algún disco se omiten con un aviso claro en lugar de importarse en modo degradado. Todo esto cierra el caso habitual en el que `zfs-import-scan.service` fallaba al boot tras una instalación fresh y dejaba el pool de datos separado indisponible hasta ejecutar `zpool import -f` a mano.
|
||||
- **El resultado del auto-import persiste en la tarjeta post-restauración.** El paso escribe una sección `data_pools_import` en `/var/lib/proxmenux/restore-state.json` (el mismo JSON que la tarjeta de la pestaña Backups consulta) y un log crudo en `/var/log/proxmenux/restore-datapools-<timestamp>.log`. La tarjeta muestra dentro de Detalles un bloque dedicado con cinco filas coloreadas (Importados / Forzados / Omitidos parcial / Omitidos ausentes / Fallidos), así que el resumen queda consultable después de cerrar el terminal de restauración, y la entrada se preserva en el historial del run para consulta posterior.
|
||||
- **Pools ZFS creados con `by-partuuid` o `/dev/sdX` en bruto reconocidos por el chequeo de presencia de discos.** El paso de auto-import y `validate_storage.sh` tratan como absolutas las entradas de `devices_by_id` que empiezan por `/` y solo prependen `/dev/disk/by-id/` a los basenames desnudos, de modo que los pools construidos contra partition UUIDs o dispositivos de bloque en bruto se detectan como presentes cuando sus discos están en el host.
|
||||
- **`/etc/systemd/network` añadido a las rutas de backup por defecto.** Ese directorio contiene los ficheros `.link` de systemd que fijan los nombres de las NICs a su MAC a través de actualizaciones de kernel y reinstalaciones — el `setup_persistent_network` del post_install los escribe para cada interfaz física, y los usuarios pueden dejar los suyos también para renombrar una NIC a algo significativo. Preservarlos a través de una restauración sobre instalación fresh mantiene intacta en el destino la política de nombres de NIC del host origen, de modo que las entradas de `/etc/network/interfaces` que referencian nombres custom siguen resolviendo tras la restauración.
|
||||
|
||||
---
|
||||
|
||||
## 🙏 Agradecimientos
|
||||
|
||||
- **@pepenai** — reportó el bloqueo del dashboard en móvil en su setup con HTTPS + reverse proxy, lo que llevó al path de limpieza del Service Worker.
|
||||
- **Pepo** — reportó el `401 missing_timestamp` en su botón Test de PVE, lo que llevó al allowlist de IPs propias del host y a la corrección v4-mapped.
|
||||
- **@ash34** (#255) — reportó el `VM/CT check unavailable: 'NoneType' object has no attribute 'get'` que llevó al coalescing defensivo de la columna `details`.
|
||||
- **Juan C.** — reportó el `zfs-import-scan.service FAILED` post-restauración en una instalación fresh con un pool de datos separado, lo que llevó al paso de auto-import descrito arriba.
|
||||
- **@pepenai** — dashboard en móvil sobre HTTPS + reverse proxy.
|
||||
- **Pepo** — autenticación de webhook desde FQDN Tailscale.
|
||||
- **@ash34** (#255) — chequeo VM/CT con la columna `details` a NULL.
|
||||
- **@f3rs3n** (#256, #257, #258) — ajuste de sysctl en bridges del firewall, ámbito de la optimización de ZFS ARC y reconciliación de nombres persistentes de NIC.
|
||||
- **Juan C.** — auto-import de pools ZFS de datos tras instalación fresh.
|
||||
- **David Barbero (@sikete)** — recompilación de drivers DKMS al actualizar el kernel.
|
||||
|
||||
|
||||
## 2026-07-14
|
||||
|
||||
Reference in New Issue
Block a user