Files
ProxMenux/scripts/post_install/customizable_post_install.sh
T
MacRimiandClaude Opus 5 da8a480eff Add audit and reports page, and a change journal
ProxMenux modifies the host: it rewrites configuration files, installs packages, enables services. Until now nobody could say afterwards what had changed, and showing the script does not answer that question — a four-hundred-line function may alter two values, and the reader has no way to know which two. This adds the two halves of an answer.

The change journal records what ProxMenux does as it does it. Eleven bash primitives capture the previous state, apply the change and record it in the same step, writing to a spool that the Monitor reads back. One hundred and thirteen functions across twenty-five scripts are instrumented, covering post-install, shared storage, security tooling, container conversions, disk operations and the PVE 8 to 9 upgrade path. The page shows the difference — rotate 7 becoming rotate 14 — and never the script. Restore and backup scripts are deliberately left out: a restore puts the host back to a state some other script already recorded.

The Audit and reports page answers the other half: what state is this host in, regardless of who put it there. Forty-three checks across seven areas read the host and classify each result as critical, warning, observation, conformant, unverified or not applicable, with the evidence they read attached to each one. A declared policy lets the reader say what this particular host is expected to do — which guests must have a backup, which storages are essential — so the report judges the host against its own intent rather than a generic template. An inventory records the hardware, network and guest topology behind those readings, a comparison shows what moved between two runs, and six report profiles produce a printable document scoped to what the reader needs. Everything is available in the eight supported languages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 21:06:04 +02:00

3944 lines
137 KiB
Bash

