diff --git a/.github/workflows/build-appimage-Release.yml b/.github/workflows/build-appimage-Release.yml index 5693b81f..dea88359 100644 --- a/.github/workflows/build-appimage-Release.yml +++ b/.github/workflows/build-appimage-Release.yml @@ -42,6 +42,26 @@ jobs: - name: Build AppImage working-directory: AppImage run: ./scripts/build_appimage.sh + + - name: Set up Python 3.11 validation runtime + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Verify bundled Python 3.11 runtime + env: + PYTHONPATH: /tmp/proxmenux_build/ProxMenux.AppDir/usr/lib/python3/vendor/cp311:/tmp/proxmenux_build/ProxMenux.AppDir/usr/lib/python3/dist-packages + run: python -c 'import gevent, greenlet, geventwebsocket, zope.interface; from gevent import pywsgi; print(gevent.__version__)' + + - name: Set up Python 3.13 validation runtime + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Verify bundled Python 3.13 runtime + env: + PYTHONPATH: /tmp/proxmenux_build/ProxMenux.AppDir/usr/lib/python3/vendor/cp313:/tmp/proxmenux_build/ProxMenux.AppDir/usr/lib/python3/dist-packages + run: python -c 'import gevent, greenlet, geventwebsocket, zope.interface; from gevent import pywsgi; print(gevent.__version__)' - name: Get version from package.json id: version diff --git a/.github/workflows/build-appimage-beta.yml b/.github/workflows/build-appimage-beta.yml index 1fec8746..27b67920 100644 --- a/.github/workflows/build-appimage-beta.yml +++ b/.github/workflows/build-appimage-beta.yml @@ -42,6 +42,26 @@ jobs: - name: Build AppImage working-directory: AppImage run: ./scripts/build_appimage.sh + + - name: Set up Python 3.11 validation runtime + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Verify bundled Python 3.11 runtime + env: + PYTHONPATH: /tmp/proxmenux_build/ProxMenux.AppDir/usr/lib/python3/vendor/cp311:/tmp/proxmenux_build/ProxMenux.AppDir/usr/lib/python3/dist-packages + run: python -c 'import gevent, greenlet, geventwebsocket, zope.interface; from gevent import pywsgi; print(gevent.__version__)' + + - name: Set up Python 3.13 validation runtime + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Verify bundled Python 3.13 runtime + env: + PYTHONPATH: /tmp/proxmenux_build/ProxMenux.AppDir/usr/lib/python3/vendor/cp313:/tmp/proxmenux_build/ProxMenux.AppDir/usr/lib/python3/dist-packages + run: python -c 'import gevent, greenlet, geventwebsocket, zope.interface; from gevent import pywsgi; print(gevent.__version__)' - name: Get version from package.json id: version diff --git a/.github/workflows/build-appimage-manual.yml b/.github/workflows/build-appimage-manual.yml index 3f3b3158..8c11c593 100644 --- a/.github/workflows/build-appimage-manual.yml +++ b/.github/workflows/build-appimage-manual.yml @@ -40,6 +40,26 @@ jobs: - name: Build AppImage working-directory: AppImage run: ./scripts/build_appimage.sh + + - name: Set up Python 3.11 validation runtime + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Verify bundled Python 3.11 runtime + env: + PYTHONPATH: /tmp/proxmenux_build/ProxMenux.AppDir/usr/lib/python3/vendor/cp311:/tmp/proxmenux_build/ProxMenux.AppDir/usr/lib/python3/dist-packages + run: python -c 'import gevent, greenlet, geventwebsocket, zope.interface; from gevent import pywsgi; print(gevent.__version__)' + + - name: Set up Python 3.13 validation runtime + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Verify bundled Python 3.13 runtime + env: + PYTHONPATH: /tmp/proxmenux_build/ProxMenux.AppDir/usr/lib/python3/vendor/cp313:/tmp/proxmenux_build/ProxMenux.AppDir/usr/lib/python3/dist-packages + run: python -c 'import gevent, greenlet, geventwebsocket, zope.interface; from gevent import pywsgi; print(gevent.__version__)' - name: Get version from package.json id: version diff --git a/.github/workflows/build-appimage.yml b/.github/workflows/build-appimage.yml index af2657fe..0fa73f0c 100644 --- a/.github/workflows/build-appimage.yml +++ b/.github/workflows/build-appimage.yml @@ -42,6 +42,26 @@ jobs: - name: Build AppImage working-directory: AppImage run: ./scripts/build_appimage.sh + + - name: Set up Python 3.11 validation runtime + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Verify bundled Python 3.11 runtime + env: + PYTHONPATH: /tmp/proxmenux_build/ProxMenux.AppDir/usr/lib/python3/vendor/cp311:/tmp/proxmenux_build/ProxMenux.AppDir/usr/lib/python3/dist-packages + run: python -c 'import gevent, greenlet, geventwebsocket, zope.interface; from gevent import pywsgi; print(gevent.__version__)' + + - name: Set up Python 3.13 validation runtime + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Verify bundled Python 3.13 runtime + env: + PYTHONPATH: /tmp/proxmenux_build/ProxMenux.AppDir/usr/lib/python3/vendor/cp313:/tmp/proxmenux_build/ProxMenux.AppDir/usr/lib/python3/dist-packages + run: python -c 'import gevent, greenlet, geventwebsocket, zope.interface; from gevent import pywsgi; print(gevent.__version__)' - name: Get version from package.json id: version diff --git a/AppImage/scripts/AppRun b/AppImage/scripts/AppRun index 399815ee..74ad4d22 100644 --- a/AppImage/scripts/AppRun +++ b/AppImage/scripts/AppRun @@ -8,7 +8,29 @@ APPDIR="$(dirname "$(readlink -f "${0}")")" export PATH="${APPDIR}/usr/bin:${PATH}" export LD_LIBRARY_PATH="${APPDIR}/usr/lib/x86_64-linux-gnu:${APPDIR}/usr/lib:${APPDIR}/lib/x86_64-linux-gnu:${APPDIR}/lib:${LD_LIBRARY_PATH}" -export PYTHONPATH="${APPDIR}/usr/lib/python3/dist-packages:${APPDIR}/usr/lib/python3/site-packages:${PYTHONPATH}" + +# Select the native dependency tree matching the host interpreter. PVE 8 +# uses CPython 3.11 and PVE 9 uses CPython 3.13; both runtimes are bundled in +# the same AppImage so its gevent HTTPS/WSS server is independent of the +# Python version used by the GitHub Actions build runner. +PYTHON_ABI="$(python3 -c 'import sys; print(f"cp{sys.version_info.major}{sys.version_info.minor}")' 2>/dev/null || true)" +PYTHON_ABI_DIR="${APPDIR}/usr/lib/python3/vendor/${PYTHON_ABI}" +PYTHON_COMMON_DIRS="${APPDIR}/usr/lib/python3/dist-packages:${APPDIR}/usr/lib/python3/site-packages" + +case "$PYTHON_ABI" in + cp311|cp313) + if [ -d "$PYTHON_ABI_DIR" ]; then + export PYTHONPATH="${PYTHON_ABI_DIR}:${PYTHON_COMMON_DIRS}${PYTHONPATH:+:${PYTHONPATH}}" + else + echo "⚠️ Bundled Python runtime ${PYTHON_ABI} is missing; HTTPS/WSS may be unavailable" >&2 + export PYTHONPATH="${PYTHON_COMMON_DIRS}${PYTHONPATH:+:${PYTHONPATH}}" + fi + ;; + *) + echo "⚠️ Unsupported host Python ABI '${PYTHON_ABI:-unknown}'; expected cp311 (PVE 8) or cp313 (PVE 9)" >&2 + export PYTHONPATH="${PYTHON_COMMON_DIRS}${PYTHONPATH:+:${PYTHONPATH}}" + ;; +esac # Change to the AppImage directory cd "${APPDIR}" diff --git a/AppImage/scripts/build_appimage.sh b/AppImage/scripts/build_appimage.sh index f4820e56..006ceaa1 100755 --- a/AppImage/scripts/build_appimage.sh +++ b/AppImage/scripts/build_appimage.sh @@ -11,6 +11,18 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DIST_DIR="$SCRIPT_DIR/../dist" APPIMAGE_ROOT="$SCRIPT_DIR/.." +# Native Python dependencies are selected explicitly for the two Python ABIs +# used by supported Proxmox VE hosts. GitHub Actions currently builds on +# Ubuntu 22.04 (CPython 3.10), while PVE 8 and PVE 9 execute the AppImage with +# the host's CPython 3.11 and 3.13 respectively. Installing gevent with the +# builder's interpreter therefore creates an AppImage that cannot import its +# C extensions on either supported host (issue #331). +GEVENT_VERSION="26.8.0" +GREENLET_VERSION="3.5.5" +ZOPE_INTERFACE_VERSION="8.6" +ZOPE_EVENT_VERSION="6.2" +GEVENT_WEBSOCKET_VERSION="0.10.1" + VERSION=$(node -p "require('$APPIMAGE_ROOT/package.json').version") APPIMAGE_NAME="ProxMenux-${VERSION}.AppImage" @@ -58,6 +70,7 @@ fi # Create directory structure mkdir -p "$APP_DIR/usr/bin" mkdir -p "$APP_DIR/usr/lib/python3/dist-packages" +mkdir -p "$APP_DIR/usr/lib/python3/vendor" mkdir -p "$APP_DIR/usr/share/applications" mkdir -p "$APP_DIR/usr/share/icons/hicolor/256x256/apps" mkdir -p "$APP_DIR/web" @@ -319,11 +332,59 @@ pip3 install --target "$APP_DIR/usr/lib/python3/dist-packages" --upgrade \ simple-websocket>=0.10.0 \ flask-sock>=0.6.0 -# Phase 3b: Install gevent for SSL+WebSocket support (WSS) -pip3 install --target "$APP_DIR/usr/lib/python3/dist-packages" --upgrade \ - gevent>=24.2.1 \ - gevent-websocket>=0.10.1 \ - greenlet>=3.0.0 +# Phase 3b: Install the complete gevent/WSS runtime separately for every +# supported host ABI. Keeping each dependency tree isolated avoids mixing +# CPython-specific modules from the build runner with those loaded by PVE. +# The two --platform values allow gevent's manylinux_2_28 wheels and the +# backwards-compatible manylinux2014 zope.interface wheels to be resolved in +# one deterministic installation. +install_gevent_runtime() { + local python_version="$1" + local abi="$2" + local target="$APP_DIR/usr/lib/python3/vendor/$abi" + + echo "📦 Installing gevent runtime for ${abi} (Python ${python_version})..." + mkdir -p "$target" + python3 -m pip install \ + --disable-pip-version-check \ + --target "$target" \ + --upgrade \ + --only-binary=:all: \ + --platform manylinux_2_28_x86_64 \ + --platform manylinux2014_x86_64 \ + --implementation cp \ + --python-version "$python_version" \ + --abi "$abi" \ + --no-deps \ + "gevent==${GEVENT_VERSION}" \ + "greenlet==${GREENLET_VERSION}" \ + "zope.interface==${ZOPE_INTERFACE_VERSION}" \ + "zope.event==${ZOPE_EVENT_VERSION}" \ + "gevent-websocket==${GEVENT_WEBSOCKET_VERSION}" +} + +install_gevent_runtime "3.11" "cp311" +install_gevent_runtime "3.13" "cp313" + +echo "🔍 Verifying bundled Python ABI extensions..." +for abi in cp311 cp313; do + abi_digits="${abi#cp}" + abi_dir="$APP_DIR/usr/lib/python3/vendor/$abi" + for extension in \ + "gevent/_gevent_c_hub_local.cpython-${abi_digits}-*.so" \ + "greenlet/_greenlet.cpython-${abi_digits}-*.so" \ + "zope/interface/_zope_interface_coptimizations.cpython-${abi_digits}-*.so"; do + if ! compgen -G "$abi_dir/$extension" >/dev/null; then + echo "❌ Missing ${abi} extension: ${extension}" >&2 + exit 1 + fi + done + if [ ! -f "$abi_dir/geventwebsocket/__init__.py" ]; then + echo "❌ Missing gevent-websocket runtime for ${abi}" >&2 + exit 1 + fi + echo " ✅ ${abi} runtime complete" +done # Phase 3c: Apprise notification hub (issue #207). One library handles # ~80 notification services behind a single URL scheme (`tgram://`, diff --git a/scripts/post_install/auto_post_install.sh b/scripts/post_install/auto_post_install.sh index feb58bfc..e73df523 100644 --- a/scripts/post_install/auto_post_install.sh +++ b/scripts/post_install/auto_post_install.sh @@ -634,19 +634,150 @@ EOF # ========================================================== +_migrate_proxmenux_bashrc() { + local bashrc="$1" + local mode="${2:-migrate}" + + # Releases up to v1.1.3 appended an unmarked block. A later release + # accidentally wrote the marker variable names literally because its + # heredoc was quoted. Remove only those ProxMenux-owned shapes, together + # with the current marked block, before writing one canonical block. + python3 - "$bashrc" "$mode" <<'PY' +import os +import stat +import sys +import tempfile +from pathlib import Path + +path = Path(sys.argv[1]) +mode = sys.argv[2] if len(sys.argv) > 2 else "migrate" +if mode not in {"inspect", "migrate"}: + raise SystemExit(f"invalid Bashrc migration mode: {mode}") +lines = path.read_text(encoding="utf-8", errors="surrogateescape").splitlines(keepends=True) + + +def content(line: str) -> str: + return line.rstrip("\r\n") + + +def prompt_style(block: list[str]) -> str: + for line in block: + if content(line).startswith("export PS1=") and r"\w" in content(line): + return "full" + return "short" + + +legacy_tail = [ + "alias l='ls -CF'", + "alias la='ls -A'", + "alias ll='ls -alF'", + "alias ls='ls --color=auto'", + "alias grep='grep --color=auto'", + "alias fgrep='fgrep --color=auto'", + "alias egrep='egrep --color=auto'", + "source /etc/profile.d/bash_completion.sh", +] +marker_pairs = ( + ("# BEGIN PMX_CORE_BASHRC", "# END PMX_CORE_BASHRC"), + ("${marker_begin}", "${marker_end}"), +) + +result: list[str] = [] +detected_style = "short" +changed = False +i = 0 +while i < len(lines): + current = content(lines[i]) + + removed = False + for marker_begin, marker_end in marker_pairs: + if current != marker_begin: + continue + end_index = next( + (j for j in range(i + 1, len(lines)) if content(lines[j]) == marker_end), + None, + ) + # Preserve incomplete ranges rather than risk consuming user content. + if end_index is None or end_index - i > 32: + continue + block = lines[i:end_index + 1] + if prompt_style(block) == "full": + detected_style = "full" + i = end_index + 1 + changed = True + removed = True + break + if removed: + continue + + # Exact legacy signature: ProxMenux header, history format, coloured + # prompt, seven stock aliases and bash-completion. Unrelated user blocks + # cannot match this complete sequence. + if current == "# ProxMenux customizations" and i + 10 < len(lines): + candidate = lines[i:i + 11] + candidate_text = [content(line) for line in candidate] + ps1 = candidate_text[2] + if ( + candidate_text[1] == 'export HISTTIMEFORMAT="%d/%m/%y %T "' + and ps1.startswith('export PS1="') + and r"\e[38;5;172m" in ps1 + and r"\e[38;5;153m" in ps1 + and r"\e[38;5;214m" in ps1 + and candidate_text[3:] == legacy_tail + ): + if prompt_style(candidate) == "full": + detected_style = "full" + i += len(candidate) + changed = True + continue + + result.append(lines[i]) + i += 1 + +if changed and mode == "migrate": + original_stat = path.stat() + fd, temporary = tempfile.mkstemp(prefix=".bashrc.proxmenux.", dir=str(path.parent)) + try: + with os.fdopen(fd, "w", encoding="utf-8", errors="surrogateescape", newline="") as handle: + handle.write("".join(result)) + handle.flush() + os.fsync(handle.fileno()) + os.chmod(temporary, stat.S_IMODE(original_stat.st_mode)) + try: + os.chown(temporary, original_stat.st_uid, original_stat.st_gid) + except PermissionError: + pass + os.replace(temporary, path) + finally: + if os.path.exists(temporary): + os.unlink(temporary) + +print(detected_style) +PY +} + + customize_bashrc() { - local FUNC_VERSION="1.1" - # description: Install the managed ProxMenux Bash prompt and aliases while preserving or selecting the short/full working-directory style. + local FUNC_VERSION="1.2" + # description: Install and safely migrate the managed ProxMenux Bash prompt and aliases while preserving or selecting the short/full working-directory style. msg_info "$(translate "Customizing bashrc for root user...")" local bashrc="/root/.bashrc" local bash_profile="/root/.bash_profile" local marker_begin="# BEGIN PMX_CORE_BASHRC" local marker_end="# END PMX_CORE_BASHRC" local prompt_path_escape='\W' + local detected_path_style="short" + + [[ -f "$bashrc" ]] || touch "$bashrc" + if ! detected_path_style="$(_migrate_proxmenux_bashrc "$bashrc" inspect)"; then + msg_error "$(translate "Failed to inspect the existing ProxMenux Bash configuration.")" + return 1 + fi # Automated installs remain non-interactive. Preserve a previous - # ProxMenux choice on re-run and use the compact \W style on first use. - if sed -n "/^${marker_begin}$/,/^${marker_end}$/p" "$bashrc" 2>/dev/null | grep -Fq '\w'; then + # ProxMenux choice on re-run, including legacy blocks, and use the compact + # \W style on first use. + if [[ "$detected_path_style" == "full" ]]; then prompt_path_escape='\w' fi case "${PMX_BASHRC_PATH_STYLE:-}" in @@ -658,13 +789,11 @@ customize_bashrc() { return 1 ;; esac - - - [ -f "${bashrc}.bak" ] || cp "$bashrc" "${bashrc}.bak" > /dev/null 2>&1 - - if grep -q "^${marker_begin}$" "$bashrc" 2>/dev/null; then - sed -i "/^${marker_begin}$/,/^${marker_end}$/d" "$bashrc" + [ -f "${bashrc}.bak" ] || cp "$bashrc" "${bashrc}.bak" > /dev/null 2>&1 + if ! _migrate_proxmenux_bashrc "$bashrc" migrate >/dev/null; then + msg_error "$(translate "Failed to migrate the existing ProxMenux Bash configuration.")" + return 1 fi @@ -706,13 +835,152 @@ EOF +_update_existing_log2ram_auto() { + local func_version="$1" + local log2ram_bin="" + local candidate resolved tmp_file + + msg_ok "$(translate "Log2RAM already registered — updating to latest configuration")" + + for candidate in \ + "$(command -v log2ram 2>/dev/null)" \ + /usr/local/bin/log2ram \ + /usr/sbin/log2ram \ + /usr/bin/log2ram + do + [[ -n "$candidate" && -f "$candidate" ]] || continue + resolved="$(readlink -f "$candidate" 2>/dev/null || printf '%s' "$candidate")" + [[ -f "$resolved" ]] || continue + log2ram_bin="$resolved" + break + done + + if [[ -z "$log2ram_bin" || ! -f /etc/log2ram.conf ]]; then + msg_error "$(translate "Log2RAM installation verification failed. Check /tmp/log2ram_install.log")" + return 1 + fi + + if grep -q 'rsync -aAXv ' "$log2ram_bin" 2>/dev/null; then + [[ -e "${log2ram_bin}.proxmenux.bak" ]] || cp -a "$log2ram_bin" "${log2ram_bin}.proxmenux.bak" + tmp_file="$(mktemp "${log2ram_bin}.proxmenux.XXXXXX")" || return 1 + cp -a "$log2ram_bin" "$tmp_file" + sed -i 's/rsync -aAXv /rsync -aXv --no-acls /g' "$tmp_file" + mv -f "$tmp_file" "$log2ram_bin" + fi + + if dpkg-query -W -f='${Status}' proxmox-backup-server 2>/dev/null \ + | grep -q 'install ok installed'; then + tmp_file="$(mktemp /etc/logrotate.d/.proxmox-backup-api.XXXXXX)" || return 1 + cat > "$tmp_file" <<'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 "$tmp_file" + chown root:root "$tmp_file" + mv -f "$tmp_file" /etc/logrotate.d/proxmox-backup-api + + tmp_file="$(mktemp /etc/cron.hourly/.proxmox-backup-logrotate.XXXXXX)" || return 1 + cat > "$tmp_file" <<'EOF' +#!/bin/sh +/usr/sbin/logrotate /etc/logrotate.d/proxmox-backup-api >/dev/null 2>&1 +EOF + chmod 0755 "$tmp_file" + chown root:root "$tmp_file" + mv -f "$tmp_file" /etc/cron.hourly/proxmox-backup-logrotate + msg_ok "$(translate "PBS API log rotation configured (hourly, size-based)")" + fi + + tmp_file="$(mktemp /usr/local/bin/.log2ram-check.XXXXXX)" || return 1 + cat > "$tmp_file" <<'EOF' +#!/usr/bin/env bash +# 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 existing pveproxy/pveam logs, 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)" +[[ -z "$L2R_BIN" && -x /usr/sbin/log2ram ]] && L2R_BIN="/usr/sbin/log2ram" +[[ -z "$L2R_BIN" ]] && exit 0 + +SIZE_MiB="$(grep -E '^SIZE=' "$CONF_FILE" 2>/dev/null | cut -d'=' -f2 | tr -dc '0-9')" +[[ -z "$SIZE_MiB" ]] && SIZE_MiB=128 +LIMIT_BYTES=$(( SIZE_MiB * 1024 * 1024 )) +WARN_BYTES=$(( LIMIT_BYTES * 80 / 100 )) +EMERGENCY_BYTES=$(( LIMIT_BYTES * 92 / 100 )) + +USED_BYTES="$(df -B1 --output=used /var/log 2>/dev/null | tail -1 | tr -dc '0-9')" +[[ -z "$USED_BYTES" ]] && exit 0 + +LOCK="/run/log2ram-check.lock" +exec 9>"$LOCK" 2>/dev/null || exit 0 +flock -n 9 || exit 0 + +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 + [ -e /var/log/pveproxy/access.log ] && : > /var/log/pveproxy/access.log 2>/dev/null || true + [ -e /var/log/pveproxy/error.log ] && : > /var/log/pveproxy/error.log 2>/dev/null || true + [ -e /var/log/pveam.log ] && : > /var/log/pveam.log 2>/dev/null || true + "$L2R_BIN" write 2>/dev/null || true +elif (( USED_BYTES > WARN_BYTES )); then + SOFT_JOURNAL_MB=$(( SIZE_MiB * 30 / 100 )) + [[ "$SOFT_JOURNAL_MB" -lt 32 ]] && SOFT_JOURNAL_MB=32 + journalctl --vacuum-size="${SOFT_JOURNAL_MB}M" >/dev/null 2>&1 || true + "$L2R_BIN" write 2>/dev/null || true +fi +EOF + chmod 0755 "$tmp_file" + chown root:root "$tmp_file" + bash -n "$tmp_file" || return 1 + mv -f "$tmp_file" /usr/local/bin/log2ram-check.sh + + tmp_file="$(mktemp /etc/cron.d/.log2ram-auto-sync.XXXXXX)" || return 1 + cat > "$tmp_file" <<'EOF' +# Log2RAM auto-sync based on /var/log usage - Created by ProxMenux +SHELL=/bin/bash +PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +MAILTO="" +# 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 "$tmp_file" + chown root:root "$tmp_file" + mv -f "$tmp_file" /etc/cron.d/log2ram-auto-sync + + register_tool "log2ram" true "$func_version" + msg_success "$(translate "Log2RAM installation and configuration completed successfully.")" +} + + install_log2ram_auto() { - local FUNC_VERSION="1.4" + local FUNC_VERSION="1.5" + local existing_log2ram_bin="" # description: Install Log2RAM with size auto-tuned to host RAM (128M/256M/512M); SSD/M.2 detection skips on rotational disks. + existing_log2ram_bin="$(command -v log2ram 2>/dev/null || true)" + if [[ -f /etc/log2ram.conf || -n "$existing_log2ram_bin" \ + || -e /etc/systemd/system/log2ram.service \ + || -d /var/hdd.log || -d /var/log.hdd ]]; then + _update_existing_log2ram_auto "$FUNC_VERSION" + return $? + fi + if [[ -f "$TOOLS_JSON" ]] && jq -e '.log2ram == true or .log2ram.installed == true' "$TOOLS_JSON" >/dev/null 2>&1; then - msg_ok "$(translate "Log2RAM already registered — updating to latest configuration")" + msg_info2 "$(translate "Preparing Log2RAM configuration")" else # ── First-time install: detect SSD/M.2 ───────────────────────────────── msg_info "$(translate "Checking if system disk is SSD or M.2...")" @@ -784,7 +1052,7 @@ install_log2ram_auto() { fi rm -rf /tmp/log2ram 2>/dev/null || true - if ! git clone https://github.com/azlux/log2ram.git /tmp/log2ram >/dev/null 2>>/tmp/log2ram_install.log; then + if ! git clone --depth 1 https://github.com/azlux/log2ram.git /tmp/log2ram >/dev/null 2>>/tmp/log2ram_install.log; then msg_error "$(translate "Failed to clone log2ram repository. Check /tmp/log2ram_install.log")" return 1 fi @@ -842,7 +1110,6 @@ EOF 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 if [[ -f /etc/log2ram.conf ]] && command -v log2ram >/dev/null 2>&1; then @@ -997,15 +1264,15 @@ EOF chown -R www-data:www-data /var/log.hdd/pveproxy chmod 0750 /var/log.hdd/pveproxy - systemctl restart systemd-journald >/dev/null 2>&1 || true #msg_ok "$(translate "Backup created:") /etc/systemd/journald.conf.bak.$(date +%Y%m%d-%H%M%S)" msg_ok "$(translate "Journald configuration adjusted to") ${USE_MB}M (Log2RAM ${LOG2RAM_SIZE})" systemctl daemon-reload >/dev/null 2>&1 || true - systemctl restart log2ram >/dev/null 2>&1 || true - log2ram clean >/dev/null 2>&1 || true - log2ram write >/dev/null 2>&1 || true - systemctl restart rsyslog >/dev/null 2>&1 || true + if ! systemctl enable log2ram >/dev/null 2>&1; then + msg_error "$(translate "Log2RAM installation verification failed. Check /tmp/log2ram_install.log")" + return 1 + fi + NECESSARY_REBOOT=1 register_tool "log2ram" true "$FUNC_VERSION" } diff --git a/scripts/post_install/customizable_post_install.sh b/scripts/post_install/customizable_post_install.sh index 6630f3fb..b3ee583e 100644 --- a/scripts/post_install/customizable_post_install.sh +++ b/scripts/post_install/customizable_post_install.sh @@ -1939,16 +1939,132 @@ enable_vfio_iommu() { # ========================================================== +_migrate_proxmenux_bashrc() { + local bashrc="$1" + local mode="${2:-migrate}" + + # Releases up to v1.1.3 appended an unmarked block. A later release + # accidentally wrote the marker variable names literally because its + # heredoc was quoted. Remove only those ProxMenux-owned shapes, together + # with the current marked block, before writing one canonical block. + python3 - "$bashrc" "$mode" <<'PY' +import os +import stat +import sys +import tempfile +from pathlib import Path + +path = Path(sys.argv[1]) +mode = sys.argv[2] if len(sys.argv) > 2 else "migrate" +if mode not in {"inspect", "migrate"}: + raise SystemExit(f"invalid Bashrc migration mode: {mode}") +lines = path.read_text(encoding="utf-8", errors="surrogateescape").splitlines(keepends=True) +def content(line: str) -> str: + return line.rstrip("\r\n") +def prompt_style(block: list[str]) -> str: + for line in block: + if content(line).startswith("export PS1=") and r"\w" in content(line): + return "full" + return "short" +legacy_tail = [ + "alias l='ls -CF'", + "alias la='ls -A'", + "alias ll='ls -alF'", + "alias ls='ls --color=auto'", + "alias grep='grep --color=auto'", + "alias fgrep='fgrep --color=auto'", + "alias egrep='egrep --color=auto'", + "source /etc/profile.d/bash_completion.sh", +] +marker_pairs = ( + ("# BEGIN PMX_CORE_BASHRC", "# END PMX_CORE_BASHRC"), + ("${marker_begin}", "${marker_end}"), +) + +result: list[str] = [] +detected_style = "short" +changed = False +i = 0 +while i < len(lines): + current = content(lines[i]) + + removed = False + for marker_begin, marker_end in marker_pairs: + if current != marker_begin: + continue + end_index = next( + (j for j in range(i + 1, len(lines)) if content(lines[j]) == marker_end), + None, + ) + # Preserve incomplete ranges rather than risk consuming user content. + if end_index is None or end_index - i > 32: + continue + block = lines[i:end_index + 1] + if prompt_style(block) == "full": + detected_style = "full" + i = end_index + 1 + changed = True + removed = True + break + if removed: + continue + + # Exact legacy signature: ProxMenux header, history format, coloured + # prompt, seven stock aliases and bash-completion. Unrelated user blocks + # cannot match this complete sequence. + if current == "# ProxMenux customizations" and i + 10 < len(lines): + candidate = lines[i:i + 11] + candidate_text = [content(line) for line in candidate] + ps1 = candidate_text[2] + if ( + candidate_text[1] == 'export HISTTIMEFORMAT="%d/%m/%y %T "' + and ps1.startswith('export PS1="') + and r"\e[38;5;172m" in ps1 + and r"\e[38;5;153m" in ps1 + and r"\e[38;5;214m" in ps1 + and candidate_text[3:] == legacy_tail + ): + if prompt_style(candidate) == "full": + detected_style = "full" + i += len(candidate) + changed = True + continue + + result.append(lines[i]) + i += 1 + +if changed and mode == "migrate": + original_stat = path.stat() + fd, temporary = tempfile.mkstemp(prefix=".bashrc.proxmenux.", dir=str(path.parent)) + try: + with os.fdopen(fd, "w", encoding="utf-8", errors="surrogateescape", newline="") as handle: + handle.write("".join(result)) + handle.flush() + os.fsync(handle.fileno()) + os.chmod(temporary, stat.S_IMODE(original_stat.st_mode)) + try: + os.chown(temporary, original_stat.st_uid, original_stat.st_gid) + except PermissionError: + pass + os.replace(temporary, path) + finally: + if os.path.exists(temporary): + os.unlink(temporary) + +print(detected_style) +PY +} + customize_bashrc() { - local FUNC_VERSION="1.1" - # description: Install the managed ProxMenux Bash prompt and aliases while preserving or selecting the short/full working-directory style. + local FUNC_VERSION="1.2" + # description: Install and safely migrate the managed ProxMenux Bash prompt and aliases while preserving or selecting the short/full working-directory style. msg_info2 "$(translate "Customizing bashrc for root user...")" msg_info "$(translate "Customizing bashrc for root user...")" @@ -1961,10 +2077,18 @@ customize_bashrc() { local short_state="on" local full_state="off" local choice="" + local detected_path_style="short" + + [[ -f "$bashrc" ]] || touch "$bashrc" + if ! detected_path_style="$(_migrate_proxmenux_bashrc "$bashrc" inspect)"; then + msg_error "$(translate "Failed to inspect the existing ProxMenux Bash configuration.")" + return 1 + fi # Preserve an existing ProxMenux-managed choice when the function is - # re-run. \W shows only the current directory; \w shows the full path. - if sed -n "/^${marker_begin}$/,/^${marker_end}$/p" "$bashrc" 2>/dev/null | grep -Fq '\w'; then + # re-run, including legacy blocks. \W shows only the current directory; + # \w shows the full path. + if [[ "$detected_path_style" == "full" ]]; then prompt_path_escape='\w' short_state="off" full_state="on" @@ -1997,13 +2121,11 @@ customize_bashrc() { return 1 ;; esac - - - [ -f "${bashrc}.bak" ] || cp "$bashrc" "${bashrc}.bak" > /dev/null 2>&1 - - if grep -q "^${marker_begin}$" "$bashrc" 2>/dev/null; then - sed -i "/^${marker_begin}$/,/^${marker_end}$/d" "$bashrc" + [ -f "${bashrc}.bak" ] || cp "$bashrc" "${bashrc}.bak" > /dev/null 2>&1 + if ! _migrate_proxmenux_bashrc "$bashrc" migrate >/dev/null; then + msg_error "$(translate "Failed to migrate the existing ProxMenux Bash configuration.")" + return 1 fi @@ -2728,9 +2850,137 @@ update_pve_appliance_manager() { +_update_existing_log2ram_custom() { + local func_version="$1" + local log2ram_bin="" + local candidate resolved tmp_file + + msg_ok "$(translate "Log2RAM already registered — updating to latest configuration")" + + for candidate in \ + "$(command -v log2ram 2>/dev/null)" \ + /usr/local/bin/log2ram \ + /usr/sbin/log2ram \ + /usr/bin/log2ram + do + [[ -n "$candidate" && -f "$candidate" ]] || continue + resolved="$(readlink -f "$candidate" 2>/dev/null || printf '%s' "$candidate")" + [[ -f "$resolved" ]] || continue + log2ram_bin="$resolved" + break + done + + if [[ -z "$log2ram_bin" || ! -f /etc/log2ram.conf ]]; then + msg_error "$(translate "Log2RAM installation verification failed. Check /tmp/log2ram_install.log")" + return 1 + fi + + if grep -q 'rsync -aAXv ' "$log2ram_bin" 2>/dev/null; then + [[ -e "${log2ram_bin}.proxmenux.bak" ]] || cp -a "$log2ram_bin" "${log2ram_bin}.proxmenux.bak" + tmp_file="$(mktemp "${log2ram_bin}.proxmenux.XXXXXX")" || return 1 + cp -a "$log2ram_bin" "$tmp_file" + sed -i 's/rsync -aAXv /rsync -aXv --no-acls /g' "$tmp_file" + mv -f "$tmp_file" "$log2ram_bin" + fi + + if dpkg-query -W -f='${Status}' proxmox-backup-server 2>/dev/null \ + | grep -q 'install ok installed'; then + tmp_file="$(mktemp /etc/logrotate.d/.proxmox-backup-api.XXXXXX)" || return 1 + cat > "$tmp_file" <<'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 "$tmp_file" + chown root:root "$tmp_file" + mv -f "$tmp_file" /etc/logrotate.d/proxmox-backup-api + + tmp_file="$(mktemp /etc/cron.hourly/.proxmox-backup-logrotate.XXXXXX)" || return 1 + cat > "$tmp_file" <<'EOF' +#!/bin/sh +/usr/sbin/logrotate /etc/logrotate.d/proxmox-backup-api >/dev/null 2>&1 +EOF + chmod 0755 "$tmp_file" + chown root:root "$tmp_file" + mv -f "$tmp_file" /etc/cron.hourly/proxmox-backup-logrotate + msg_ok "$(translate "PBS API log rotation configured (hourly, size-based)")" + fi + + if [[ -f /usr/local/bin/log2ram-check.sh ]]; then + tmp_file="$(mktemp /usr/local/bin/.log2ram-check.XXXXXX)" || return 1 + cat > "$tmp_file" <<'EOF' +#!/usr/bin/env bash +# 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 existing pveproxy/pveam logs, 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)" +[[ -z "$L2R_BIN" && -x /usr/sbin/log2ram ]] && L2R_BIN="/usr/sbin/log2ram" +[[ -z "$L2R_BIN" ]] && exit 0 + +SIZE_MiB="$(grep -E '^SIZE=' "$CONF_FILE" 2>/dev/null | cut -d'=' -f2 | tr -dc '0-9')" +[[ -z "$SIZE_MiB" ]] && SIZE_MiB=128 +LIMIT_BYTES=$(( SIZE_MiB * 1024 * 1024 )) +WARN_BYTES=$(( LIMIT_BYTES * 80 / 100 )) +EMERGENCY_BYTES=$(( LIMIT_BYTES * 92 / 100 )) + +USED_BYTES="$(df -B1 --output=used /var/log 2>/dev/null | tail -1 | tr -dc '0-9')" +[[ -z "$USED_BYTES" ]] && exit 0 + +LOCK="/run/log2ram-check.lock" +exec 9>"$LOCK" 2>/dev/null || exit 0 +flock -n 9 || exit 0 + +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 + [ -e /var/log/pveproxy/access.log ] && : > /var/log/pveproxy/access.log 2>/dev/null || true + [ -e /var/log/pveproxy/error.log ] && : > /var/log/pveproxy/error.log 2>/dev/null || true + [ -e /var/log/pveam.log ] && : > /var/log/pveam.log 2>/dev/null || true + "$L2R_BIN" write 2>/dev/null || true +elif (( USED_BYTES > WARN_BYTES )); then + SOFT_JOURNAL_MB=$(( SIZE_MiB * 30 / 100 )) + [[ "$SOFT_JOURNAL_MB" -lt 32 ]] && SOFT_JOURNAL_MB=32 + journalctl --vacuum-size="${SOFT_JOURNAL_MB}M" >/dev/null 2>&1 || true + "$L2R_BIN" write 2>/dev/null || true +fi +EOF + chmod 0755 "$tmp_file" + chown root:root "$tmp_file" + bash -n "$tmp_file" || return 1 + mv -f "$tmp_file" /usr/local/bin/log2ram-check.sh + fi + + register_tool "log2ram" true "$func_version" + msg_success "$(translate "Log2RAM installation and configuration completed successfully.")" +} + + configure_log2ram() { - local FUNC_VERSION="1.3" + local FUNC_VERSION="1.5" + local existing_log2ram_bin="" # description: Install Log2RAM with user-chosen RAM size; prompts for size and SSD/M.2 awareness before applying. + + existing_log2ram_bin="$(command -v log2ram 2>/dev/null || true)" + if [[ -f /etc/log2ram.conf || -n "$existing_log2ram_bin" \ + || -e /etc/systemd/system/log2ram.service \ + || -d /var/hdd.log || -d /var/log.hdd ]]; then + _update_existing_log2ram_custom "$FUNC_VERSION" + return $? + fi + msg_info2 "$(translate "Preparing Log2RAM configuration")" sleep 1 @@ -2809,13 +3059,7 @@ configure_log2ram() { fi rm -rf /tmp/log2ram 2>/dev/null || true - # Pin to a tested release — `master` could ship breaking changes - # without notice. `LOG2RAM_TAG` env var lets the caller upgrade. - local LOG2RAM_TAG="${LOG2RAM_TAG:-1.7.0}" - if ! git clone --depth 1 --branch "$LOG2RAM_TAG" \ - https://github.com/azlux/log2ram.git /tmp/log2ram \ - >/dev/null 2>>/tmp/log2ram_install.log \ - && ! git clone --depth 1 https://github.com/azlux/log2ram.git /tmp/log2ram \ + if ! git clone --depth 1 https://github.com/azlux/log2ram.git /tmp/log2ram \ >/dev/null 2>>/tmp/log2ram_install.log; then msg_error "$(translate "Failed to clone log2ram repository. Check /tmp/log2ram_install.log")" return 1 @@ -2874,7 +3118,6 @@ EOF fi systemctl daemon-reload >/dev/null 2>&1 || true - systemctl enable --now log2ram >/dev/null 2>&1 || true if [[ -f /etc/log2ram.conf ]] && command -v log2ram >/dev/null 2>&1; then msg_ok "$(translate "Log2RAM installed successfully")" @@ -2905,7 +3148,7 @@ EOF # 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 +# truncate existing pveproxy/pveam logs, 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)" @@ -2932,9 +3175,9 @@ if (( USED_BYTES > EMERGENCY_BYTES )); then 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 + [ -e /var/log/pveproxy/access.log ] && : > /var/log/pveproxy/access.log 2>/dev/null || true + [ -e /var/log/pveproxy/error.log ] && : > /var/log/pveproxy/error.log 2>/dev/null || true + [ -e /var/log/pveam.log ] && : > /var/log/pveam.log 2>/dev/null || true "$L2R_BIN" write 2>/dev/null || true elif (( USED_BYTES > WARN_BYTES )); then SOFT_JOURNAL_MB=$(( SIZE_MiB * 30 / 100 )) @@ -3001,7 +3244,6 @@ MaxLevelConsole=notice MaxLevelWall=crit EOF - systemctl restart systemd-journald >/dev/null 2>&1 || true [[ "$BAK_OK" = "0" ]] && msg_ok "$(translate "Backup created:") /etc/systemd/journald.conf.bak.$(date +%Y%m%d-%H%M%S)" msg_ok "$(translate "Journald configuration adjusted to") ${USE_MB}M (Log2RAM ${LOG2RAM_SIZE})" @@ -3014,12 +3256,11 @@ EOF chmod 0750 /var/log.hdd/pveproxy systemctl restart cron >/dev/null 2>&1 || true - systemctl restart log2ram >/dev/null 2>&1 || true - - - log2ram write >/dev/null 2>&1 || true - log2ram clean >/dev/null 2>&1 || true - systemctl restart rsyslog >/dev/null 2>&1 || true + if ! systemctl enable log2ram >/dev/null 2>&1; then + msg_error "$(translate "Log2RAM installation verification failed. Check /tmp/log2ram_install.log")" + return 1 + fi + NECESSARY_REBOOT=1 msg_success "$(translate "Log2RAM installation and configuration completed successfully.")" register_tool "log2ram" true "$FUNC_VERSION" diff --git a/scripts/post_install/update_post_install_function.sh b/scripts/post_install/update_post_install_function.sh index c96ea225..17690202 100644 --- a/scripts/post_install/update_post_install_function.sh +++ b/scripts/post_install/update_post_install_function.sh @@ -106,10 +106,10 @@ ensure_flow_loaded() { } # Suppress the Monitor's `service_fail` notifications while this -# wrapper runs. Tools like log2ram or chrony stop and reinstall their -# systemd units, and systemd flags the transient "failed" state -# before the fresh unit starts — same class of noise as PVE services -# restarting during an apt full-upgrade. Shared markers with +# wrapper runs. Some post-install functions legitimately restart their +# services, and systemd can expose a transient "failed" state before the +# fresh unit starts — same class of noise as PVE services restarting +# during an apt full-upgrade. Shared markers with # scripts/utilities/proxmox_update.sh; consumed by # AppImage/scripts/notification_events.py::is_apt_active_on_host. _PROXMENUX_UPDATE_MARKER="/var/run/proxmenux-update-in-progress"