#!/bin/bash
# ==========================================================
# ProxMenux - Customizable Post-Install Script
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : GPL-3.0
# https://github.com/MacRimi/ProxMenux/blob/main/LICENSE
# Version : 1.4
# ==========================================================
# Description:
# Interactive post-installation configurator for Proxmox VE.
# Groups ~30 optimizations into 10 categories (Basic Settings,
# System, Virtualization, Network, Storage, Security,
# Customization, Monitoring, Performance, Optional) and presents
# a checklist per category so the user picks exactly what to
# apply. Reversible changes are registered in installed_tools.json
# for later restoration from Uninstall Optimizations; package upgrades
# are intentionally excluded because they cannot be rolled back safely.
#
# Features:
# - Checklist UI per category (10 categories, ~30 tools total).
# - Superset of the Automated script: includes the 13 baseline
# optimizations plus opt-in items (IOMMU/VFIO, Fastfetch,
# Figurine, Ceph repo, HA, AMD fixes, pigz, ZFS ARC, …).
# - Idempotent: safe to run repeatedly.
# - Registration + rollback: every registered tool has a reverse
# function in uninstall-tools.sh.
#
# Credits:
# Incorporates ideas and snippets originally published under BSD
# by Adrian Jon Kriel (eXtremeSHOK) in xshok-proxmox, and elements
# of the Proxmox VE Post Install script from the Proxmox VE
# Helper-Scripts Community (MIT).
# https://github.com/extremeshok/xshok-proxmox
# https://github.com/community-scripts/ProxmoxVE
# ==========================================================
# Configuration
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
# Load shared global functions
if [[ -f "$LOCAL_SCRIPTS/global/common-functions.sh" ]]; then
source "$LOCAL_SCRIPTS/global/common-functions.sh"
fi
if [[ -f "$LOCAL_SCRIPTS/global/utils-install-functions.sh" ]]; then
source "$LOCAL_SCRIPTS/global/utils-install-functions.sh"
fi
# Recording is part of writing: sourced before any function runs so a
# change made without it is a mistake we can find, not one we can make.
if [[ -f "$LOCAL_SCRIPTS/global/pmx_journal.sh" ]]; then
source "$LOCAL_SCRIPTS/global/pmx_journal.sh"
fi
# ==========================================================
OS_CODENAME="$(grep "VERSION_CODENAME=" /etc/os-release | cut -d"=" -f 2 | xargs )"
RAM_SIZE_GB=$(( $(vmstat -s | grep -i "total memory" | xargs | cut -d" " -f 1) / 1024 / 1000))
NECESSARY_REBOOT=0
SCRIPT_TITLE="Customizable post-installation optimization script"
TOOLS_JSON="/usr/local/share/proxmenux/installed_tools.json"
# Sprint 12A: customizable_post_install.sh always emits source=custom so
# the update detector knows to re-run the customizable flow (which asks
# the user for parameters again) instead of the silent auto flow.
SCRIPT_SOURCE="custom"
ensure_tools_json() {
[ -f "$TOOLS_JSON" ] || echo "{}" > "$TOOLS_JSON"
}
# Sprint 12A: register_tool accepts (key, state, version, source).
# Each function declares `local FUNC_VERSION="X.Y"` on its first line and
# passes "$FUNC_VERSION" here. We use `local` rather than a `# version:`
# comment because bash's `declare -f` strips comments — comments would be
# silently lost when the update wrapper sourced the script and re-ran a
# function, leaving the registered version stuck at the default. See
# auto_post_install.sh for the full contract.
register_tool() {
local tool="$1"
local state="$2"
local version="${3:-1.0}"
local source="${4:-${SCRIPT_SOURCE:-unknown}}"
# Recorded here rather than in each function: this is the one call the
# whole of post-install already makes, so every applied tool reaches
# the journal even where the function itself still writes directly.
# Such an entry says what was applied and admits it cannot say what
# changed, which is the honest account for anything not yet migrated.
if declare -F pmx_record_applied >/dev/null 2>&1; then
PMX_JOURNAL_FUNCTION="${FUNCNAME[1]:-$tool}" \
PMX_JOURNAL_VERSION="$version" \
PMX_JOURNAL_SOURCE="$source" \
pmx_record_applied "$tool" "$version" \
"$([[ "$state" == "true" ]] && echo applied || echo removed)"
fi
ensure_tools_json
if [[ "$state" == "true" ]]; then
jq --arg t "$tool" --arg ver "$version" --arg src "$source" \
'.[$t]={"installed": true, "version": $ver, "source": $src}' \
"$TOOLS_JSON" > "$TOOLS_JSON.tmp" && mv "$TOOLS_JSON.tmp" "$TOOLS_JSON"
else
jq --arg t "$tool" '.[$t]=false' \
"$TOOLS_JSON" > "$TOOLS_JSON.tmp" && mv "$TOOLS_JSON.tmp" "$TOOLS_JSON"
fi
}
setup_proxmox_repositories() {
local FUNC_VERSION="1.1"
# Description: Configure Proxmox + Debian APT repositories (no-subscription)
# and set the correct file permissions.
msg_info2 "$(translate "Configuring Proxmox APT repositories...")"
if ! ensure_repositories; then
msg_error "$(translate "Failed to configure Proxmox repositories")"
register_tool "proxmox_repos" false "$FUNC_VERSION"
return 1
fi
local f
for f in /etc/apt/sources.list.d/proxmox.sources \
/etc/apt/sources.list.d/debian.sources \
/etc/apt/sources.list.d/pve-no-subscription.list \
/etc/apt/sources.list.d/pve-enterprise.list; do
[[ -f "$f" ]] && chmod 0644 "$f" 2>/dev/null
done
register_tool "proxmox_repos" true "$FUNC_VERSION"
msg_success "$(translate "Proxmox APT repositories configured")"
}
check_extremeshok_warning() {
local marker_file="/etc/extremeshok"
if [[ -f "$marker_file" ]]; then
dialog --backtitle "ProxMenux" --title "xshok-proxmox Post-Install Detected" \
--yesno "\n$(translate "It appears that you have already executed the xshok-proxmox post-install script on this system.")\n\n\
$(translate "If you continue, some adjustments may be duplicated or conflict with those already made by xshok.")\n\n\
$(translate "Do you want to continue anyway?")" 13 70
local response=$?
if [[ $response -ne 0 ]]; then
show_proxmenux_logo
msg_warn "$(translate "Action cancelled due to previous xshok-proxmox modifications.")"
echo -e
msg_success "$(translate "Press Enter to return to menu...")"
read -r
exit 1
fi
fi
}
# ==========================================================
enable_kexec() {
local FUNC_VERSION="1.1"
pmx_journal_context "enable_kexec" "$FUNC_VERSION"
# description: Install kexec-tools and add a Ctrl+Alt+K hotkey + systemd unit for fast reboots that skip BIOS/POST.
msg_info2 "$(translate "Configuring kexec for quick reboots...")"
NECESSARY_REBOOT=1
# Set default answers for debconf
pmx_apply_setting "kexec-tools/load_kexec" \
"debconf-show kexec-tools 2>/dev/null | grep -E '^[* ]*kexec-tools/load_kexec:'" \
bash -c 'echo "kexec-tools kexec-tools/load_kexec boolean false" | debconf-set-selections'
msg_info "$(translate "Installing kexec-tools...")"
# Install kexec-tools without showing output
if ! dpkg -s kexec-tools >/dev/null 2>&1; then
pmx_install_pkg kexec-tools
msg_ok "$(translate "kexec-tools installed successfully")"
else
msg_ok "$(translate "kexec-tools is already installed")"
fi
# Create systemd service file
local service_file="/etc/systemd/system/kexec-pve.service"
if [ ! -f "$service_file" ]; then
pmx_write_file "$service_file" <<'EOF'
[Unit]
Description=Loading new kernel into memory
Documentation=man:kexec(8)
DefaultDependencies=no
Before=reboot.target
RequiresMountsFor=/boot
#Before=shutdown.target umount.target final.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/sbin/kexec -d -l /boot/pve/vmlinuz --initrd=/boot/pve/initrd.img --reuse-cmdline
[Install]
WantedBy=default.target
EOF
msg_ok "$(translate "kexec-pve service file created")"
else
msg_ok "$(translate "kexec-pve service file is already configured")"
fi
# Enable the service
if ! systemctl is-enabled kexec-pve.service > /dev/null 2>&1; then
pmx_apply_setting "kexec-pve.service enablement" \
"systemctl is-enabled kexec-pve.service 2>/dev/null" \
systemctl enable kexec-pve.service
msg_ok "$(translate "kexec-pve service enabled")"
else
msg_ok "$(translate "kexec-pve service is already enabled")"
fi
if [ ! -f /root/.bash_profile ]; then
pmx_write_file /root/.bash_profile < /dev/null
fi
if ! grep -q "alias reboot-quick='systemctl kexec'" /root/.bash_profile; then
echo "alias reboot-quick='systemctl kexec'" | pmx_append_file /root/.bash_profile
msg_ok "$(translate "reboot-quick alias added")"
else
msg_ok "$(translate "reboot-quick alias is already configured")"
fi
msg_success "$(translate "kexec configured successfully. Use the command: reboot-quick")"
register_tool "kexec" true "$FUNC_VERSION"
}
# ==========================================================
apt_upgrade() {
local pve_version
pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+' | head -1)
if [[ -z "$pve_version" ]]; then
msg_error "Unable to detect Proxmox version."
return 1
fi
if [[ "$pve_version" -ge 9 ]]; then
bash "$LOCAL_SCRIPTS/global/update-pve9_2.sh"
else
bash "$LOCAL_SCRIPTS/global/update-pve8.sh"
fi
msg_success "$(translate "Proxmox repository configuration completed")"
}
# ==========================================================
optimize_journald() {
local FUNC_VERSION="1.0"
# description: Cap journald size, raise rate limit and force info-level logging so the log viewer and Fail2Ban work.
msg_info2 "$(translate "Limiting size and optimizing journald")"
NECESSARY_REBOOT=1
local journald_conf="/etc/systemd/journald.conf"
local config_changed=false
msg_info "$(translate "Configuring journald...")"
# Create a temporary configuration
cat <<EOF > /tmp/journald.conf.new
[Journal]
# Store on disk
Storage=persistent
# Don't split Journald logs by user
SplitMode=none
# Reasonable rate limits (evita spam en kernel/auditd)
RateLimitIntervalSec=30s
RateLimitBurst=1000
# Disable Journald forwarding to syslog
ForwardToSyslog=no
# Don't forward to wall (para evitar mensajes en terminales)
ForwardToWall=no
# Disable signing of the logs, save cpu resources
Seal=no
Compress=yes
# Fix the log size
SystemMaxUse=64M
RuntimeMaxUse=60M
# Optimize the logging and speed up tasks
# MaxLevelStore=info allows ProxMenux Monitor to display system logs correctly.
# Using "warning" causes the log viewer to show nearly identical entries across
# all date ranges (1d/3d/7d) because most activity is info-level.
# It also prevents Fail2Ban from detecting SSH/Proxmox auth failures via journal.
MaxLevelStore=info
MaxLevelSyslog=info
MaxLevelKMsg=warning
MaxLevelConsole=notice
MaxLevelWall=crit
EOF
# This function already declines to write when nothing differs; the
# journal replaces the move so the audit sees what was replaced.
if ! cmp -s "$journald_conf" "/tmp/journald.conf.new"; then
pmx_journal_context "optimize_journald" "$FUNC_VERSION"
pmx_write_file "$journald_conf" < /tmp/journald.conf.new
rm -f "/tmp/journald.conf.new"
config_changed=true
else
rm "/tmp/journald.conf.new"
fi
if [ "$config_changed" = true ]; then
systemctl restart systemd-journald.service > /dev/null 2>&1
msg_ok "$(translate "Journald configuration updated and service restarted")"
else
msg_ok "$(translate "Journald configuration is already optimized")"
fi
# Clean and rotate logs
journalctl --vacuum-size=64M --vacuum-time=1d > /dev/null 2>&1
journalctl --rotate > /dev/null 2>&1
msg_success "$(translate "Journald optimization completed")"
register_tool "journald" true "$FUNC_VERSION"
}
# ==========================================================
# ==========================================================
configure_kernel_panic() {
local FUNC_VERSION="1.0"
# description: Auto-reboot on kernel panic / oops / hardlockup; write crash dumps to /var/crash.
msg_info2 "$(translate "Configuring kernel panic behavior")"
NECESSARY_REBOOT=1
local config_file="/etc/sysctl.d/99-kernelpanic.conf"
msg_info "$(translate "Updating kernel panic configuration...")"
# Written through the journal, so the audit can show what this
# replaced — or that the file did not exist before.
pmx_journal_context "configure_kernel_panic" "$FUNC_VERSION"
pmx_write_file "$config_file" <<EOF
# Enable restart on kernel panic, kernel oops and hardlockup
kernel.core_pattern = /var/crash/core.%t.%p
# Reboot on kernel panic after 10s
kernel.panic = 10
# Panic on kernel oops, kernel exploits generally create an oops
kernel.panic_on_oops = 1
# Panic on a hardlockup
kernel.hardlockup_panic = 1
EOF
msg_ok "$(translate "Kernel panic configuration updated and applied")"
register_tool "kernel_panic" true "$FUNC_VERSION"
msg_success "$(translate "Kernel panic behavior configuration completed")"
}
# ==========================================================
increase_system_limits() {
local FUNC_VERSION="1.1"
pmx_journal_context "increase_system_limits" "$FUNC_VERSION"
# description: Raise inotify watches, file descriptors, process keys and PID limits to enterprise levels.
msg_info2 "$(translate "Increasing various system limits...")"
NECESSARY_REBOOT=1
# Function to safely append or replace configuration
append_or_replace() {
local file="$1"
local content="$2"
local temp_file=$(mktemp)
if [ -f "$file" ]; then
grep -vF "# ProxMenux configuration" "$file" > "$temp_file"
fi
echo -e "# ProxMenux configuration\n$content" >> "$temp_file"
pmx_write_file "$file" < "$temp_file"
rm -f "$temp_file"
}
# Increase max user watches
msg_info "$(translate "Configuring max user watches...")"
append_or_replace "/etc/sysctl.d/99-maxwatches.conf" "
fs.inotify.max_user_watches = 1048576
fs.inotify.max_user_instances = 1048576
fs.inotify.max_queued_events = 1048576"
msg_ok "$(translate "Max user watches configured")"
# Increase max FD limit / ulimit
msg_info "$(translate "Configuring max FD limit / ulimit...")"
append_or_replace "/etc/security/limits.d/99-limits.conf" "
* soft nproc 1048576
* hard nproc 1048576
* soft nofile 1048576
* hard nofile 1048576
root soft nproc unlimited
root hard nproc unlimited
root soft nofile unlimited
root hard nofile unlimited"
msg_ok "$(translate "Max FD limit / ulimit configured")"
# Increase kernel max Key limit
msg_info "$(translate "Configuring kernel max Key limit...")"
append_or_replace "/etc/sysctl.d/99-maxkeys.conf" "
kernel.keys.root_maxkeys=1000000
kernel.keys.maxkeys=1000000"
msg_ok "$(translate "Kernel max Key limit configured")"
# Set systemd ulimits
msg_info "$(translate "Setting systemd ulimits...")"
for file in /etc/systemd/system.conf /etc/systemd/user.conf; do
if ! grep -q "^DefaultLimitNOFILE=" "$file"; then
echo "DefaultLimitNOFILE=1048576" | pmx_append_file "$file"
fi
done
msg_ok "$(translate "Systemd ulimits set")"
# Configure PAM limits
msg_info "$(translate "Configuring PAM limits...")"
for file in /etc/pam.d/common-session /etc/pam.d/runuser-l; do
if ! grep -q "^session required pam_limits.so" "$file"; then
echo 'session required pam_limits.so' | pmx_append_file "$file"
fi
done
msg_ok "$(translate "PAM limits configured")"
# Set ulimit for the shell user
msg_info "$(translate "Setting ulimit for the shell user...")"
if ! grep -q "ulimit -n 1048576" /root/.profile; then
pmx_edit_file /root/.profile '/ulimit -n 256000/d' 2>/dev/null || true
echo "ulimit -n 1048576" | pmx_append_file /root/.profile
fi
msg_ok "$(translate "Shell user ulimit set")"
# Configure swappiness
msg_info "$(translate "Configuring kernel swappiness...")"
append_or_replace "/etc/sysctl.d/99-swap.conf" "
vm.swappiness = 10
vm.vfs_cache_pressure = 100"
msg_ok "$(translate "Swappiness configuration created successfully")"
# Increase Max FS open files
msg_info "$(translate "Increasing maximum file system open files...")"
append_or_replace "/etc/sysctl.d/99-fs.conf" "
fs.nr_open = 2097152
fs.file-max = 2097152
fs.aio-max-nr = 1048576"
msg_ok "$(translate "Max FS open files configuration created successfully")"
register_tool "system_limits" true "$FUNC_VERSION"
msg_success "$(translate "System limits increase completed.")"
}
# ==========================================================
skip_apt_languages() {
local FUNC_VERSION="1.0"
pmx_journal_context "skip_apt_languages" "$FUNC_VERSION"
# description: Stop APT from downloading translation files to speed up updates.
msg_info2 "$(translate "Configuring APT to skip downloading additional languages")"
# 1. Detect locale
local default_locale=""
if [ -f /etc/default/locale ]; then
default_locale=$(grep '^LANG=' /etc/default/locale | cut -d= -f2 | tr -d '"')
elif [ -f /etc/environment ]; then
default_locale=$(grep '^LANG=' /etc/environment | cut -d= -f2 | tr -d '"')
fi
# Fallback
default_locale="${default_locale:-en_US.UTF-8}"
# Normalize for comparison (en_US.UTF-8 → en_US.utf8)
local normalized_locale
normalized_locale=$(echo "$default_locale" | tr 'A-Z' 'a-z' | sed 's/utf-8/utf8/;s/-/_/')
# 2. Only generate if missing
if ! locale -a | grep -qi "^$normalized_locale$"; then
# Only add to locale.gen if missing
if ! grep -qE "^${default_locale}[[:space:]]+UTF-8" /etc/locale.gen; then
echo "$default_locale UTF-8" | pmx_append_file /etc/locale.gen
fi
msg_info "$(translate "Generating missing locale:") $default_locale"
pmx_record_execution "Generate locale" "locale-gen $default_locale"
locale-gen "$default_locale"
msg_ok "$(translate "Locale generated")"
fi
# 3. Set APT to skip language downloads
local config_file="/etc/apt/apt.conf.d/99-disable-translations"
local config_content='Acquire::Languages "none";'
msg_info "$(translate "Setting APT language configuration...")"
if [ -f "$config_file" ] && grep -Fxq "$config_content" "$config_file"; then
msg_ok "$(translate "APT language configuration already set")"
else
printf '%s\n' "$config_content" | pmx_write_file "$config_file"
msg_ok "$(translate "APT language configuration updated")"
fi
register_tool "apt_languages" true "$FUNC_VERSION"
msg_success "$(translate "APT configured to skip downloading additional languages")"
}
# ==========================================================
configure_time_sync() {
local FUNC_VERSION="1.0"
pmx_journal_context "configure_time_sync" "$FUNC_VERSION"
# description: Detect timezone from public IP and enable systemd time sync (NTP).
msg_info2 "$(translate "Configuring system time settings...")"
this_ip=$(dig +short myip.opendns.com @resolver1.opendns.com 2>/dev/null)
if [ -z "$this_ip" ]; then
msg_warn "$(translate "Failed to obtain public IP address - keeping current timezone settings")"
return 0
fi
timezone=$(curl -s --connect-timeout 10 "https://ipapi.co/${this_ip}/timezone" 2>/dev/null | tr -d '[:space:]')
if [ -z "$timezone" ] || [ "$timezone" = "undefined" ]; then
msg_warn "$(translate "Failed to determine timezone from IP address - keeping current timezone settings")"
return 0
fi
# Validate against the system's IANA timezone database before applying.
# ipapi.co can return rate-limit JSON, an error string, or stale data; the
# previous code accepted anything that wasn't literally "undefined" and
# passed it straight to `timedatectl set-timezone`, which silently kept
# the old TZ on a bad value.
if ! timedatectl list-timezones 2>/dev/null | grep -Fxq "$timezone"; then
msg_warn "$(translate "API returned an invalid timezone") ($timezone) - $(translate "keeping current settings")"
return 0
fi
msg_ok "$(translate "Found timezone $timezone for IP $this_ip")"
pmx_apply_setting "timezone" "timedatectl show -p Timezone --value" \
timedatectl set-timezone "$timezone"
if timedatectl set-timezone "$timezone"; then
msg_ok "$(translate "Timezone set to $timezone")"
pmx_apply_setting "ntp" "timedatectl show -p NTP --value" \
timedatectl set-ntp true
if timedatectl set-ntp true; then
msg_ok "$(translate "Time settings configured - Timezone:") $timezone"
register_tool "time_sync" true "$FUNC_VERSION"
pmx_record_execution "Restart Postfix" "systemctl restart postfix"
systemctl restart postfix 2>/dev/null || true
else
msg_warn "$(translate "Failed to enable automatic time synchronization")"
fi
else
msg_warn "$(translate "Failed to set timezone - keeping current settings")"
fi
}
# ==========================================================
# ==========================================================
# configure_entropy removed — modern kernels (5.6+) have built-in entropy generation
# haveged is no longer needed and adds unnecessary overhead
# ==========================================================
apply_amd_fixes() {
local FUNC_VERSION="1.0"
pmx_journal_context "apply_amd_fixes" "$FUNC_VERSION"
# description: Detect AMD EPYC/Ryzen CPUs and apply microcode + IOMMU + KVM-specific kernel boot params.
msg_info2 "$(translate "Detecting AMD CPU and applying fixes if necessary...")"
NECESSARY_REBOOT=1
local cpu_model
cpu_model=$(grep -i -m 1 "model name" /proc/cpuinfo || true)
if echo "$cpu_model" | grep -qiE "EPYC|Ryzen"; then
msg_ok "$(translate "AMD CPU detected")"
else
msg_ok "$(translate "No AMD CPU detected. Skipping AMD fixes.")"
return 0
fi
msg_info "$(translate "Applying AMD-specific fixes...")"
local cmdline_file="/etc/kernel/cmdline"
local grub_file="/etc/default/grub"
local added_param="idle=nomwait"
local uses_zfs=false
if grep -q "root=ZFS=" "$cmdline_file" 2>/dev/null; then
uses_zfs=true
fi
if $uses_zfs && [[ -f "$cmdline_file" ]]; then
# ZFS/systemd-boot
if ! grep -qw "$added_param" "$cmdline_file"; then
cp "$cmdline_file" "${cmdline_file}.bak"
pmx_edit_file "$cmdline_file" "s|\s*$| $added_param|"
msg_ok "$(translate "Added '$added_param' to /etc/kernel/cmdline")"
else
msg_ok "$(translate "'$added_param' already present in /etc/kernel/cmdline")"
fi
if command -v proxmox-boot-tool >/dev/null 2>&1; then
pmx_record_execution "Refresh Proxmox boot configuration" "proxmox-boot-tool refresh"
proxmox-boot-tool refresh >/dev/null 2>&1 && \
msg_ok "$(translate "proxmox-boot-tool refreshed")" || \
msg_warn "$(translate "Failed to refresh proxmox-boot-tool")"
fi
else
# GRUB (no ZFS)
if [[ -f "$grub_file" ]]; then
grep -q '^GRUB_CMDLINE_LINUX_DEFAULT="' "$grub_file" || echo 'GRUB_CMDLINE_LINUX_DEFAULT=""' | pmx_append_file "$grub_file"
if ! grep -q 'GRUB_CMDLINE_LINUX_DEFAULT=' "$grub_file"; then
msg_warn "$(translate "GRUB_CMDLINE_LINUX_DEFAULT not found in GRUB config")"
else
if ! grep -q "GRUB_CMDLINE_LINUX_DEFAULT=.*\b$added_param\b" "$grub_file"; then
cp "$grub_file" "${grub_file}.bak"
pmx_edit_file "$grub_file" "s/^\(GRUB_CMDLINE_LINUX_DEFAULT=\"[^\"]*\)\"/\1 $added_param\"/"
msg_ok "$(translate "Added '$added_param' to GRUB_CMDLINE_LINUX_DEFAULT")"
else
msg_ok "$(translate "'$added_param' already present in GRUB_CMDLINE_LINUX_DEFAULT")"
fi
pmx_record_execution "Regenerate GRUB configuration" "update-grub"
update-grub >/dev/null 2>&1 && \
msg_ok "$(translate "GRUB configuration updated")" || \
msg_warn "$(translate "Failed to update GRUB")"
fi
else
msg_warn "$(translate "GRUB config file not found; skipping GRUB changes")"
fi
fi
local kvm_conf="/etc/modprobe.d/kvm.conf"
[[ -f "$kvm_conf" ]] || pmx_write_file "$kvm_conf" < /dev/null
if ! grep -q "^options kvm " "$kvm_conf"; then
echo "options kvm ignore_msrs=Y report_ignored_msrs=N" | pmx_append_file "$kvm_conf"
msg_ok "$(translate "KVM MSR options added to /etc/modprobe.d/kvm.conf")"
else
if ! grep -q "ignore_msrs=" "$kvm_conf"; then
pmx_edit_file "$kvm_conf" 's/^options kvm /options kvm ignore_msrs=Y /'
else
pmx_edit_file "$kvm_conf" 's/ignore_msrs=[YNyn]/ignore_msrs=Y/'
fi
if ! grep -q "report_ignored_msrs=" "$kvm_conf"; then
pmx_edit_file "$kvm_conf" 's/^options kvm .*/& report_ignored_msrs=N/'
else
pmx_edit_file "$kvm_conf" 's/report_ignored_msrs=[YNyn]/report_ignored_msrs=N/'
fi
msg_ok "$(translate "KVM MSR options ensured in /etc/modprobe.d/kvm.conf")"
fi
msg_success "$(translate "AMD CPU fixes applied successfully")"
register_tool "amd_fixes" true "$FUNC_VERSION"
}
# ==========================================================
force_apt_ipv4() {
local FUNC_VERSION="1.0"
# description: Force APT to use IPv4 to avoid stalls on hosts with broken IPv6 connectivity.
msg_info2 "$(translate "Configuring APT to use IPv4...")"
local config_file="/etc/apt/apt.conf.d/99-force-ipv4"
local config_content="Acquire::ForceIPv4 \"true\";"
if [ -f "$config_file" ] && grep -q "$config_content" "$config_file"; then
msg_ok "$(translate "APT configured to use IPv4")"
else
msg_info "$(translate "Creating APT configuration to force IPv4...")"
if echo -e "$config_content\n" > "$config_file"; then
msg_ok "$(translate "APT configured to use IPv4")"
fi
fi
register_tool "apt_ipv4" true "$FUNC_VERSION"
msg_success "$(translate "APT IPv4 configuration completed")"
}
# ==========================================================
apply_network_optimizations() {
local FUNC_VERSION="1.2"
pmx_journal_context "apply_network_optimizations" "$FUNC_VERSION"
# 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
pmx_write_file /etc/sysctl.d/99-network.conf <<'EOF'
# ==========================================================
# ProxMenux - Network tuning (PVE 9 compatible)
# ==========================================================
# Core buffers & queues
net.core.netdev_max_backlog = 8192
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.core.somaxconn = 8192
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.log_martians = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv4.conf.default.secure_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.default.log_martians = 0
# rp_filter=2 (loose) instead of the kernel default 1 (strict). Loose
# allows asymmetric routing typical of a Proxmox host with VMs on
# multiple bridges — strict mode would drop legitimate VM traffic when
# the reverse path differs. Trade-off: slightly weaker spoofing
# protection from local LAN. Document this choice rather than the
# default.
net.ipv4.conf.all.rp_filter = 2
net.ipv4.conf.default.rp_filter = 2
# ICMP
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
# TCP/IP
# Wider ephemeral port range than Debian's default 32768-60999 so
# Proxmox hosts handing out NAT/forward ports for many VMs/CTs don't
# run out under load. Privileged-port range (1-1023) is still protected
# by capabilities — the kernel won't actually pick one for an
# unprivileged ephemeral allocation.
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_mtu_probing = 1
net.ipv4.tcp_rfc1337 = 1
net.ipv4.tcp_sack = 1
net.ipv4.tcp_rmem = 8192 87380 16777216
net.ipv4.tcp_wmem = 8192 65536 16777216
# Unix sockets
net.unix.max_dgram_qlen = 4096
EOF
pmx_record_execution "Apply network sysctl configuration" "sysctl --system"
sysctl --system > /dev/null 2>&1
pmx_write_file /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
pmx_write_file /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
pmx_remove_file /etc/udev/rules.d/99-proxmenux-fwbr-tune.rules
pmx_write_file /etc/udev/rules.d/99-zz-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-zz-proxmenux-fwbr-tune.rules
chown root:root /etc/udev/rules.d/99-zz-proxmenux-fwbr-tune.rules
pmx_record_execution "Reload systemd configuration" "systemctl daemon-reload"
systemctl daemon-reload >/dev/null 2>&1 || true
pmx_record_execution "Reload udev rules" "udevadm control --reload-rules"
udevadm control --reload-rules >/dev/null 2>&1 || true
pmx_enable_service proxmenux-fwbr-tune.service || true
pmx_record_execution "Tune existing Proxmox firewall bridge interfaces" "/usr/local/sbin/proxmenux-fwbr-tune"
/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
echo "source /etc/network/interfaces.d/*" | pmx_append_file "$interfaces_file"
fi
msg_ok "$(translate "Network optimization completed")"
register_tool "network_optimization" true "$FUNC_VERSION"
}
# ==========================================================
install_openvswitch() {
local FUNC_VERSION="1.0"
# description: Install OpenVSwitch for software-defined networking inside VMs and containers.
msg_info2 "$(translate "Installing OpenVSwitch for virtual internal network...")"
# Install OpenVSwitch
msg_info "$(translate "Installing OpenVSwitch packages...")"
(
/usr/bin/env DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::='--force-confdef' install openvswitch-switch openvswitch-common 2>&1 | \
while IFS= read -r line; do
if [[ $line == *"Installing"* ]] || [[ $line == *"Unpacking"* ]]; then
printf "\r%-$(($(tput cols)-1))s\r" " " # Clear current line
printf "\r%s" "$line"
fi
done
)
if [ $? -eq 0 ]; then
printf "\r%-$(($(tput cols)-1))s\r" " " # Clear final line
msg_ok "$(translate "OpenVSwitch installed successfully")"
else
printf "\r%-$(($(tput cols)-1))s\r" " " # Clear final line
msg_warn "$(translate "Failed to install OpenVSwitch")"
fi
# Verify installation
if command -v ovs-vsctl >/dev/null 2>&1; then
msg_success "$(translate "OpenVSwitch is ready to use")"
register_tool "openvswitch" true "$FUNC_VERSION"
else
msg_warn "$(translate "OpenVSwitch installation could not be verified")"
fi
}
# ==========================================================
enable_tcp_fast_open() {
local FUNC_VERSION="1.0"
pmx_journal_context "enable_tcp_fast_open" "$FUNC_VERSION"
# description: Enable TCP Fast Open (clients + server) and BBR congestion control for better latency under load.
msg_info2 "$(translate "Configuring TCP optimizations...")"
local bbr_conf="/etc/sysctl.d/99-kernel-bbr.conf"
local tfo_conf="/etc/sysctl.d/99-tcp-fastopen.conf"
local reboot_needed=0
# Enable Google TCP BBR congestion control
msg_info "$(translate "Enabling Google TCP BBR congestion control...")"
if [ ! -f "$bbr_conf" ] || ! grep -q "net.ipv4.tcp_congestion_control = bbr" "$bbr_conf"; then
pmx_write_file "$bbr_conf" <<EOF
# TCP BBR congestion control
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
EOF
msg_ok "$(translate "TCP BBR configuration created successfully")"
reboot_needed=1
else
msg_ok "$(translate "TCP BBR configuration created successfully")"
fi
# Enable TCP Fast Open
msg_info "$(translate "Enabling TCP Fast Open...")"
if [ ! -f "$tfo_conf" ] || ! grep -q "net.ipv4.tcp_fastopen = 3" "$tfo_conf"; then
pmx_write_file "$tfo_conf" <<EOF
# TCP Fast Open (TFO)
net.ipv4.tcp_fastopen = 3
EOF
msg_ok "$(translate "TCP Fast Open configuration created successfully")"
else
msg_ok "$(translate "TCP Fast Open configuration created successfully")"
fi
# Apply changes
pmx_record_execution "Apply sysctl configuration" "sysctl --system"
sysctl --system > /dev/null 2>&1
if [ "$reboot_needed" -eq 1 ]; then
NECESSARY_REBOOT=1
fi
msg_success "$(translate "TCP optimizations configuration completed")"
register_tool "tcp_optimizations" true "$FUNC_VERSION"
}
# ==========================================================
install_ceph() {
local FUNC_VERSION="1.1"
pmx_journal_context "install_ceph" "$FUNC_VERSION"
# description: Install Ceph (client + server packages) for distributed RBD/CephFS storage; PVE 8/9 aware repo selection.
msg_info2 "$(translate "Installing Ceph support...")"
local pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+' | head -1)
local current_codename=$(lsb_release -cs 2>/dev/null || echo "bookworm")
local is_pve9=false
local ceph_version="squid"
local target_codename="bookworm"
if [ "$pve_version" -ge 9 ] 2>/dev/null || [ "$current_codename" = "trixie" ]; then
is_pve9=true
target_codename="trixie"
ceph_version="squid"
msg_info2 "$(translate "Detected Proxmox VE 9.x - Installing Ceph Squid for Debian Trixie")"
else
target_codename="$current_codename"
ceph_version="squid"
msg_info2 "$(translate "Detected Proxmox VE 8.x - Installing Ceph Squid for Debian") $target_codename"
fi
if pveceph status &>/dev/null; then
msg_ok "$(translate "Ceph is already installed")"
msg_success "$(translate "Ceph installation check completed")"
return 0
fi
if [[ ! -r /usr/share/keyrings/proxmox-archive-keyring.gpg ]]; then
msg_error "$(translate "The Proxmox archive keyring is missing; Ceph installation cannot continue safely")"
return 1
fi
# Configure Ceph repository based on version
msg_info "$(translate "Configuring Ceph repository for PVE") $pve_version..."
if [ "$is_pve9" = true ]; then
# ==========================================
# CEPH CONFIGURATION FOR PROXMOX VE 9
# ==========================================
[ -f /etc/apt/sources.list.d/ceph-squid.list ] && pmx_remove_file /etc/apt/sources.list.d/ceph-squid.list
[ -f /etc/apt/sources.list.d/ceph.list ] && pmx_remove_file /etc/apt/sources.list.d/ceph.list
# Create new deb822 format Ceph repository for PVE 9
msg_info "$(translate "Creating Ceph repository for PVE 9 (deb822 format)...")"
pmx_write_file /etc/apt/sources.list.d/ceph.sources << EOF
Types: deb
URIs: https://download.proxmox.com/debian/ceph-${ceph_version}
Suites: ${target_codename}
Components: no-subscription
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
EOF
chmod 0644 /etc/apt/sources.list.d/ceph.sources
msg_ok "$(translate "Ceph repository configured for PVE 9")"
else
# ==========================================
# CEPH CONFIGURATION FOR PROXMOX VE 8
# ==========================================
# Use legacy format for PVE 8
msg_info "$(translate "Creating Ceph repository for PVE 8 (legacy format)...")"
echo "deb [signed-by=/usr/share/keyrings/proxmox-archive-keyring.gpg] https://download.proxmox.com/debian/ceph-${ceph_version} ${target_codename} no-subscription" | pmx_write_file /etc/apt/sources.list.d/ceph-${ceph_version}.list
msg_ok "$(translate "Ceph repository configured for PVE 8")"
fi
msg_info "$(translate "Updating package lists...")"
pmx_record_execution "Update package lists for Ceph" "apt-get update"
update_output=$(apt-get update 2>&1)
update_exit_code=$?
if [ $update_exit_code -eq 0 ]; then
msg_ok "$(translate "Package lists updated successfully")"
else
msg_warn "$(translate "Package update had issues, checking details...")"
if echo "$update_output" | grep -Eqi 'NO_PUBKEY|GPG error|EXPKEYSIG|BADSIG|not signed|signatures? (could not|couldn.t) be verified'; then
msg_error "$(translate "Ceph repository signature verification failed; installation has been stopped")"
return 1
elif echo "$update_output" | grep -q "404\|Failed to fetch"; then
msg_warn "$(translate "Some repositories are not available, continuing with available ones...")"
else
msg_warn "$(translate "Package update completed with warnings, continuing...")"
fi
fi
msg_info "$(translate "Verifying Ceph packages availability...")"
if apt-cache search ceph-common | grep -q "ceph-common"; then
msg_ok "$(translate "Ceph packages are available")"
else
msg_warn "$(translate "Ceph packages may not be available, but continuing installation...")"
fi
tput civis
tput sc
pmx_record_execution "Install Ceph packages" "pveceph install"
(pveceph install 2>&1 | \
while IFS= read -r line; do
if [[ $line == *"Installing"* ]] || [[ $line == *"Unpacking"* ]] || [[ $line == *"Setting up"* ]] || [[ $line == *"Processing"* ]]; then
package_name=$(echo "$line" | sed -E 's/.*(Installing|Unpacking|Setting up|Processing) ([^ ]+).*/\2/' | head -c 30)
[ -z "$package_name" ] && package_name="$(translate "Ceph components")"
tput rc
tput ed
row=$(( $(tput lines) - 4 ))
tput cup $row 0; echo "$(translate "Installing Ceph packages...")"
tput cup $((row + 1)) 0; echo "──────────────────────────────────────────────"
tput cup $((row + 2)) 0; echo "$(translate "Current"): $package_name"
tput cup $((row + 3)) 0; echo "──────────────────────────────────────────────"
fi
done)
ceph_install_exit_code=$?
tput rc
tput ed
tput cnorm
msg_info "$(translate "Verifying Ceph installation...")"
sleep 3
if pveceph status &>/dev/null; then
msg_ok "$(translate "Ceph packages installed and verified successfully")"
local ceph_version_info=$(ceph --version 2>/dev/null | head -1 || echo "$(translate "Version info not available")")
msg_ok "$(translate "Installed"): $ceph_version_info"
if [ "$is_pve9" = true ]; then
if pveceph pool ls &>/dev/null 2>&1 || [ $? -eq 2 ]; then
msg_ok "$(translate "Ceph integration with PVE 9 verified")"
else
msg_warn "$(translate "Ceph installed but integration may need configuration")"
fi
msg_success "$(translate "Ceph installation completed successfully")"
fi
elif command -v ceph >/dev/null 2>&1; then
msg_warn "$(translate "Ceph packages installed but service verification failed")"
msg_info2 "$(translate "This may be normal for a fresh installation")"
msg_success "$(translate "Ceph installation process completed")"
else
msg_warn "$(translate "Ceph installation could not be verified")"
msg_info2 "$(translate "You may need to run 'pveceph install' manually")"
msg_success "$(translate "Ceph installation process finished with warnings")"
fi
# Track install in the registry so the Uninstall menu can offer
# `apt purge ceph-*` + repo removal. Audit Tier 6 — `install_ceph` /
# `enable_ha` sin `register_tool` ni uninstall.
register_tool "ceph" true "$FUNC_VERSION"
}
# ==========================================================
_reconcile_external_zfs_arc_settings() {
local managed_conf="$1"
local backup_dir="$BASE_DIR/backups/zfs_arc"
local manifest="$backup_dir/manifest.tsv"
local conf_file backup_file tmp_file tmp_manifest post_hash
while IFS= read -r -d '' conf_file; do
[[ "$conf_file" == "$managed_conf" ]] && continue
if ! grep -Eq \
'^[[:space:]]*options[[:space:]]+zfs([[:space:]]|$).*zfs_arc_(min|max)=' \
"$conf_file" 2>/dev/null; then
continue
fi
msg_warn "$(translate "Conflicting ZFS ARC settings detected in:") $conf_file"
mkdir -p "$backup_dir" || return 1
touch "$manifest" || return 1
backup_file="$backup_dir/$(basename "$conf_file").before-proxmenux"
if [[ ! -f "$backup_file" ]]; then
cp -p "$conf_file" "$backup_file" || return 1
fi
tmp_file=$(mktemp "${conf_file}.proxmenux.XXXXXX") || return 1
cp -p "$conf_file" "$tmp_file" || {
rm -f "$tmp_file"
return 1
}
# Remove only zfs_arc_min/max tokens from active `options zfs`
# directives. Other ZFS module options and comments stay untouched.
awk '
/^[[:space:]]*options[[:space:]]+zfs([[:space:]]|$)/ {
line = $0
gsub(/[[:space:]]+zfs_arc_(min|max)=[^[:space:]#]+/, "", line)
if (line ~ /^[[:space:]]*options[[:space:]]+zfs[[:space:]]*$/) {
next
}
if (line ~ /^[[:space:]]*options[[:space:]]+zfs[[:space:]]*#/) {
sub(/^[[:space:]]*options[[:space:]]+zfs[[:space:]]*/, "", line)
}
print line
next
}
{ print }
' "$conf_file" > "$tmp_file" || {
rm -f "$tmp_file"
return 1
}
mv -f "$tmp_file" "$conf_file" || return 1
post_hash=$(sha256sum "$conf_file" | awk '{print $1}')
tmp_manifest=$(mktemp "${manifest}.XXXXXX") || return 1
awk -F '\t' -v path="$conf_file" '$1 != path' "$manifest" > "$tmp_manifest"
printf '%s\t%s\t%s\n' "$conf_file" "$backup_file" "$post_hash" >> "$tmp_manifest"
mv -f "$tmp_manifest" "$manifest" || return 1
msg_ok "$(translate "Conflicting ZFS ARC settings backed up and reconciled:") $conf_file"
done < <(find /etc/modprobe.d -maxdepth 1 -type f -name '*.conf' -print0 2>/dev/null)
}
optimize_zfs_arc() {
local FUNC_VERSION="1.3"
# description: Cap ZFS ARC max using Proxmox VE's 10%-of-RAM policy (16 GiB ceiling), safely reconcile conflicting module settings and report the pool-size guideline before applying it.
local zfs_conf="/etc/modprobe.d/99-zfsarc.conf"
local gib=$((1024 * 1024 * 1024))
local tib=$((1024 * 1024 * 1024 * 1024))
local ram_kib ram_bytes arc_max current_arc_max pool_bytes=0 pool_size
local pool_tib pool_guideline arc_max_human current_arc_max_human pool_guideline_human
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
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
ram_kib=$(awk '/MemTotal:/ { print $2; exit }' /proc/meminfo)
if [[ ! "$ram_kib" =~ ^[0-9]+$ || "$ram_kib" -le 0 ]]; then
msg_error "$(translate "Unable to determine the installed memory.")"
return 1
fi
ram_bytes=$((ram_kib * 1024))
arc_max=$((ram_bytes / 10))
(( arc_max > 16 * gib )) && arc_max=$((16 * gib))
(( arc_max < 64 * 1024 * 1024 )) && arc_max=$((64 * 1024 * 1024))
while read -r pool_size; do
[[ "$pool_size" =~ ^[0-9]+$ ]] || continue
pool_bytes=$((pool_bytes + pool_size))
done < <(zpool list -H -p -o size 2>/dev/null)
pool_tib=$(((pool_bytes + tib - 1) / tib))
pool_guideline=$((2 * gib + pool_tib * gib))
current_arc_max=$(awk '$1 == "c_max" { print $3; exit }' /proc/spl/kstat/zfs/arcstats 2>/dev/null || true)
if [[ ! "$current_arc_max" =~ ^[0-9]+$ ]]; then
current_arc_max=$(cat /sys/module/zfs/parameters/zfs_arc_max 2>/dev/null || true)
fi
if command -v numfmt >/dev/null 2>&1; then
arc_max_human=$(numfmt --to=iec-i --suffix=B "$arc_max")
pool_guideline_human=$(numfmt --to=iec-i --suffix=B "$pool_guideline")
if [[ "$current_arc_max" =~ ^[0-9]+$ ]]; then
current_arc_max_human=$(numfmt --to=iec-i --suffix=B "$current_arc_max")
fi
fi
arc_max_human=${arc_max_human:-"$arc_max bytes"}
pool_guideline_human=${pool_guideline_human:-"$pool_guideline bytes"}
current_arc_max_human=${current_arc_max_human:-${current_arc_max:-unknown}}
msg_info "$(translate "Current effective ZFS ARC maximum:") $current_arc_max_human"
msg_info "$(translate "Proposed ZFS ARC maximum:") $arc_max_human"
if (( arc_max < pool_guideline )); then
msg_warn "$(translate "The proposed ARC maximum is below Proxmox VE's pool-size guideline:") $pool_guideline_human"
msg_info2 "$(translate "Consider adding RAM or reducing the host workload if ZFS performance is insufficient.")"
fi
if ! _reconcile_external_zfs_arc_settings "$zfs_conf"; then
msg_error "$(translate "Failed to reconcile conflicting ZFS ARC settings.")"
return 1
fi
if [[ -f "$zfs_conf" && ! -f "${zfs_conf}.bak" ]]; then
cp -p "$zfs_conf" "${zfs_conf}.bak"
fi
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
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_human"
msg_success "$(translate "ZFS ARC optimization completed")"
register_tool "zfs_arc" true "$FUNC_VERSION"
}
# ==========================================================
enable_zfs_autotrim() {
local FUNC_VERSION="1.0"
# description: Enable ZFS autotrim on detected pools and record only pools changed by ProxMenux.
local state_file="$BASE_DIR/zfs_autotrim_pools"
local tmp_file="${state_file}.tmp"
local pools=()
local pool current
local changed=false
pool_supports_autotrim() {
local pool_name="$1"
local vdev dev_path block_device rotational discard_granularity
local found_device=false
while read -r vdev; do
[[ -z "$vdev" ]] && continue
found_device=true
dev_path=$(readlink -f "$vdev" 2>/dev/null || true)
if [[ -z "$dev_path" || ! -b "$dev_path" ]]; then
return 1
fi
block_device=$(lsblk -no PKNAME "$dev_path" 2>/dev/null | head -n1)
[[ -z "$block_device" ]] && block_device=$(basename "$dev_path")
rotational=$(cat "/sys/block/$block_device/queue/rotational" 2>/dev/null || true)
discard_granularity=$(cat "/sys/block/$block_device/queue/discard_granularity" 2>/dev/null || true)
if [[ "$rotational" != "0" || -z "$discard_granularity" || "$discard_granularity" == "0" ]]; then
return 1
fi
done < <(
zpool status -P "$pool_name" 2>/dev/null |
awk '
$1 == "NAME" { in_config=1; next }
in_config && $1 == "errors:" { exit }
in_config && $1 ~ /^\// && $2 ~ /^(ONLINE|DEGRADED|FAULTED|OFFLINE|UNAVAIL|REMOVED)$/ { print $1 }
'
)
[[ "$found_device" == true ]]
}
if ! command -v zpool >/dev/null 2>&1; then
msg_info2 "$(translate "ZFS not detected. Skipping ZFS autotrim.")"
return 0
fi
mapfile -t pools < <(zpool list -H -o name 2>/dev/null)
if [[ ${#pools[@]} -eq 0 ]]; then
msg_info2 "$(translate "No ZFS pools detected. Skipping ZFS autotrim.")"
return 0
fi
msg_info "$(translate "Checking ZFS autotrim configuration...")"
mkdir -p "$BASE_DIR"
: > "$tmp_file"
for pool in "${pools[@]}"; do
current=$(zpool get -H -o value autotrim "$pool" 2>/dev/null || true)
if [[ "$current" == "on" ]]; then
msg_ok "$(translate "ZFS autotrim already enabled for pool:") $pool"
continue
fi
if [[ "$current" != "off" ]]; then
msg_warn "$(translate "ZFS autotrim is not supported for pool:") $pool"
continue
fi
if ! pool_supports_autotrim "$pool"; then
stop_spinner
msg_info2 "$(translate "Pool does not appear to use SSD/NVMe devices with discard support. Skipping ZFS autotrim for pool:") $pool"
continue
fi
if zpool set autotrim=on "$pool" >/dev/null 2>&1; then
printf '%s\n' "$pool" >> "$tmp_file"
changed=true
msg_ok "$(translate "ZFS autotrim enabled for pool:") $pool"
else
msg_warn "$(translate "Failed to enable ZFS autotrim for pool:") $pool"
fi
done
if [[ "$changed" == true ]]; then
if [[ -s "$state_file" ]]; then
sort -u "$state_file" "$tmp_file" > "${tmp_file}.merged"
mv "${tmp_file}.merged" "$state_file"
rm -f "$tmp_file"
else
mv "$tmp_file" "$state_file"
fi
register_tool "zfs_autotrim" true "$FUNC_VERSION"
else
rm -f "$tmp_file"
fi
msg_success "$(translate "ZFS autotrim setup completed")"
}
install_zfs_auto_snapshot() {
local FUNC_VERSION="1.0"
# description: Install zfs-auto-snapshot with cron schedules for hourly/daily/weekly/monthly snapshots.
msg_info2 "$(translate "Installing and configuring ZFS auto-snapshot...")"
# Check if zfs-auto-snapshot is already installed
if command -v zfs-auto-snapshot >/dev/null 2>&1; then
msg_ok "$(translate "zfs-auto-snapshot is already installed")"
else
# Install zfs-auto-snapshot
msg_info "$(translate "Installing zfs-auto-snapshot package...")"
if /usr/bin/env DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::='--force-confdef' install zfs-auto-snapshot > /dev/null 2>&1; then
msg_ok "$(translate "zfs-auto-snapshot installed successfully")"
else
msg_error "$(translate "Failed to install zfs-auto-snapshot")"
return 1
fi
fi
# Configure snapshot schedules
config_zfs_auto_snapshot
msg_success "$(translate "ZFS auto-snapshot installation and configuration completed")"
register_tool "zfs_auto_snapshot" true "$FUNC_VERSION"
}
config_zfs_auto_snapshot() {
msg_info "$(translate "Configuring snapshot schedules...")"
# Update 15-minute snapshots
update_snapshot_schedule "/etc/cron.d/zfs-auto-snapshot" "frequent" "4" "*/15"
# Update other snapshot schedules
update_snapshot_schedule "/etc/cron.hourly/zfs-auto-snapshot" "hourly" "1"
update_snapshot_schedule "/etc/cron.daily/zfs-auto-snapshot" "daily" "1"
update_snapshot_schedule "/etc/cron.weekly/zfs-auto-snapshot" "weekly" "1"
update_snapshot_schedule "/etc/cron.monthly/zfs-auto-snapshot" "monthly" "1"
}
update_snapshot_schedule() {
local config_file="$1"
local schedule_type="$2"
local keep_value="$3"
local frequency="$4"
pmx_journal_context "update_snapshot_schedule" "$FUNC_VERSION"
if [ -f "$config_file" ]; then
if ! grep -q ".*--keep=$keep_value" "$config_file"; then
if [ -n "$frequency" ]; then
pmx_edit_file "$config_file" "s|^\*/[0-9]*.*--keep=[0-9]*|$frequency * * * * root /usr/sbin/zfs-auto-snapshot --quiet --syslog --label=$schedule_type --keep=$keep_value|"
else
pmx_edit_file "$config_file" "s|--keep=[0-9]*|--keep=$keep_value|g"
fi
msg_ok "$(translate "Updated $schedule_type snapshot schedule")"
else
msg_ok "$(translate "$schedule_type snapshot schedule already configured")"
fi
fi
}
# ==========================================================
disable_rpc() {
local FUNC_VERSION="1.1"
# description: Disable rpcbind service/socket while preserving their exact previous systemd state for rollback.
local state_file="$BASE_DIR/rpcbind.state"
local state_tmp="${state_file}.tmp.$$"
local unit load_state enabled_state active_state
msg_info2 "$(translate "Disabling portmapper/rpcbind for security...")"
mkdir -p "$BASE_DIR"
if [[ ! -s "$state_file" ]]; then
: > "$state_tmp"
for unit in rpcbind.socket rpcbind.service; do
load_state="$(systemctl show -p LoadState --value "$unit" 2>/dev/null || true)"
[[ -z "$load_state" || "$load_state" == "not-found" ]] && continue
enabled_state="$(systemctl is-enabled "$unit" 2>/dev/null || true)"
active_state="$(systemctl is-active "$unit" 2>/dev/null || true)"
printf '%s|%s|%s\n' "$unit" "${enabled_state:-unknown}" "${active_state:-unknown}" >> "$state_tmp"
done
if [[ ! -s "$state_tmp" ]]; then
rm -f "$state_tmp"
msg_warn "$(translate "rpcbind units were not found; no changes were made")"
return 0
fi
mv "$state_tmp" "$state_file"
fi
# Register as soon as the original state is safely persisted. If a
# later systemd operation fails, Uninstall Optimizations must still
# expose the recovery path instead of leaving a hidden partial change.
register_tool "rpc" true "$FUNC_VERSION"
msg_info "$(translate "Disabling and stopping rpcbind service and socket...")"
pmx_journal_context "disable_rpc" "$FUNC_VERSION"
pmx_disable_service rpcbind.socket || true
pmx_disable_service rpcbind.service || true
for unit in rpcbind.socket rpcbind.service; do
active_state="$(systemctl is-active "$unit" 2>/dev/null || true)"
enabled_state="$(systemctl is-enabled "$unit" 2>/dev/null || true)"
if [[ "$active_state" == "active" || "$active_state" == "activating" ||
"$enabled_state" == "enabled" || "$enabled_state" == "enabled-runtime" ]]; then
msg_error "$(translate "rpcbind could not be disabled completely")"
return 1
fi
done
msg_ok "$(translate "rpcbind service and socket have been disabled and stopped")"
msg_success "$(translate "portmapper/rpcbind has been disabled")"
}
# ==========================================================
configure_pigz() {
local FUNC_VERSION="1.0"
pmx_journal_context "configure_pigz" "$FUNC_VERSION"
# description: Replace gzip with pigz (parallel implementation) for faster vzdump backup compression.
msg_info2 "$(translate "Configuring pigz as a faster replacement for gzip...")"
# Enable pigz in vzdump configuration
msg_info "$(translate "Enabling pigz in vzdump configuration...")"
if ! grep -q "^pigz: 1" /etc/vzdump.conf; then
pmx_edit_file /etc/vzdump.conf "s/#pigz:.*/pigz: 1/"
msg_ok "$(translate "pigz enabled in vzdump configuration")"
else
msg_ok "$(translate "pigz enabled in vzdump configuration")"
fi
# Install pigz
if ! dpkg -s pigz >/dev/null 2>&1; then
msg_info "$(translate "Installing pigz...")"
if pmx_install_pkg pigz; then
msg_ok "$(translate "pigz installed successfully")"
else
msg_error "$(translate "Failed to install pigz")"
return 1
fi
else
msg_ok "$(translate "pigz installed successfully")"
fi
# Create pigz wrapper script
msg_info "$(translate "Creating pigz wrapper script...")"
if [ ! -f /bin/pigzwrapper ] || ! cmp -s /bin/pigzwrapper - <<EOF
#!/bin/sh
PATH=/bin:\$PATH
GZIP="-1"
exec /usr/bin/pigz "\$@"
EOF
then
pmx_write_file /bin/pigzwrapper <<EOF
#!/bin/sh
PATH=/bin:\$PATH
GZIP="-1"
exec /usr/bin/pigz "\$@"
EOF
chmod +x /bin/pigzwrapper
msg_ok "$(translate "pigz wrapper script created")"
else
msg_ok "$(translate "pigz wrapper script created")"
fi
# Replace gzip with pigz wrapper
msg_info "$(translate "Replacing gzip with pigz wrapper...")"
if [ ! -f /bin/gzip.original ]; then
mv -f /bin/gzip /bin/gzip.original && \
pmx_write_file /bin/gzip < /bin/pigzwrapper && \
chmod +x /bin/gzip
msg_ok "$(translate "gzip replaced with pigz wrapper successfully")"
else
msg_ok "$(translate "gzip replaced with pigz wrapper successfully")"
fi
msg_success "$(translate "pigz configuration completed")"
register_tool "pigz" true "$FUNC_VERSION"
}
# ==========================================================
# ==========================================================
# ==========================================================
install_guest_agent() {
local FUNC_VERSION="1.0"
# description: Detect the host's hypervisor (qemu/vmware/hyperv/virtualbox) and install the matching guest agent.
msg_info2 "$(translate "Detecting virtualization and installing guest agent...")"
NECESSARY_REBOOT=1
local virt_env=""
local guest_agent=""
# Detect virtualization environment
if [ "$(dmidecode -s system-manufacturer | xargs)" == "QEMU" ] || [ "$(systemd-detect-virt | xargs)" == "kvm" ]; then
virt_env="QEMU/KVM"
guest_agent="qemu-guest-agent"
elif [ "$(systemd-detect-virt | xargs)" == "vmware" ]; then
virt_env="VMware"
guest_agent="open-vm-tools"
elif [ "$(systemd-detect-virt | xargs)" == "oracle" ]; then
virt_env="VirtualBox"
guest_agent="virtualbox-guest-utils"
else
msg_ok "$(translate "Guest agent detection completed")"
msg_success "$(translate "Guest agent installation process completed")"
return
fi
# Install guest agent
if [ -n "$guest_agent" ]; then
msg_info "$(translate "Installing $guest_agent for $virt_env...")"
if /usr/bin/env DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::='--force-confdef' install $guest_agent > /dev/null 2>&1; then
msg_ok "$(translate "$guest_agent installed successfully")"
# Persist which package was installed so the uninstaller knows
# what to apt purge later (different per hypervisor).
mkdir -p /usr/local/share/proxmenux 2>/dev/null
echo "$guest_agent" > /usr/local/share/proxmenux/guest_agent.pkg
register_tool "guest_agent" true "$FUNC_VERSION"
else
msg_error "$(translate "Failed to install $guest_agent")"
fi
fi
msg_success "$(translate "Guest agent installation process completed")"
}
# ==========================================================
# ==========================================================
enable_vfio_iommu() {
local FUNC_VERSION="1.0"
pmx_journal_context "enable_vfio_iommu" "$FUNC_VERSION"
# description: Enable IOMMU and load VFIO modules to allow GPU/PCI passthrough into VMs.
msg_info2 "$(translate "Enabling IOMMU and configuring VFIO for PCI passthrough...")"
NECESSARY_REBOOT=1
# Detect if system uses ZFS/systemd-boot (Proxmox)
local uses_zfs=false
local cmdline_file="/etc/kernel/cmdline"
if [[ -f "$cmdline_file" ]] && grep -qE 'root=ZFS=|root=ZFS/' "$cmdline_file"; then
uses_zfs=true
fi
if [[ "$uses_zfs" == true ]] && [[ -f "$cmdline_file" ]]; then
msg_info "$(translate "Cleaning up duplicate parameters...")"
cp "$cmdline_file" "${cmdline_file}.cleanup.bak"
pmx_edit_file "$cmdline_file" 's/intel_iommu=on[[:space:]]*intel_iommu=on/intel_iommu=on/g'
pmx_edit_file "$cmdline_file" 's/amd_iommu=on[[:space:]]*amd_iommu=on/amd_iommu=on/g'
pmx_edit_file "$cmdline_file" 's/iommu=pt[[:space:]]*iommu=pt/iommu=pt/g'
msg_ok "$(translate "Duplicate parameters cleaned")"
fi
# Detect CPU type and set IOMMU parameter
local cpu_info=$(cat /proc/cpuinfo)
local iommu_param=""
local grub_file="/etc/default/grub"
local additional_params="pcie_acs_override=downstream,multifunction"
if [[ "$cpu_info" == *"GenuineIntel"* ]]; then
msg_info "$(translate "Detected Intel CPU")"
iommu_param="intel_iommu=on"
elif [[ "$cpu_info" == *"AuthenticAMD"* ]]; then
msg_info "$(translate "Detected AMD CPU")"
iommu_param="amd_iommu=on"
else
msg_warning "$(translate "Unknown CPU type. IOMMU might not be properly enabled.")"
return 1
fi
# Configure /etc/kernel/cmdline or GRUB
if [[ "$uses_zfs" == true ]]; then
# SYSTEMD-BOOT - Verificación mejorada
local needs_iommu_param=false
local needs_iommu_pt=false
local needs_additional=false
# Verificar qué parámetros faltan
if ! grep -q "$iommu_param" "$cmdline_file"; then
needs_iommu_param=true
fi
if ! grep -q "iommu=pt" "$cmdline_file"; then
needs_iommu_pt=true
fi
if ! grep -q "pcie_acs_override=" "$cmdline_file"; then
needs_additional=true
fi
# Solo agregar lo que falta
if [[ "$needs_iommu_param" == true ]] || [[ "$needs_iommu_pt" == true ]] || [[ "$needs_additional" == true ]]; then
cp "$cmdline_file" "${cmdline_file}.bak"
local params_to_add=""
[[ "$needs_iommu_param" == true ]] && params_to_add+=" $iommu_param"
[[ "$needs_iommu_pt" == true ]] && params_to_add+=" iommu=pt"
[[ "$needs_additional" == true ]] && params_to_add+=" $additional_params"
pmx_edit_file "$cmdline_file" "s|\s*$|$params_to_add|"
msg_ok "$(translate "IOMMU parameters added to /etc/kernel/cmdline")"
else
msg_ok "$(translate "IOMMU already configured in /etc/kernel/cmdline")"
fi
else
# GRUB - Verificación mejorada
local needs_update=false
if ! grep -q "$iommu_param" "$grub_file" || ! grep -q "iommu=pt" "$grub_file"; then
needs_update=true
fi
if [[ "$needs_update" == true ]]; then
cp "$grub_file" "${grub_file}.bak"
# Agregar parámetros que falten
local current_line=$(grep "GRUB_CMDLINE_LINUX_DEFAULT=" "$grub_file")
local params_to_add=""
if ! echo "$current_line" | grep -q "$iommu_param"; then
params_to_add+=" $iommu_param"
fi
if ! echo "$current_line" | grep -q "iommu=pt"; then
params_to_add+=" iommu=pt"
fi
if ! echo "$current_line" | grep -q "pcie_acs_override="; then
params_to_add+=" $additional_params"
fi
pmx_edit_file "$grub_file" "/GRUB_CMDLINE_LINUX_DEFAULT=/ s|\"$|$params_to_add\"|"
msg_ok "$(translate "IOMMU enabled in GRUB configuration")"
else
msg_ok "$(translate "IOMMU already enabled in GRUB configuration")"
fi
fi
# Configure VFIO modules
local modules_file="/etc/modules"
msg_info "$(translate "Checking VFIO modules...")"
# vfio_virqfd was merged into the vfio module in kernel 6.2+
# Adding it as a separate module on kernel >= 6.2 generates warnings
local kernel_major kernel_minor
kernel_major=$(uname -r | cut -d. -f1)
kernel_minor=$(uname -r | cut -d. -f2)
local vfio_modules=("vfio" "vfio_iommu_type1" "vfio_pci")
if (( kernel_major < 6 || ( kernel_major == 6 && kernel_minor < 2 ) )); then
vfio_modules+=("vfio_virqfd")
fi
for module in "${vfio_modules[@]}"; do
if ! grep -q "^$module" "$modules_file"; then
echo "$module" | pmx_append_file "$modules_file"
fi
done
msg_ok "$(translate "VFIO modules configured.")"
# Blacklist conflicting drivers (sin cambios)
local blacklist_file="/etc/modprobe.d/blacklist.conf"
msg_info "$(translate "Checking conflicting drivers blacklist...")"
[[ -f "$blacklist_file" ]] || pmx_write_file "$blacklist_file" < /dev/null
local blacklist_drivers=("nouveau" "lbm-nouveau" "radeon" "nvidia" "nvidiafb")
for driver in "${blacklist_drivers[@]}"; do
if ! grep -q "^blacklist $driver" "$blacklist_file"; then
echo "blacklist $driver" | pmx_append_file "$blacklist_file"
fi
done
if ! grep -q "options nouveau modeset=0" "$blacklist_file"; then
echo "options nouveau modeset=0" | pmx_append_file "$blacklist_file"
fi
msg_ok "$(translate "Conflicting drivers blacklisted successfully.")"
# Update initramfs and bootloader
msg_info "$(translate "Updating initramfs, GRUB, and EFI boot, patience...")"
pmx_record_execution "Regenerate initramfs" "update-initramfs -u -k all"
update-initramfs -u -k all > /dev/null 2>&1
if [[ "$uses_zfs" == true ]]; then
pmx_record_execution "Refresh Proxmox boot configuration" "proxmox-boot-tool refresh"
proxmox-boot-tool refresh > /dev/null 2>&1
else
pmx_record_execution "Regenerate GRUB configuration" "update-grub"
update-grub > /dev/null 2>&1
fi
msg_success "$(translate "IOMMU and VFIO setup completed")"
register_tool "vfio_iommu" true "$FUNC_VERSION"
}
# ==========================================================
_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.2"
pmx_journal_context "customize_bashrc" "$FUNC_VERSION"
# 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...")"
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 prompt_path_style="${PMX_BASHRC_PATH_STYLE:-}"
local short_state="on"
local full_state="off"
local choice=""
local detected_path_style="short"
[[ -f "$bashrc" ]] || pmx_write_file "$bashrc" < /dev/null
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, 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"
fi
case "$prompt_path_style" in
short)
prompt_path_escape='\W'
;;
full)
prompt_path_escape='\w'
;;
"")
if [[ -t 0 && -t 1 ]] && command -v whiptail >/dev/null 2>&1; then
if ! choice=$(whiptail \
--title "$(translate "Bash prompt path")" \
--radiolist "$(translate "Choose how the current directory is shown in the Bash prompt:")" \
14 76 2 \
"short" "$(translate "Current directory only") (\\W)" "$short_state" \
"full" "$(translate "Full path") (\\w)" "$full_state" \
3>&1 1>&2 2>&3); then
msg_warn "$(translate "Cancelled by user.")"
return 1
fi
[[ "$choice" == "full" ]] && prompt_path_escape='\w' || prompt_path_escape='\W'
fi
;;
*)
msg_error "PMX_BASHRC_PATH_STYLE must be 'short' or 'full'."
return 1
;;
esac
[ -f "${bashrc}.bak" ] || cp "$bashrc" "${bashrc}.bak" > /dev/null 2>&1
local migrated_bashrc
migrated_bashrc="$(mktemp)"
cp -p "$bashrc" "$migrated_bashrc"
if ! _migrate_proxmenux_bashrc "$migrated_bashrc" migrate >/dev/null; then
rm -f "$migrated_bashrc"
msg_error "$(translate "Failed to migrate the existing ProxMenux Bash configuration.")"
return 1
fi
pmx_write_file "$bashrc" < "$migrated_bashrc"
rm -f "$migrated_bashrc"
pmx_append_file "$bashrc" << EOF
${marker_begin}
# ProxMenux core customizations
export HISTTIMEFORMAT="%d/%m/%y %T "
export PS1="\[\e[31m\][\[\e[m\]\[\e[38;5;172m\]\u\[\e[m\]@\[\e[38;5;153m\]\h\[\e[m\] \[\e[38;5;214m\]${prompt_path_escape}\[\e[m\]\[\e[31m\]]\[\e[m\]\\$ "
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_end}
EOF
if ! grep -q "source /root/.bashrc" "$bash_profile" 2>/dev/null; then
echo "source /root/.bashrc" | pmx_append_file "$bash_profile" 2>/dev/null
fi
msg_ok "$(translate "Bashrc customization completed")"
msg_info "$(translate "The new prompt will be used in new terminal sessions.")"
msg_info "$(translate "To apply it to the current shell now, run:") source /root/.bashrc"
register_tool "bashrc_custom" true "$FUNC_VERSION"
}
# ==========================================================
setup_motd() {
local FUNC_VERSION="1.0"
pmx_journal_context "setup_motd" "$FUNC_VERSION"
# description: Add the ProxMenux MOTD banner while preserving the original file contents or absence for rollback.
msg_info2 "$(translate "Configuring MOTD (Message of the Day) banner...")"
local motd_file="${PROXMENUX_MOTD_FILE:-/etc/motd}"
local custom_message=" This system is optimised by: ProxMenux"
local state_file="$BASE_DIR/motd.state"
local original_file="$BASE_DIR/motd.original"
local changes_made=false
msg_info "$(translate "Checking MOTD configuration...")"
mkdir -p "$BASE_DIR"
if [[ ! -f "$state_file" ]]; then
if grep -Fqx "$custom_message" "$motd_file" 2>/dev/null; then
if [[ -f "${motd_file}.bak" ]]; then
cp -a "${motd_file}.bak" "$original_file"
printf 'present\n' > "$state_file"
else
printf 'legacy-marker\n' > "$state_file"
fi
elif [[ -e "$motd_file" ]]; then
cp -a "$motd_file" "$original_file"
printf 'present\n' > "$state_file"
else
printf 'absent\n' > "$state_file"
fi
fi
# Check if the custom message already exists
if grep -Fqx "$custom_message" "$motd_file" 2>/dev/null; then
msg_ok "$(translate "Custom MOTD message is already configured")"
else
# Add the custom message at the beginning of the file
[[ -f "$motd_file" ]] || pmx_write_file "$motd_file" < /dev/null
local motd_tmp
motd_tmp="$(mktemp)"
{
printf '%s\n\n' "$custom_message"
cat "$motd_file"
} > "$motd_tmp"
pmx_write_file "$motd_file" < "$motd_tmp"
rm -f "$motd_tmp"
changes_made=true
msg_ok "$(translate "Custom message added to MOTD")"
fi
pmx_edit_file "$motd_file" '/^$/N;/^\n$/D'
if $changes_made; then
msg_success "$(translate "MOTD configuration updated successfully")"
else
msg_success "$(translate "MOTD configuration was already up to date")"
fi
register_tool "motd" true "$FUNC_VERSION"
}
# ==========================================================
optimize_logrotate() {
local FUNC_VERSION="1.1"
# description: Replace logrotate.conf with a Log2RAM-friendly profile (daily rotation, copytruncate).
msg_info2 "$(translate "Optimizing logrotate configuration...")"
local logrotate_conf="/etc/logrotate.conf"
local backup_conf="${logrotate_conf}.bak"
# The .bak stays until reverting from the journal exists:
# uninstall_logrotate restores from it, and migrating the write must
# not quietly disable the rollback that is already shipping.
cp -n "$logrotate_conf" "$backup_conf" 2>/dev/null || true
msg_info "$(translate "Applying optimized logrotate configuration...")"
pmx_journal_context "optimize_logrotate" "$FUNC_VERSION"
pmx_write_file "$logrotate_conf" <<EOF
# ProxMenux optimized configuration (Log2RAM-friendly)
daily
su root adm
rotate 7
size 10M
compress
delaycompress
missingok
notifempty
create 0640 root adm
copytruncate
include /etc/logrotate.d
EOF
systemctl restart logrotate > /dev/null 2>&1
msg_ok "$(translate "Logrotate service restarted successfully")"
register_tool "logrotate" true "$FUNC_VERSION"
msg_success "$(translate "Logrotate optimization completed")"
}
# ==========================================================
remove_subscription_banner() {
local FUNC_VERSION="1.1"
# description: Patch the Proxmox web UI to suppress the "no valid subscription" dialog (PVE 8 + 9 variants supported).
local pve_version
pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+' | head -1)
if [[ -z "$pve_version" ]]; then
msg_error "Unable to detect Proxmox version."
return 1
fi
if [[ "$pve_version" -ge 9 ]]; then
if ! bash "$LOCAL_SCRIPTS/global/remove-banner-pve-v3.sh"; then
msg_error "$(translate "Subscription banner removal failed")"
return 1
fi
else
if ! bash "$LOCAL_SCRIPTS/global/remove-banner-pve8.sh"; then
msg_error "$(translate "Subscription banner removal failed")"
return 1
fi
fi
register_tool "subscription_banner" true "$FUNC_VERSION"
}
# ==========================================================
optimize_memory_settings() {
local FUNC_VERSION="1.2"
# description: Tune swappiness, dirty page ratios and compaction proactiveness for VM hosts without overriding the kernel's memory-overcommit policy.
msg_info2 "$(translate "Optimizing memory settings...")"
NECESSARY_REBOOT=1
local sysctl_conf="/etc/sysctl.d/99-memory.conf"
if [ -f "$sysctl_conf" ] && grep -q "Memory Optimising" "$sysctl_conf"; then
msg_info "$(translate "Old memory configuration detected. Replacing with balanced optimization...")"
else
msg_info "$(translate "Applying balanced memory optimization settings...")"
fi
# Composed in full before writing: the journal records the file as
# it ends up, not a write followed by an append.
local memory_settings
memory_settings="$(cat <<EOF
# Balanced Memory Optimization
# Improve responsiveness without excessive memory reservation
# Avoid unnecessary swapping
vm.swappiness = 10
# Lower dirty memory thresholds to free memory faster
vm.dirty_ratio = 15
vm.dirty_background_ratio = 5
# Avoid excessive virtual memory areas (safe for most applications)
vm.max_map_count = 262144
EOF
)"
if [ -f /proc/sys/vm/compaction_proactiveness ]; then
memory_settings+=$'\n''vm.compaction_proactiveness = 20'
msg_ok "$(translate "Enabled memory compaction proactiveness")"
fi
pmx_journal_context "optimize_memory_settings" "$FUNC_VERSION"
printf '%s\n' "$memory_settings" | pmx_write_file "$sysctl_conf"
msg_ok "$(translate "Memory settings optimized successfully")"
msg_success "$(translate "Memory optimization completed.")"
register_tool "memory_settings" true "$FUNC_VERSION"
}
# ==========================================================
optimize_vzdump() {
local FUNC_VERSION="1.0"
pmx_journal_context "optimize_vzdump" "$FUNC_VERSION"
# description: Lift vzdump bandwidth/IO limits so backups run at the storage's real throughput.
msg_info2 "$(translate "Optimizing vzdump backup speed...")"
local vzdump_conf="/etc/vzdump.conf"
# Backup the current config so the uninstall path can restore the
# user's original values. The previous code edited in-place with no
# backup; users with custom bwlimit/ionice lost them silently.
if [[ -f "$vzdump_conf" && ! -f "${vzdump_conf}.bak" ]]; then
cp -p "$vzdump_conf" "${vzdump_conf}.bak"
fi
# Configure bandwidth limit
msg_info "$(translate "Configuring bandwidth limit for vzdump...")"
if ! grep -q "^bwlimit: 0" "$vzdump_conf"; then
pmx_edit_file "$vzdump_conf" '/^#*bwlimit:/d'
echo "bwlimit: 0" | pmx_append_file "$vzdump_conf"
fi
msg_ok "$(translate "Bandwidth limit configured")"
# Configure I/O priority
msg_info "$(translate "Configuring I/O priority for vzdump...")"
if ! grep -q "^ionice: 5" "$vzdump_conf"; then
pmx_edit_file "$vzdump_conf" '/^#*ionice:/d'
echo "ionice: 5" | pmx_append_file "$vzdump_conf"
fi
msg_ok "$(translate "I/O priority configured")"
msg_success "$(translate "vzdump backup speed optimization completed")"
register_tool "vzdump_speed" true "$FUNC_VERSION"
}
# ==========================================================
install_ovh_rtm() {
local FUNC_VERSION="1.0"
# description: Detect OVH-rented hardware via whois lookup and install OVH Real-Time Monitoring (no-op on non-OVH).
msg_info2 "$(translate "Detecting if this is an OVH server and installing OVH RTM if necessary...")"
# Get the public IP and check if it belongs to OVH
msg_info "$(translate "Checking if the server belongs to OVH...")"
public_ip=$(curl -s ipinfo.io/ip)
# `--` ends whois client option parsing so "-t IP" reaches the cymru server
# as the query string. Previous form had a leading space inside the quotes
# ("\" -t IP\"") that mangled the query and caused detection to never match.
is_ovh=$(whois -h v4.whois.cymru.com -- "-t $public_ip" | tail -n 1 | cut -d'|' -f3 | grep -i "ovh")
if [ -n "$is_ovh" ]; then
msg_ok "$(translate "OVH server detected")"
msg_info "$(translate "Installing OVH RTM (Real Time Monitoring)...")"
if wget -qO - https://last-public-ovh-infra-yak.snap.mirrors.ovh.net/yak/archives/apply.sh | OVH_PUPPET_MANIFEST=distribyak/catalog/master/puppet/manifests/common/rtmv2.pp bash > /dev/null 2>&1; then
msg_ok "$(translate "OVH RTM installed successfully")"
register_tool "ovh_rtm" true "$FUNC_VERSION"
else
msg_error "$(translate "Failed to install OVH RTM")"
fi
else
msg_ok "$(translate "Not an OVH server, skipping RTM installation")"
fi
msg_success "$(translate "OVH server detection and RTM installation process completed")"
}
# ==========================================================
enable_ha() {
local FUNC_VERSION="1.0"
# description: Enable the Proxmox HA stack (pve-ha-lrm, pve-ha-crm, corosync) for cluster failover.
msg_info2 "$(translate "Enabling High Availability (HA) services...")"
NECESSARY_REBOOT=1
msg_info "$(translate "Enabling High Availability (HA) services...")"
# Enable all necessary services
systemctl enable -q --now pve-ha-lrm pve-ha-crm corosync &>/dev/null
msg_ok "$(translate "High Availability services have been enabled successfully")"
msg_success "$(translate "High Availability setup completed")"
register_tool "ha" true "$FUNC_VERSION"
}
# ==========================================================
configure_fastfetch() {
local FUNC_VERSION="1.1"
pmx_journal_context "configure_fastfetch" "$FUNC_VERSION"
# description: Install Fastfetch system summary tool with the ProxMenux logo + status block as the SSH login banner.
msg_info2 "$(translate "Installing and configuring Fastfetch...")"
# Define paths
local fastfetch_bin="/usr/local/bin/fastfetch"
local fastfetch_config_dir="$HOME/.config/fastfetch"
local logos_dir="/usr/local/share/fastfetch/logos"
local fastfetch_config="$fastfetch_config_dir/config.jsonc"
apply_fastfetch_config() {
local config_tmp status
config_tmp="$(mktemp)"
if jq "$@" "$fastfetch_config" > "$config_tmp"; then
pmx_write_file "$fastfetch_config" < "$config_tmp"
status=$?
else
status=$?
fi
rm -f "$config_tmp"
return "$status"
}
download_fastfetch_logo() {
local path="$1" url="$2"
local -a statuses
wget -qO - "$url" | pmx_write_file "$path"
statuses=("${PIPESTATUS[@]}")
[[ "${statuses[0]}" -eq 0 && "${statuses[1]}" -eq 0 ]]
}
# Ensure directories exist
mkdir -p "$fastfetch_config_dir"
mkdir -p "$logos_dir"
if command -v fastfetch &> /dev/null; then
pmx_record_execution "Remove existing Fastfetch package" "apt-get remove --purge -y fastfetch"
apt-get remove --purge -y fastfetch > /dev/null 2>&1
pmx_remove_file /usr/bin/fastfetch
pmx_remove_file /usr/local/bin/fastfetch
fi
msg_info "$(translate "Downloading the latest Fastfetch release...")"
# `--connect-timeout`/`--max-time` so a slow GitHub API call doesn't
# hang the menu indefinitely. `FASTFETCH_PIN_TAG` env var lets the
# caller pin to a known release if upstream introduces a breaking
# change. Audit Tier 6 — recursos remotos sin pinning de versión.
local fastfetch_deb_url=""
if [[ -n "${FASTFETCH_PIN_TAG:-}" ]]; then
fastfetch_deb_url="https://github.com/fastfetch-cli/fastfetch/releases/download/${FASTFETCH_PIN_TAG}/fastfetch-linux-amd64.deb"
else
fastfetch_deb_url=$(curl -s --connect-timeout 5 --max-time 15 https://api.github.com/repos/fastfetch-cli/fastfetch/releases/latest |
jq -r '.assets[] | select(.name | test("fastfetch-linux-amd64.deb")) | .browser_download_url')
fi
if [[ -z "$fastfetch_deb_url" ]]; then
msg_error "$(translate "Failed to retrieve Fastfetch download URL.")"
return 1
fi
msg_ok "$(translate "Fastfetch download URL retrieved successfully.")"
wget -qO /tmp/fastfetch.deb "$fastfetch_deb_url"
pmx_record_execution "Install Fastfetch package" "dpkg -i /tmp/fastfetch.deb"
if dpkg -i /tmp/fastfetch.deb > /dev/null 2>&1; then
pmx_record_execution "Resolve Fastfetch package dependencies" "apt-get install -f -y"
apt-get install -f -y > /dev/null 2>&1
msg_ok "$(translate "Fastfetch installed successfully")"
else
msg_error "$(translate "Failed to install Fastfetch.")"
return 1
fi
rm -f /tmp/fastfetch.deb
if ! command -v fastfetch &> /dev/null; then
msg_error "$(translate "Fastfetch is not installed correctly.")"
return 1
fi
if [ ! -f "$fastfetch_config" ]; then
echo '{"$schema": "https://github.com/fastfetch-cli/fastfetch/raw/dev/doc/json_schema.json", "modules": []}' | pmx_write_file "$fastfetch_config"
fi
pmx_record_execution "Generate Fastfetch configuration" "fastfetch --gen-config-force"
fastfetch --gen-config-force > /dev/null 2>&1
while true; do
# Define logo options
local logo_options=("ProxMenux" "Proxmox (default)" "JC Channel" "Comunidad Helper-Scripts" "Home-Labs-Club" "Proxmology" "Custom")
local choice
choice=$(whiptail --title "$(translate "Fastfetch Logo Selection")" --menu "$(translate "Choose a logo for Fastfetch:")" 20 78 7 \
"1" "${logo_options[0]}" \
"2" "${logo_options[1]}" \
"3" "${logo_options[2]}" \
"4" "${logo_options[3]}" \
"5" "${logo_options[4]}" \
"6" "${logo_options[5]}" \
"7" "${logo_options[6]}" \
3>&1 1>&2 2>&3)
case $choice in
1)
msg_info "$(translate "Downloading ProxMenux logo...")"
local proxmenux_logo_path="$logos_dir/ProxMenux.txt"
if download_fastfetch_logo "$proxmenux_logo_path" "https://raw.githubusercontent.com/MacRimi/ProxMenux/main/images/logos_txt/logo.txt"; then
apply_fastfetch_config --arg path "$proxmenux_logo_path" '. + {logo: $path}'
msg_ok "$(translate "ProxMenux logo applied")"
else
msg_error "$(translate "Failed to download ProxMenux logo")"
fi
break
;;
2)
msg_info "$(translate "Using default Proxmox logo...")"
apply_fastfetch_config 'del(.logo)'
msg_ok "$(translate "Default Proxmox logo applied")"
break
;;
3)
msg_info "$(translate "Downloading JC Channel logo...")"
local jc_channel_logo_path="$logos_dir/jc_channel.txt"
if download_fastfetch_logo "$jc_channel_logo_path" "https://raw.githubusercontent.com/MacRimi/ProxMenux/main/images/logos_txt/jc_channel.txt"; then
apply_fastfetch_config --arg path "$jc_channel_logo_path" '. + {logo: $path}'
msg_ok "$(translate "JC Channel logo applied")"
else
msg_error "$(translate "Failed to download JC Channel logo")"
fi
break
;;
4)
msg_info "$(translate "Downloading Helper-Scripts logo...")"
local helper_scripts_logo_path="$logos_dir/Helper_Scripts.txt"
if download_fastfetch_logo "$helper_scripts_logo_path" "https://raw.githubusercontent.com/MacRimi/ProxMenux/main/images/logos_txt/Helper_Scripts.txt"; then
apply_fastfetch_config --arg path "$helper_scripts_logo_path" '. + {logo: $path}'
msg_ok "$(translate "Helper-Scripts logo applied")"
else
msg_error "$(translate "Failed to download Helper-Scripts logo")"
fi
break
;;
5)
msg_info "$(translate "Downloading Home-Labs-Club logo...")"
local home_lab_club_logo_path="$logos_dir/home_labsclub.txt"
if download_fastfetch_logo "$home_lab_club_logo_path" "https://raw.githubusercontent.com/MacRimi/ProxMenux/main/images/logos_txt/home_labsclub.txt"; then
apply_fastfetch_config --arg path "$home_lab_club_logo_path" '. + {logo: $path}'
msg_ok "$(translate "Home-Lab-Club logo applied")"
else
msg_error "$(translate "Failed to download Home-Lab-Club logo")"
fi
break
;;
6)
msg_info "$(translate "Downloading Proxmology logo...")"
local proxmology_logo_path="$logos_dir/proxmology.txt"
if download_fastfetch_logo "$proxmology_logo_path" "https://raw.githubusercontent.com/MacRimi/ProxMenux/main/images/logos_txt/proxmology.txt"; then
apply_fastfetch_config --arg path "$proxmology_logo_path" '. + {logo: $path}'
msg_ok "$(translate "Proxmology logo applied")"
else
msg_error "$(translate "Failed to download Proxmology logo")"
fi
break
;;
7)
whiptail --title "$(translate "Custom Logo Instructions")" --msgbox "$(translate "To use a custom Fastfetch logo, place your ASCII logo file in:\n\n/usr/local/share/fastfetch/logos/\n\nThe file should not exceed 35 lines to fit properly in the terminal.\n\nPress OK to continue and select your logo.")" 15 70
local logo_files=($(ls "$logos_dir"/*.txt 2>/dev/null))
if [ ${#logo_files[@]} -eq 0 ]; then
whiptail --title "$(translate "No Custom Logos Found")" --msgbox "$(translate "No custom logos were found in /usr/local/share/fastfetch/logos/.\n\nPlease add a logo and try again.")" 10 60
continue
fi
local menu_items=()
local index=1
for file in "${logo_files[@]}"; do
menu_items+=("$index" "$(basename "$file")")
index=$((index+1))
done
local selected_logo_index
selected_logo_index=$(whiptail --title "$(translate "Select a Custom Logo")" --menu "$(translate "Choose a custom logo:")" 20 70 10 "${menu_items[@]}" 3>&1 1>&2 2>&3)
if [ -z "$selected_logo_index" ]; then
continue
fi
local selected_logo="${logo_files[$((selected_logo_index-1))]}"
apply_fastfetch_config --arg path "$selected_logo" '. + {logo: $path}'
msg_ok "$(translate "Custom logo applied: $(basename "$selected_logo")")"
break
;;
*)
msg_warn "$(translate "You must select a logo to continue.")"
;;
esac
done
# Modify Fastfetch modules to display custom title
msg_info "$(translate "Modifying Fastfetch configuration...")"
apply_fastfetch_config '.modules |= map(select(. != "title"))'
apply_fastfetch_config 'del(.modules[] | select(type == "object" and .type == "custom"))'
apply_fastfetch_config '.modules |= [{"type": "custom", "format": "\u001b[1;38;5;166mSystem optimised by ProxMenux\u001b[0m"}] + .'
msg_ok "$(translate "Fastfetch now displays: System optimised by: ProxMenux")"
pmx_record_execution "Generate Fastfetch configuration" "fastfetch --gen-config"
fastfetch --gen-config > /dev/null 2>&1
msg_ok "$(translate "Fastfetch configuration updated")"
pmx_edit_file "$HOME/.profile" '/fastfetch/d' 2>/dev/null || true
pmx_edit_file /etc/profile '/fastfetch/d' 2>/dev/null || true
pmx_remove_file /etc/update-motd.d/99-fastfetch
pmx_edit_file "$HOME/.bashrc" '/# BEGIN FASTFETCH/,/# END FASTFETCH/d' 2>/dev/null || true
if ! grep -q '# BEGIN FASTFETCH' "$HOME/.bashrc"; then
pmx_append_file "$HOME/.bashrc" << 'EOF'
# BEGIN FASTFETCH
# Run Fastfetch only in interactive sessions
if [[ $- == *i* ]] && command -v fastfetch &>/dev/null; then
clear
fastfetch
fi
# END FASTFETCH
EOF
fi
msg_ok "$(translate "Fastfetch will start automatically in the console")"
msg_success "$(translate "Fastfetch installation and configuration completed")"
register_tool "fastfetch" true "$FUNC_VERSION"
}
# ==========================================================
# ==========================================================
configure_figurine() {
local FUNC_VERSION="1.1"
pmx_journal_context "configure_figurine" "$FUNC_VERSION"
# description: Install Figurine (ASCII-art hostname banner) and wire it into the SSH login flow.
msg_info2 "$(translate "Installing and configuring Figurine...")"
# `FIGURINE_VERSION` env var allows pinning to a specific release;
# default tracks the last tested upstream tag. Audit Tier 6 —
# recursos remotos sin pinning de versión.
local version="${FIGURINE_VERSION:-2.0.0}"
local file="figurine_linux_amd64_v${version}.tar.gz"
local url="https://github.com/arsham/figurine/releases/download/v${version}/${file}"
local temp_dir; temp_dir=$(mktemp -d)
local install_dir="/usr/local/bin"
local profile_script="/etc/profile.d/figurine.sh"
local bin_path="${install_dir}/figurine"
local bashrc="/root/.bashrc"
cleanup_dir() { rm -rf "$temp_dir" 2>/dev/null || true; }
trap cleanup_dir EXIT
[[ -f "$bashrc" ]] || pmx_write_file "$bashrc" < /dev/null
if command -v figurine &>/dev/null; then
msg_info "$(translate "Updating Figurine binary...")"
else
msg_info "$(translate "Downloading Figurine v${version}...")"
fi
if ! wget -qO "${temp_dir}/${file}" "$url"; then
msg_error "$(translate "Failed to download Figurine")"
return 1
fi
if ! tar -xf "${temp_dir}/${file}" -C "${temp_dir}"; then
msg_error "$(translate "Failed to extract package")"
return 1
fi
msg_ok "$(translate "Extraction successful")"
if [[ ! -f "${temp_dir}/deploy/figurine" ]]; then
msg_error "$(translate "Binary not found in extracted content.")"
return 1
fi
msg_info "$(translate "Installing binary to ${install_dir}...")"
pmx_write_file "$bin_path" < "${temp_dir}/deploy/figurine"
chmod 0755 "$bin_path"
chown root:root "$bin_path"
pmx_write_file "$profile_script" << 'EOF'
/usr/local/bin/figurine -f "3d.flf" $(hostname)
EOF
chmod +x "$profile_script"
ensure_aliases() {
local bashrc="/root/.bashrc"
[[ -f "$bashrc" ]] || pmx_write_file "$bashrc" < /dev/null
if ! grep -q "shopt -s expand_aliases" "$bashrc" 2>/dev/null; then
echo "shopt -s expand_aliases" | pmx_append_file "$bashrc"
fi
local -a ALIASES=(
"aptup=apt update && apt dist-upgrade"
"lxcclean=bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/clean-lxcs.sh)\""
"lxcupdate=bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/update-lxcs.sh)\""
"kernelclean=bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/kernel-clean.sh)\""
"cpugov=bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/scaling-governor.sh)\""
"lxctrim=bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/fstrim.sh)\""
"updatecerts=pvecm updatecerts"
"seqwrite=sync; fio --randrepeat=1 --ioengine=libaio --direct=1 --name=test --filename=test --bs=4M --size=32G --readwrite=write --ramp_time=4"
"seqread=sync; fio --randrepeat=1 --ioengine=libaio --direct=1 --name=test --filename=test --bs=4M --size=32G --readwrite=read --ramp_time=4"
"ranwrite=sync; fio --randrepeat=1 --ioengine=libaio --direct=1 --name=test --filename=test --bs=4k --size=4G --readwrite=randwrite --ramp_time=4"
"ranread=sync; fio --randrepeat=1 --ioengine=libaio --direct=1 --name=test --filename=test --bs=4k --size=4G --readwrite=randread --ramp_time=4"
)
for entry in "${ALIASES[@]}"; do
local name="${entry%%=*}"
local cmd="${entry#*=}"
local safe_cmd=${cmd//\'/\'\\\'\'}
pmx_edit_file "$bashrc" -E "/^[[:space:]]*alias[[:space:]]+${name}=.*/d"
printf "alias %s='%s'\n" "$name" "$safe_cmd" | pmx_append_file "$bashrc"
done
. "$bashrc"
}
ensure_aliases
msg_ok "$(translate "Aliases added to .bashrc")"
msg_success "$(translate "Figurine installation and configuration completed successfully.")"
register_tool "figurine" true "$FUNC_VERSION"
}
# ==========================================================
update_pve_appliance_manager() {
msg_info "$(translate "Updating PVE application manager...")"
if pveam update > /dev/null 2>&1; then
msg_ok "$(translate "PVE application manager updated")"
else
msg_warn "$(translate "No updates or failed to fetch templates")"
fi
}
# ==========================================================
_update_existing_log2ram_custom() {
local func_version="$1"
local log2ram_bin=""
local candidate resolved tmp_file
pmx_journal_context "_update_existing_log2ram_custom" "$func_version"
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"
sed 's/rsync -aAXv /rsync -aXv --no-acls /g' "$log2ram_bin" | pmx_write_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"
pmx_write_file /etc/logrotate.d/proxmox-backup-api < "$tmp_file"
rm -f "$tmp_file"
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"
pmx_write_file /etc/cron.hourly/proxmox-backup-logrotate < "$tmp_file"
chmod 0755 /etc/cron.hourly/proxmox-backup-logrotate
chown root:root /etc/cron.hourly/proxmox-backup-logrotate
rm -f "$tmp_file"
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
pmx_write_file /usr/local/bin/log2ram-check.sh < "$tmp_file"
chmod 0755 /usr/local/bin/log2ram-check.sh
chown root:root /usr/local/bin/log2ram-check.sh
rm -f "$tmp_file"
fi
register_tool "log2ram" true "$func_version"
msg_success "$(translate "Log2RAM installation and configuration completed successfully.")"
}
configure_log2ram() {
local FUNC_VERSION="1.5"
local existing_log2ram_bin=""
pmx_journal_context "configure_log2ram" "$FUNC_VERSION"
# 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
RAM_SIZE_GB=$(free -g | awk '/^Mem:/{print $2}')
[[ -z "$RAM_SIZE_GB" || "$RAM_SIZE_GB" -eq 0 ]] && RAM_SIZE_GB=4
if (( RAM_SIZE_GB <= 8 )); then
DEFAULT_SIZE="128" # MiB
DEFAULT_HOURS="1"
elif (( RAM_SIZE_GB <= 16 )); then
DEFAULT_SIZE="256"
DEFAULT_HOURS="3"
else
DEFAULT_SIZE="512"
DEFAULT_HOURS="6"
fi
USER_SIZE=$(whiptail --title "Log2RAM" --inputbox \
"$(translate "Enter the maximum size (in MB) to allocate for /var/log in RAM (e.g. 128, 256, 512):")\n\n$(translate "Recommended for $RAM_SIZE_GB GB RAM:") ${DEFAULT_SIZE}M" \
12 70 "$DEFAULT_SIZE" 3>&1 1>&2 2>&3) || return 0
if ! [[ "$USER_SIZE" =~ ^[0-9]+$ ]]; then
msg_error "$(translate "Invalid size. Please enter a number in MB (e.g., 128, 256, 512).")"
return 1
fi
(( USER_SIZE < 64 )) && USER_SIZE=64 # mínimo razonable
(( USER_SIZE > 8192 )) && USER_SIZE=8192 # límite de seguridad
LOG2RAM_SIZE="${USER_SIZE}M"
CRON_HOURS=$(whiptail --title "Log2RAM" --radiolist \
"$(translate "Select the sync interval (in hours):")\n\n$(translate "Suggested interval: every $DEFAULT_HOURS hour(s)")" \
15 70 5 \
"1" "$(translate "Every hour")" $([[ "$DEFAULT_HOURS" = "1" ]] && echo ON || echo OFF) \
"3" "$(translate "Every 3 hours")" $([[ "$DEFAULT_HOURS" = "3" ]] && echo ON || echo OFF) \
"6" "$(translate "Every 6 hours")" $([[ "$DEFAULT_HOURS" = "6" ]] && echo ON || echo OFF) \
"12" "$(translate "Every 12 hours")" OFF \
3>&1 1>&2 2>&3) || return 0
if whiptail --title "Log2RAM" --yesno "$(translate "Enable auto-sync if /var/log exceeds 90% of its size?")" 10 60; then
ENABLE_AUTOSYNC=true
else
ENABLE_AUTOSYNC=false
fi
msg_info "$(translate "Cleaning previous Log2RAM installation...")"
pmx_disable_service log2ram || true
pmx_disable_service log2ram-daily.timer || true
local obsolete_path
for obsolete_path in \
/etc/cron.d/log2ram /etc/cron.d/log2ram-auto-sync \
/etc/cron.hourly/log2ram /etc/cron.daily/log2ram \
/etc/cron.weekly/log2ram /etc/cron.monthly/log2ram \
/usr/local/bin/log2ram-check.sh /usr/local/bin/log2ram /usr/sbin/log2ram \
/etc/systemd/system/log2ram.service \
/etc/systemd/system/log2ram-daily.timer \
/etc/systemd/system/log2ram-daily.service \
/etc/systemd/system/sysinit.target.wants/log2ram.service \
/etc/log2ram.conf /etc/log2ram.conf.* /etc/logrotate.d/log2ram
do
pmx_remove_file "$obsolete_path" 2>/dev/null || true
done
rm -rf /etc/systemd/system/log2ram.service.d 2>/dev/null || true
rm -rf /var/log.hdd /tmp/log2ram 2>/dev/null || true
pmx_record_execution "Reload systemd configuration" "systemctl daemon-reload"
systemctl daemon-reload >/dev/null 2>&1 || true
pmx_record_execution "Restart cron" "systemctl restart cron"
systemctl restart cron >/dev/null 2>&1 || true
msg_ok "$(translate "Previous installation cleaned")"
msg_info "$(translate "Installing Log2RAM from GitHub...")"
if ! command -v git >/dev/null 2>&1; then
msg_info "$(translate "Installing required package: git")"
pmx_record_execution "Update package lists for Log2RAM" "apt-get update -qq"
apt-get update -qq >/dev/null 2>&1
pmx_install_pkg git
fi
rm -rf /tmp/log2ram 2>/dev/null || true
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
cd /tmp/log2ram || { msg_error "$(translate "Failed to access log2ram directory")"; return 1; }
pmx_record_execution "Run the Log2RAM installer" "bash install.sh"
if ! bash install.sh >>/tmp/log2ram_install.log 2>&1; then
msg_error "$(translate "Failed to run log2ram installer. Check /tmp/log2ram_install.log")"
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"
pmx_edit_file "$_l2r_bin" 's/rsync -aAXv /rsync -aXv --no-acls /g'
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
pmx_write_file /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
pmx_write_file /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
pmx_record_execution "Reload systemd configuration" "systemctl daemon-reload"
systemctl daemon-reload >/dev/null 2>&1 || true
if [[ -f /etc/log2ram.conf ]] && command -v log2ram >/dev/null 2>&1; then
msg_ok "$(translate "Log2RAM installed successfully")"
else
msg_error "$(translate "Log2RAM installation verification failed. Check /tmp/log2ram_install.log")"
return 1
fi
pmx_edit_file /etc/log2ram.conf "s/^SIZE=.*/SIZE=$LOG2RAM_SIZE/"
LOG2RAM_BIN="$(command -v log2ram || echo /usr/sbin/log2ram)"
pmx_write_file /etc/cron.d/log2ram <<EOF
# Log2RAM periodic sync - Created by ProxMenux
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""
0 */$CRON_HOURS * * * root $LOG2RAM_BIN write >/dev/null 2>&1
EOF
chmod 0644 /etc/cron.d/log2ram
chown root:root /etc/cron.d/log2ram
msg_ok "$(translate "Log2RAM write scheduled every") $CRON_HOURS $(translate "hour(s)")"
if [[ "$ENABLE_AUTOSYNC" == true ]]; then
pmx_write_file /usr/local/bin/log2ram-check.sh <<'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 +x /usr/local/bin/log2ram-check.sh
pmx_write_file /etc/cron.d/log2ram-auto-sync <<'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=""
*/5 * * * * root /usr/local/bin/log2ram-check.sh >/dev/null 2>&1
EOF
chmod 0644 /etc/cron.d/log2ram-auto-sync
chown root:root /etc/cron.d/log2ram-auto-sync
msg_ok "$(translate "Auto-sync enabled when /var/log exceeds 80% of") $LOG2RAM_SIZE"
else
pmx_remove_file /usr/local/bin/log2ram-check.sh 2>/dev/null || true
pmx_remove_file /etc/cron.d/log2ram-auto-sync 2>/dev/null || true
msg_info2 "$(translate "Auto-sync was not enabled")"
fi
# --- Ajuste de systemd-journald proporcional al tamaño de Log2RAM ---
msg_info "$(translate "Adjusting systemd-journald limits to match Log2RAM size...")"
if [[ -f /etc/systemd/journald.conf ]]; then
cp -n /etc/systemd/journald.conf "/etc/systemd/journald.conf.bak.$(date +%Y%m%d-%H%M%S)"
BAK_OK=$?
fi
SIZE_MB=$(echo "$LOG2RAM_SIZE" | tr -dc '0-9')
# Repartos: 55% persistente / 10% libre / 25% runtime (pisos mínimos)
USE_MB=$(( SIZE_MB * 55 / 100 ))
KEEP_MB=$(( SIZE_MB * 10 / 100 ))
RUNTIME_MB=$(( SIZE_MB * 25 / 100 ))
[ "$USE_MB" -lt 80 ] && USE_MB=80
[ "$RUNTIME_MB" -lt 32 ] && RUNTIME_MB=32
[ "$KEEP_MB" -lt 8 ] && KEEP_MB=8
# Reescribir bloque [Journal] de forma segura
pmx_edit_file /etc/systemd/journald.conf '/^\[Journal\]/,$d' 2>/dev/null || true
pmx_append_file /etc/systemd/journald.conf <<EOF
[Journal]
Storage=persistent
SplitMode=none
RateLimitIntervalSec=30s
RateLimitBurst=1000
ForwardToSyslog=no
ForwardToWall=no
Seal=no
Compress=yes
SystemMaxUse=${USE_MB}M
SystemKeepFree=${KEEP_MB}M
RuntimeMaxUse=${RUNTIME_MB}M
# MaxLevelStore=info: required for ProxMenux Monitor log display and Fail2Ban detection.
# Using "warning" silently discards most system logs making date filters useless.
MaxLevelStore=info
MaxLevelSyslog=info
MaxLevelKMsg=warning
MaxLevelConsole=notice
MaxLevelWall=crit
EOF
[[ "$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})"
mkdir -p /var/log/pveproxy
chown -R www-data:www-data /var/log/pveproxy
chmod 0750 /var/log/pveproxy
mkdir -p /var/log.hdd/pveproxy
chown -R www-data:www-data /var/log.hdd/pveproxy
chmod 0750 /var/log.hdd/pveproxy
pmx_record_execution "Restart cron" "systemctl restart cron"
systemctl restart cron >/dev/null 2>&1 || true
if ! pmx_apply_setting "service-enabled:log2ram" "systemctl is-enabled log2ram" \
systemctl enable log2ram; 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"
}
# ==========================================================
setup_persistent_network() {
local FUNC_VERSION="1.2"
# description: Pin NIC names to MAC addresses via systemd .link files so kernel updates don't shuffle interface names.
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
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
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")"
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")"
fi
msg_ok "$(translate "Changes will apply after reboot.")"
else
msg_warn "$(translate "No physical interfaces found")"
fi
msg_success "$(translate "Setting up persistent network interfaces successfully.")"
register_tool "persistent_network" true "$FUNC_VERSION"
NECESSARY_REBOOT=1
}
# ==========================================================
# ==========================================================
# Auxiliary help functions
# ==========================================================
# ==========================================================
install_system_utils() {
local FUNC_VERSION="1.1"
# description: Install selected system utilities and track only packages that were newly added by ProxMenux.
local state_file="$BASE_DIR/system_utils.packages"
local new_packages_tmp=""
msg_info2 "$(translate "Installing system utilities...")"
# Build checklist from global PROXMENUX_UTILS array
local checklist_items=()
for util_entry in "${PROXMENUX_UTILS[@]}"; do
IFS=':' read -r pkg cmd desc <<< "$util_entry"
checklist_items+=("$pkg" "$(translate "$desc")" "OFF")
done
exec 3>&1
local selected
selected=$(dialog --clear --backtitle "ProxMenux" \
--title "$(translate "Select utilities to install")" \
--checklist "$(translate "Use SPACE to select, ENTER to confirm")" \
25 80 20 "${checklist_items[@]}" 2>&1 1>&3)
local dialog_exit=$?
exec 3>&-
if [[ $dialog_exit -ne 0 || -z "$selected" ]]; then
msg_warn "$(translate "No utilities selected")"
return 0
fi
clear
show_proxmenux_logo
if ! ensure_repositories; then
msg_error "$(translate "Failed to configure repositories. Installation aborted.")"
return 1
fi
new_packages_tmp="$(mktemp)"
local success=0 failed=0 warning=0
local selected_array
IFS=' ' read -ra selected_array <<< "$selected"
for util in "${selected_array[@]}"; do
util=$(echo "$util" | tr -d '"')
local pkg_cmd="$util" pkg_desc="$util"
local was_installed=false
if dpkg-query -W -f='${Status}' "$util" 2>/dev/null | grep -q '^install ok installed$'; then
was_installed=true
fi
for util_entry in "${PROXMENUX_UTILS[@]}"; do
IFS=':' read -r epkg ecmd edesc <<< "$util_entry"
if [[ "$epkg" == "$util" ]]; then
pkg_cmd="$ecmd"
pkg_desc="$edesc"
break
fi
done
install_single_package "$util" "$pkg_cmd" "$pkg_desc"
local install_result=$?
case $install_result in
0) success=$((success + 1)) ;;
1) failed=$((failed + 1)) ;;
2) warning=$((warning + 1)) ;;
esac
if [[ "$was_installed" == false ]] &&
dpkg-query -W -f='${Status}' "$util" 2>/dev/null | grep -q '^install ok installed$'; then
printf '%s\n' "$util" >> "$new_packages_tmp"
fi
done
if [[ -s "$new_packages_tmp" ]]; then
mkdir -p "$BASE_DIR"
{
[[ -f "$state_file" ]] && cat "$state_file"
cat "$new_packages_tmp"
} | sort -u > "${state_file}.tmp"
mv "${state_file}.tmp" "$state_file"
fi
rm -f "$new_packages_tmp"
hash -r 2>/dev/null
echo
msg_info2 "$(translate "Installation summary"):"
[[ $success -gt 0 ]] && msg_ok "$(translate "Successful"): $success"
[[ $warning -gt 0 ]] && msg_warn "$(translate "With warnings"): $warning"
[[ $failed -gt 0 ]] && msg_error "$(translate "Failed"): $failed"
if [[ -s "$state_file" ]]; then
register_tool "system_utils" true "$FUNC_VERSION"
fi
msg_success "$(translate "Utilities installation completed")"
}
custom_post_category_label() {
case "$1" in
"Basic Settings") translate "Basic Settings" ;;
"System") translate "System" ;;
"Virtualization") translate "Virtualization" ;;
"Network") translate "Network" ;;
"Storage") translate "Storage" ;;
"Security") translate "Security" ;;
"Customization") translate "Customization" ;;
"Monitoring") translate "Monitoring" ;;
"Performance") translate "Performance" ;;
"Optional") translate "Optional" ;;
*) echo "$1" ;;
esac
}
custom_post_description_label() {
case "$1" in
"Configure Proxmox APT repositories") translate "Configure Proxmox APT repositories" ;;
"Update and upgrade system") translate "Update and upgrade system" ;;
"Synchronize time automatically") translate "Synchronize time automatically" ;;
"Skip downloading additional languages") translate "Skip downloading additional languages" ;;
"Install common system utilities") translate "Install common system utilities" ;;
"Optimize journald") translate "Optimize journald" ;;
"Optimize logrotate") translate "Optimize logrotate" ;;
"Increase various system limits") translate "Increase various system limits" ;;
"Optimize Memory") translate "Optimize Memory" ;;
"Enable fast reboots") translate "Enable fast reboots" ;;
"Enable restart on kernel panic") translate "Enable restart on kernel panic" ;;
"Apply AMD CPU fixes") translate "Apply AMD CPU fixes" ;;
"Install relevant guest agent") translate "Install relevant guest agent" ;;
"Enable VFIO IOMMU support") translate "Enable VFIO IOMMU support" ;;
"Force APT to use IPv4") translate "Force APT to use IPv4" ;;
"Apply network optimizations") translate "Apply network optimizations" ;;
"Install Open vSwitch") translate "Install Open vSwitch" ;;
"Enable TCP BBR/Fast Open control") translate "Enable TCP BBR/Fast Open control" ;;
"Interface Names (persistent)") translate "Interface Names (persistent)" ;;
"Optimize ZFS ARC size") translate "Optimize ZFS ARC size" ;;
"Install ZFS auto-snapshot") translate "Install ZFS auto-snapshot" ;;
"Enable ZFS autotrim (SSD/NVMe pools)") translate "Enable ZFS autotrim (SSD/NVMe pools)" ;;
"Increase vzdump backup speed") translate "Increase vzdump backup speed" ;;
"Disable portmapper/rpcbind") translate "Disable portmapper/rpcbind" ;;
"Customize bashrc") translate "Customize bashrc" ;;
"Set up custom MOTD banner") translate "Set up custom MOTD banner" ;;
"Remove subscription banner") translate "Remove subscription banner" ;;
"Install OVH Real Time Monitoring") translate "Install OVH Real Time Monitoring" ;;
"Use pigz for faster gzip compression") translate "Use pigz for faster gzip compression" ;;
"Install and configure Fastfetch") translate "Install and configure Fastfetch" ;;
"Update Proxmox VE Appliance Manager") translate "Update Proxmox VE Appliance Manager" ;;
"Add latest Ceph support") translate "Add latest Ceph support" ;;
"Enable High Availability services") translate "Enable High Availability services" ;;
"Install Figurine") translate "Install Figurine" ;;
"Install and configure Log2RAM") translate "Install and configure Log2RAM" ;;
*) echo "$1" ;;
esac
}
format_custom_post_line() {
local description="$1"
local category="$2"
local max_description_length=52
if command -v python3 >/dev/null 2>&1; then
python3 - "$description" "$category" "$max_description_length" <<'PY'
import sys
import unicodedata
description = sys.argv[1]
category = sys.argv[2]
max_width = int(sys.argv[3])
def display_width(text):
width = 0
for char in text:
if unicodedata.combining(char):
continue
width += 2 if unicodedata.east_asian_width(char) in ("F", "W") else 1
return width
def fit_text(text, width):
result = ""
used = 0
for char in text:
char_width = 0 if unicodedata.combining(char) else (2 if unicodedata.east_asian_width(char) in ("F", "W") else 1)
if used + char_width > width:
break
result += char
used += char_width
return result, used
if display_width(description) > max_width:
description, used_width = fit_text(description, max_width - 3)
description += "..."
used_width += 3
else:
used_width = display_width(description)
print(f"{description}{' ' * (max_width - used_width)} | {category}")
PY
return
fi
if [ ${#description} -gt $max_description_length ]; then
description="${description:0:$((max_description_length - 3))}..."
fi
printf '%-*s | %s' "$max_description_length" "$description" "$category"
}
format_custom_post_header() {
local description_label
local category_label
local checklist_prefix=" "
description_label="$(translate "Description")"
category_label="$(translate "Category")"
printf '%s%s' "$checklist_prefix" "$(format_custom_post_line "$description_label" "$category_label")"
}
# Main menu function
main_menu() {
local header_line
local HEADER
if [[ "$LANGUAGE" == "es" ]]; then
HEADER="Seleccione las opciones a configurar:\n\n Descripción | Categoría"
else
HEADER="$(translate "Choose options to configure:")\n\n Description | Category"
fi
header_line="$(format_custom_post_header)"
HEADER="$(translate "Choose options to configure:")\n\n${header_line}"
declare -A category_order=(
["Basic Settings"]=1 ["System"]=2 ["Virtualization"]=3
["Network"]=4 ["Storage"]=5 ["Security"]=6 ["Customization"]=7
["Monitoring"]=8 ["Performance"]=9 ["Optional"]=10
)
local options=(
"Basic Settings|Configure Proxmox APT repositories|REPOS"
"Basic Settings|Update and upgrade system|APTUPGRADE"
"Basic Settings|Synchronize time automatically|TIMESYNC"
"Basic Settings|Skip downloading additional languages|NOAPTLANG"
"Basic Settings|Install common system utilities|UTILS"
"System|Optimize journald|JOURNALD"
"System|Optimize logrotate|LOGROTATE"
"System|Increase various system limits|LIMITS"
# Entropy (haveged) removed — modern kernels 5.6+ have built-in entropy generation
"System|Optimize Memory|MEMORYFIXES"
"System|Enable fast reboots|KEXEC"
"System|Enable restart on kernel panic|KERNELPANIC"
"Optional|Apply AMD CPU fixes|AMDFIXES"
"Virtualization|Install relevant guest agent|GUESTAGENT"
"Virtualization|Enable VFIO IOMMU support|VFIO_IOMMU"
"Network|Force APT to use IPv4|APTIPV4"
"Network|Apply network optimizations|NET"
"Network|Install Open vSwitch|OPENVSWITCH"
"Network|Enable TCP BBR/Fast Open control|TCPFASTOPEN"
"Network|Interface Names (persistent)|PERSISNET"
"Storage|Optimize ZFS ARC size|ZFSARC"
"Storage|Install ZFS auto-snapshot|ZFSAUTOSNAPSHOT"
"Storage|Enable ZFS autotrim (SSD/NVMe pools)|ZFSAUTOTRIM"
"Storage|Increase vzdump backup speed|VZDUMP"
"Security|Disable portmapper/rpcbind|DISABLERPC"
"Customization|Customize bashrc|BASHRC"
"Customization|Set up custom MOTD banner|MOTD"
"Customization|Remove subscription banner|NOSUBBANNER"
"Monitoring|Install OVH Real Time Monitoring|OVHRTM"
"Performance|Use pigz for faster gzip compression|PIGZ"
"Optional|Install and configure Fastfetch|FASTFETCH"
"Optional|Update Proxmox VE Appliance Manager|PVEAM"
"Optional|Add latest Ceph support|CEPH"
"Optional|Enable High Availability services|ENABLE_HA"
"Optional|Install Figurine|FIGURINE"
"Optional|Install and configure Log2RAM|LOG2RAM"
)
IFS=$'\n' sorted_options=($(for option in "${options[@]}"; do
IFS='|' read -r category description function_name <<< "$option"
printf "%d|%s|%s|%s\n" "${category_order[$category]:-999}" "$category" "$description" "$function_name"
done | sort -n | cut -d'|' -f2-))
unset IFS
local temp_descriptions=()
local temp_categories=()
for option in "${sorted_options[@]}"; do
IFS='|' read -r category description function_name <<< "$option"
local desc_translated
local category_translated
desc_translated="$(custom_post_description_label "$description")"
category_translated="$(custom_post_category_label "$category")"
temp_descriptions+=("$desc_translated")
temp_categories+=("$category_translated")
done
local checklist_items=()
local i=1
local desc_index=0
local previous_category=""
for option in "${sorted_options[@]}"; do
IFS='|' read -r category description function_name <<< "$option"
if [[ "$category" != "$previous_category" && "$category" == "Optional" && -n "$previous_category" ]]; then
checklist_items+=("" "==============================================================" "")
fi
local desc_translated="${temp_descriptions[$desc_index]}"
local category_translated="${temp_categories[$desc_index]}"
desc_index=$((desc_index + 1))
local line
line="$(format_custom_post_line "$desc_translated" "$category_translated")"
checklist_items+=("$i" "$line" "off")
i=$((i + 1))
previous_category="$category"
done
exec 3>&1
selected_indices=$(dialog --clear \
--backtitle "ProxMenux" \
--title "$(translate "Post-Installation Options")" \
--checklist "$HEADER" 22 88 15 \
"${checklist_items[@]}" \
2>&1 1>&3)
local dialog_exit=$?
exec 3>&-
if [[ $dialog_exit -ne 0 || -z "$selected_indices" ]]; then
exit 0
fi
declare -A selected_functions
read -ra indices_array <<< "$selected_indices"
for index in "${indices_array[@]}"; do
if [[ -z "$index" ]] || ! [[ "$index" =~ ^[0-9]+$ ]]; then
continue
fi
local item_index=$(( (index - 1) * 3 + 1 ))
if [[ $item_index -lt ${#checklist_items[@]} ]]; then
local selected_line="${checklist_items[$item_index]}"
if [[ "$selected_line" =~ ^.*(\-\-\-|===+).*$ ]]; then
return 1
fi
fi
option=${sorted_options[$((index - 1))]}
IFS='|' read -r _ description function_name <<< "$option"
selected_functions[$function_name]=1
[[ "$function_name" == "FASTFETCH" ]] && selected_functions[MOTD]=0
done
clear
show_proxmenux_logo
msg_title "$SCRIPT_TITLE"
for option in "${sorted_options[@]}"; do
IFS='|' read -r _ description function_name <<< "$option"
if [[ ${selected_functions[$function_name]} -eq 1 ]]; then
case $function_name in
REPOS) setup_proxmox_repositories ;;
APTUPGRADE) apt_upgrade ;;
TIMESYNC) configure_time_sync ;;
NOAPTLANG) skip_apt_languages ;;
UTILS) install_system_utils ;;
JOURNALD) optimize_journald ;;
LOGROTATE) optimize_logrotate ;;
LIMITS) increase_system_limits ;;
# ENTROPY removed — modern kernels 5.6+ have built-in entropy
MEMORYFIXES) optimize_memory_settings ;;
KEXEC) enable_kexec ;;
KERNELPANIC) configure_kernel_panic ;;
AMDFIXES) apply_amd_fixes ;;
GUESTAGENT) install_guest_agent ;;
VFIO_IOMMU) enable_vfio_iommu ;;
APTIPV4) force_apt_ipv4 ;;
NET) apply_network_optimizations ;;
OPENVSWITCH) install_openvswitch ;;
TCPFASTOPEN) enable_tcp_fast_open ;;
ZFSARC) optimize_zfs_arc ;;
ZFSAUTOSNAPSHOT) install_zfs_auto_snapshot ;;
ZFSAUTOTRIM) enable_zfs_autotrim ;;
VZDUMP) optimize_vzdump ;;
DISABLERPC) disable_rpc ;;
BASHRC) customize_bashrc ;;
MOTD) setup_motd ;;
NOSUBBANNER) remove_subscription_banner ;;
OVHRTM) install_ovh_rtm ;;
PIGZ) configure_pigz ;;
FASTFETCH) configure_fastfetch ;;
CEPH) install_ceph ;;
ENABLE_HA) enable_ha ;;
FIGURINE) configure_figurine ;;
LOG2RAM) configure_log2ram ;;
PVEAM) update_pve_appliance_manager ;;
PERSISNET) setup_persistent_network ;;
*) echo "Option $function_name not implemented yet" ;;
esac
fi
done
if [[ "$NECESSARY_REBOOT" -eq 1 ]]; then
whiptail --title "Reboot Required" \
--yesno "$(translate "Some changes require a reboot to take effect. Do you want to restart now?")" 10 60
if [[ $? -eq 0 ]]; then
msg_info "$(translate "Removing no longer required packages and purging old cached updates...")"
apt-get -y autoremove >/dev/null 2>&1
apt-get -y autoclean >/dev/null 2>&1
msg_ok "$(translate "Cleanup finished")"
msg_success "$(translate "Press Enter to continue...")"
read -r
msg_warn "$(translate "Rebooting the system...")"
reboot
else
msg_info "$(translate "Removing no longer required packages and purging old cached updates...")"
apt-get -y autoremove >/dev/null 2>&1
apt-get -y autoclean >/dev/null 2>&1
msg_ok "$(translate "Cleanup finished")"
msg_info2 "$(translate "You can reboot later manually.")"
msg_success "$(translate "Press Enter to continue...")"
read -r
exit 0
fi
fi
msg_success "$(translate "All changes applied. No reboot required.")"
msg_success "$(translate "Press Enter to return to menu...")"
read -r
clear
}
# Sprint 12B: only run the interactive menu when this script is invoked
# directly. When sourced from another script (e.g. the post-install
# update wrapper that re-runs a single function), don't trigger the
# extremeshok warning or the main menu.
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
check_extremeshok_warning
main_menu
fi