Files
ProxMenux/scripts/share/lxc-mount-manager_minimal.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

1001 lines
38 KiB
Bash

#!/bin/bash
# ==========================================================
# ProxMenux - LXC Mount Manager
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : GPL-3.0
# https://github.com/MacRimi/ProxMenux/blob/main/LICENSE
# Version : 1.0
# ==========================================================
# Description:
# Bind-mounts a host directory into an LXC container using
# Proxmox's native pct set -mpN syntax. Handles the permission
# quirks of unprivileged containers on the host side — never
# modifies anything inside the container.
#
# Features:
# - Unified host-directory picker (mounted CIFS/NFS shares,
# fstab-inactive entries, /mnt/* local dirs, /mnt/pve/*
# Proxmox storages, manual entry).
# - Active fix per source type:
# - CIFS → offer remount with uid=0,gid=0,file_mode=0777
# - NFS → offer chmod 1777 + setfacl on the share
# - Local → offer chmod o+rwx + ACL (unprivileged only)
# - Auto-detects privileged vs unprivileged containers.
# - View / remove existing mp* entries.
# - Optional CT restart at end with mount-point smoke test.
# ==========================================================
BASE_DIR="/usr/local/share/proxmenux"
source "$BASE_DIR/utils.sh"
if [[ -f "/usr/local/share/proxmenux/scripts/global/pmx_journal.sh" ]]; then
source "/usr/local/share/proxmenux/scripts/global/pmx_journal.sh"
fi
load_language
initialize_cache
# ==========================================================
# DIRECTORY DETECTION
# ==========================================================
detect_mounted_shares() {
local mounted_shares=()
while IFS= read -r line; do
local device mount_point fs_type
read -r device mount_point fs_type _ <<< "$line"
local type=""
case "$fs_type" in
nfs|nfs4) type="NFS" ;;
cifs) type="CIFS/SMB" ;;
*) continue ;;
esac
# Skip internal Proxmox mounts
local skip=false
for internal in /mnt/pve/local /mnt/pve/local-lvm /mnt/pve/local-zfs \
/mnt/pve/backup /mnt/pve/dump /mnt/pve/images \
/mnt/pve/template /mnt/pve/snippets /mnt/pve/vztmpl; do
if [[ "$mount_point" == "$internal" || "$mount_point" =~ ^${internal}/ ]]; then
skip=true
break
fi
done
[[ "$skip" == true ]] && continue
local size used
local df_info
df_info=$(df -h "$mount_point" 2>/dev/null | tail -n1)
if [[ -n "$df_info" ]]; then
size=$(echo "$df_info" | awk '{print $2}')
used=$(echo "$df_info" | awk '{print $3}')
else
size="N/A"
used="N/A"
fi
local source="Manual"
[[ "$mount_point" =~ ^/mnt/pve/ ]] && source="Proxmox-Storage"
mounted_shares+=("$mount_point|$device|$type|$size|$used|$source")
done < /proc/mounts
printf '%s\n' "${mounted_shares[@]}"
}
detect_fstab_network_mounts() {
local fstab_mounts=()
while IFS= read -r line; do
[[ "$line" =~ ^[[:space:]]*# ]] && continue
[[ -z "${line// }" ]] && continue
local source mount_point fs_type
read -r source mount_point fs_type _ <<< "$line"
local type=""
case "$fs_type" in
nfs|nfs4) type="NFS" ;;
cifs) type="CIFS/SMB" ;;
*) continue ;;
esac
[[ ! -d "$mount_point" ]] && continue
# Skip if already mounted (already captured by detect_mounted_shares)
local is_mounted=false
while IFS= read -r proc_line; do
local proc_mp proc_fs
read -r _ proc_mp proc_fs _ <<< "$proc_line"
if [[ "$proc_mp" == "$mount_point" && ("$proc_fs" == "nfs" || "$proc_fs" == "nfs4" || "$proc_fs" == "cifs") ]]; then
is_mounted=true
break
fi
done < /proc/mounts
[[ "$is_mounted" == false ]] && fstab_mounts+=("$mount_point|$source|$type|0|0|fstab-inactive")
done < /etc/fstab
printf '%s\n' "${fstab_mounts[@]}"
}
detect_local_directories() {
local local_dirs=()
local network_mps=()
# Collect network mount points to exclude
while IFS='|' read -r mp _ _ _ _ _; do
[[ -n "$mp" ]] && network_mps+=("$mp")
done < <({ detect_mounted_shares; detect_fstab_network_mounts; })
if [[ -d "/mnt" ]]; then
for dir in /mnt/*/; do
[[ ! -d "$dir" ]] && continue
local dir_path="${dir%/}"
[[ "$(basename "$dir_path")" == "pve" ]] && continue
local is_network=false
for nmp in "${network_mps[@]}"; do
[[ "$dir_path" == "$nmp" ]] && is_network=true && break
done
[[ "$is_network" == true ]] && continue
local dir_size
dir_size=$(du -sh "$dir_path" 2>/dev/null | awk '{print $1}')
local_dirs+=("$dir_path|Local|Directory|$dir_size|-|Manual")
done
fi
printf '%s\n' "${local_dirs[@]}"
}
# ==========================================================
# HOST DIRECTORY SELECTION
# ==========================================================
detect_problematic_storage() {
local dir="$1"
local check_source="$2"
local check_type="$3"
while IFS='|' read -r mp _ type _ _ source; do
if [[ "$mp" == "$dir" && "$source" == "$check_source" && "$type" == "$check_type" ]]; then
return 0
fi
done < <(detect_mounted_shares)
return 1
}
select_host_directory_unified() {
local mounted_shares fstab_mounts local_dirs
mounted_shares=$(detect_mounted_shares)
fstab_mounts=$(detect_fstab_network_mounts)
local_dirs=$(detect_local_directories)
# Deduplicate and build option list
local all_entries=()
declare -A seen_paths
# Process network shares (mounted + fstab)
while IFS='|' read -r mp device type size used source; do
[[ -z "$mp" ]] && continue
[[ -n "${seen_paths[$mp]}" ]] && continue
seen_paths["$mp"]=1
local prefix=""
case "$source" in
"Proxmox-Storage") prefix="PVE-" ;;
"fstab-inactive") prefix="fstab(off)-" ;;
*) prefix="" ;;
esac
local info="${prefix}${type}"
[[ "$size" != "N/A" && "$size" != "0" ]] && info="${info} [${used}/${size}]"
all_entries+=("$mp" "$info")
done < <(echo "$mounted_shares"; echo "$fstab_mounts")
# Process local directories
while IFS='|' read -r mp _ type size _ _; do
[[ -z "$mp" ]] && continue
[[ -n "${seen_paths[$mp]}" ]] && continue
seen_paths["$mp"]=1
local info="Local"
[[ -n "$size" && "$size" != "0" ]] && info="Local [${size}]"
all_entries+=("$mp" "$info")
done < <(echo "$local_dirs")
# Add Proxmox storage paths (/mnt/pve/*)
if [[ -d "/mnt/pve" ]]; then
for dir in /mnt/pve/*/; do
[[ ! -d "$dir" ]] && continue
local dir_path="${dir%/}"
[[ -n "${seen_paths[$dir_path]}" ]] && continue
seen_paths["$dir_path"]=1
all_entries+=("$dir_path" "Proxmox-Storage")
done
fi
all_entries+=("MANUAL" "$(translate "Enter path manually")")
local result
result=$(dialog --clear --colors --title "$(translate "Select Host Directory")" \
--menu "\n$(translate "Select the directory to bind to container:")" 25 85 15 \
"${all_entries[@]}" 3>&1 1>&2 2>&3)
local dialog_exit=$?
[[ $dialog_exit -ne 0 ]] && return 1
[[ -z "$result" || "$result" =~ ^━ ]] && return 1
if [[ "$result" == "MANUAL" ]]; then
result=$(whiptail --title "$(translate "Manual Path Entry")" \
--inputbox "$(translate "Enter the full path to the host directory:")" \
10 70 "/mnt/" 3>&1 1>&2 2>&3)
[[ $? -ne 0 ]] && return 1
fi
[[ -z "$result" ]] && return 1
if [[ ! -d "$result" ]]; then
whiptail --title "$(translate "Invalid Path")" \
--msgbox "$(translate "The selected path is not a valid directory:") $result" 8 70
return 1
fi
# Store the storage type as a global so the main flow can act on it later.
# We don't block the user here — the active fix happens after we know the container type.
LMM_HOST_DIR_TYPE="local"
if detect_problematic_storage "$result" "Proxmox-Storage" "CIFS/SMB"; then
LMM_HOST_DIR_TYPE="cifs"
elif detect_problematic_storage "$result" "Proxmox-Storage" "NFS"; then
LMM_HOST_DIR_TYPE="nfs"
fi
echo "$result"
return 0
}
# ==========================================================
# CONTAINER SELECTION
# ==========================================================
select_lxc_container() {
local ct_list
ct_list=$(pct list 2>/dev/null | awk 'NR>1 {print $1, $2, $3}')
if [[ -z "$ct_list" ]]; then
whiptail --title "Error" --msgbox "$(translate "No LXC containers available")" 8 50
return 1
fi
local options=()
while read -r id name status; do
[[ -n "$id" && "$id" =~ ^[0-9]+$ ]] && options+=("$id" "${name:-unnamed} ($status)")
done <<< "$ct_list"
if [[ ${#options[@]} -eq 0 ]]; then
dialog --title "Error" --msgbox "$(translate "No valid containers found")" 8 50
return 1
fi
local ctid
ctid=$(dialog --title "$(translate "Select LXC Container")" \
--menu "$(translate "Select container:")" 25 85 15 \
"${options[@]}" 3>&1 1>&2 2>&3)
[[ $? -ne 0 || -z "$ctid" ]] && return 1
echo "$ctid"
return 0
}
select_container_mount_point() {
local ctid="$1"
local host_dir="$2"
local FUNC_VERSION="1.0"
pmx_journal_context "select_container_mount_point" "$FUNC_VERSION"
local base_name
base_name=$(basename "$host_dir")
while true; do
local choice
choice=$(dialog --clear --title "$(translate "Configure Mount Point inside LXC")" \
--menu "\n$(translate "Where to mount inside container?")" 16 70 3 \
"1" "$(translate "Create new directory in /mnt")" \
"2" "$(translate "Enter path manually")" \
"3" "$(translate "Cancel")" 3>&1 1>&2 2>&3)
[[ $? -ne 0 ]] && return 1
local mount_point
case "$choice" in
1)
mount_point=$(whiptail --inputbox "$(translate "Enter folder name for /mnt:")" \
10 60 "$base_name" 3>&1 1>&2 2>&3)
[[ $? -ne 0 || -z "$mount_point" ]] && continue
mount_point="/mnt/$mount_point"
;;
2)
mount_point=$(whiptail --inputbox "$(translate "Enter full path:")" \
10 70 "/mnt/$base_name" 3>&1 1>&2 2>&3)
[[ $? -ne 0 || -z "$mount_point" ]] && continue
;;
3) return 1 ;;
esac
# Validate path format
if [[ ! "$mount_point" =~ ^/ ]]; then
whiptail --msgbox "$(translate "Path must be absolute (start with /)")" 8 60
continue
fi
# Check if path is already used as a mount point in this CT
if pct config "$ctid" 2>/dev/null | grep -qE "mp=${mount_point}(,|$)"; then
whiptail --msgbox "$(translate "This path is already used as a mount point in this container.")" 8 70
continue
fi
# Create directory inside CT (only if CT is running)
local ct_status
ct_status=$(pct status "$ctid" 2>/dev/null | awk '{print $2}')
if [[ "$ct_status" == "running" ]]; then
pmx_record_execution "create mount directory ${mount_point} in CT ${ctid}" \
"pct exec ${ctid} -- mkdir -p ${mount_point}"
pct exec "$ctid" -- mkdir -p "$mount_point" 2>/dev/null
fi
echo "$mount_point"
return 0
done
}
# ==========================================================
# MOUNT MANAGEMENT
# ==========================================================
get_next_mp_index() {
local ctid="$1"
local conf="/etc/pve/lxc/${ctid}.conf"
if [[ ! "$ctid" =~ ^[0-9]+$ ]] || [[ ! -f "$conf" ]]; then
echo "0"
return 0
fi
local next=0
local used
used=$(awk -F: '/^mp[0-9]+:/ {print $1}' "$conf" | sed 's/mp//' | sort -n)
for idx in $used; do
[[ "$idx" -ge "$next" ]] && next=$((idx + 1))
done
echo "$next"
}
add_bind_mount() {
local ctid="$1"
local host_path="$2"
local ct_path="$3"
local FUNC_VERSION="1.0"
pmx_journal_context "add_bind_mount" "$FUNC_VERSION"
if [[ ! "$ctid" =~ ^[0-9]+$ || -z "$host_path" || -z "$ct_path" ]]; then
msg_error "$(translate "Invalid parameters for bind mount")"
return 1
fi
# Check if this host path is already mounted in this CT
if pct config "$ctid" 2>/dev/null | grep -qF " ${host_path},"; then
msg_warn "$(translate "Mount already exists for this path in container") $ctid"
return 1
fi
local mpidx
mpidx=$(get_next_mp_index "$ctid")
local result
pmx_record_execution "add bind mount ${host_path} to CT ${ctid} at ${ct_path}" \
"pct set ${ctid} -mp${mpidx} ${host_path},mp=${ct_path},shared=1,backup=0"
result=$(pct set "$ctid" -mp${mpidx} "$host_path,mp=$ct_path,shared=1,backup=0" 2>&1)
if [[ $? -eq 0 ]]; then
msg_ok "$(translate "Bind mount added:") $host_path$ct_path (mp${mpidx})"
return 0
else
msg_error "$(translate "Failed to add bind mount:") $result"
return 1
fi
}
# ==========================================================
# VIEW / REMOVE
# ==========================================================
view_mount_points() {
show_proxmenux_logo
msg_title "$(translate "Current LXC Mount Points")"
local ct_list
ct_list=$(pct list 2>/dev/null | awk 'NR>1 {print $1, $2, $3}')
if [[ -z "$ct_list" ]]; then
msg_warn "$(translate "No LXC containers found")"
echo ""
msg_success "$(translate "Press Enter to continue...")"
read -r
return 1
fi
local found_mounts=false
while read -r id name status; do
[[ -z "$id" || ! "$id" =~ ^[0-9]+$ ]] && continue
local conf="/etc/pve/lxc/${id}.conf"
[[ ! -f "$conf" ]] && continue
local mounts
mounts=$(grep "^mp[0-9]*:" "$conf" 2>/dev/null)
[[ -z "$mounts" ]] && continue
found_mounts=true
echo -e "${TAB}${BOLD}$(translate "Container") $id: $name ($status)${CL}"
while IFS= read -r mount_line; do
[[ -z "$mount_line" ]] && continue
local mp_id mount_info host_path container_path options
mp_id=$(echo "$mount_line" | cut -d: -f1)
mount_info=$(echo "$mount_line" | cut -d: -f2-)
host_path=$(echo "$mount_info" | cut -d, -f1)
container_path=$(echo "$mount_info" | grep -o 'mp=[^,]*' | cut -d= -f2)
options=$(echo "$mount_info" | sed 's/^[^,]*,mp=[^,]*,*//')
echo -e "${TAB} ${BGN}$mp_id:${CL} ${BL}$host_path${CL}${BL}$container_path${CL}"
[[ -n "$options" ]] && echo -e "${TAB} ${DGN}$options${CL}"
done <<< "$mounts"
echo ""
done <<< "$ct_list"
if [[ "$found_mounts" == false ]]; then
msg_ok "$(translate "No mount points found in any container")"
fi
echo ""
msg_success "$(translate "Press Enter to continue...")"
read -r
}
remove_mount_point() {
local FUNC_VERSION="1.0"
pmx_journal_context "remove_mount_point" "$FUNC_VERSION"
show_proxmenux_logo
msg_title "$(translate "Remove LXC Mount Point")"
local container_id
container_id=$(select_lxc_container)
[[ $? -ne 0 || -z "$container_id" ]] && return 1
local conf="/etc/pve/lxc/${container_id}.conf"
if [[ ! -f "$conf" ]]; then
msg_error "$(translate "Container configuration not found")"
echo ""
msg_success "$(translate "Press Enter to continue...")"
read -r
return 1
fi
local mounts
mounts=$(grep "^mp[0-9]*:" "$conf" 2>/dev/null)
if [[ -z "$mounts" ]]; then
show_proxmenux_logo
msg_title "$(translate "Remove LXC Mount Point")"
msg_warn "$(translate "No mount points found in container") $container_id"
echo ""
msg_success "$(translate "Press Enter to continue...")"
read -r
return 1
fi
local options=()
while IFS= read -r mount_line; do
[[ -z "$mount_line" ]] && continue
local mp_id mount_info host_path container_path
mp_id=$(echo "$mount_line" | cut -d: -f1)
mount_info=$(echo "$mount_line" | cut -d: -f2-)
host_path=$(echo "$mount_info" | cut -d, -f1)
container_path=$(echo "$mount_info" | grep -o 'mp=[^,]*' | cut -d= -f2)
options+=("$mp_id" "$host_path$container_path")
done <<< "$mounts"
if [[ ${#options[@]} -eq 0 ]]; then
show_proxmenux_logo
msg_title "$(translate "Remove LXC Mount Point")"
msg_warn "$(translate "No valid mount points found")"
echo ""
msg_success "$(translate "Press Enter to continue...")"
read -r
return 1
fi
local selected_mp
selected_mp=$(dialog --clear --title "$(translate "Select Mount Point to Remove")" \
--menu "\n$(translate "Select mount point to remove from container") $container_id:" 20 80 10 \
"${options[@]}" 3>&1 1>&2 2>&3)
[[ $? -ne 0 || -z "$selected_mp" ]] && return 1
local selected_mount_line mount_info host_path container_path
selected_mount_line=$(grep "^${selected_mp}:" "$conf")
mount_info=$(echo "$selected_mount_line" | cut -d: -f2-)
host_path=$(echo "$mount_info" | cut -d, -f1)
container_path=$(echo "$mount_info" | grep -o 'mp=[^,]*' | cut -d= -f2)
local confirm_msg
confirm_msg="$(translate "Remove Mount Point Confirmation:")
$(translate "Container ID"): $container_id
$(translate "Mount Point ID"): $selected_mp
$(translate "Host Path"): $host_path
$(translate "Container Path"): $container_path
$(translate "NOTE: The host directory and its contents will remain unchanged.")
$(translate "Proceed with removal")?"
if ! dialog --clear --title "$(translate "Confirm Mount Point Removal")" --yesno "$confirm_msg" 18 80; then
return 1
fi
show_proxmenux_logo
msg_title "$(translate "Remove LXC Mount Point")"
msg_info "$(translate "Removing mount point") $selected_mp $(translate "from container") $container_id..."
pmx_record_execution "remove mount point ${selected_mp} from CT ${container_id}" \
"pct set ${container_id} --delete ${selected_mp}"
if pct set "$container_id" --delete "$selected_mp" 2>/dev/null; then
msg_ok "$(translate "Mount point removed successfully")"
local ct_status
ct_status=$(pct status "$container_id" | awk '{print $2}')
if [[ "$ct_status" == "running" ]]; then
echo ""
if whiptail --yesno "$(translate "Container is running. Restart to apply changes?")" 8 60; then
msg_info "$(translate "Restarting container...")"
pmx_record_execution "restart CT ${container_id} after removing ${selected_mp}" \
"pct reboot ${container_id}"
if pct reboot "$container_id"; then
sleep 3
msg_ok "$(translate "Container restarted successfully")"
else
msg_warn "$(translate "Failed to restart container — restart manually")"
fi
fi
fi
echo ""
echo -e "${TAB}${BOLD}$(translate "Mount Point Removal Summary:")${CL}"
echo -e "${TAB}${BGN}$(translate "Container:")${CL} ${BL}$container_id${CL}"
echo -e "${TAB}${BGN}$(translate "Removed Mount:")${CL} ${BL}$selected_mp${CL}"
echo -e "${TAB}${BGN}$(translate "Host Path:")${CL} ${BL}$host_path (preserved)${CL}"
echo -e "${TAB}${BGN}$(translate "Container Path:")${CL} ${BL}$container_path (unmounted)${CL}"
else
msg_error "$(translate "Failed to remove mount point")"
fi
echo ""
msg_success "$(translate "Press Enter to continue...")"
read -r
}
# ==========================================================
# ACTIVE FIXES FOR NETWORK STORAGE (CIFS / NFS)
# These functions act on problems instead of just warning about them.
# ==========================================================
lmm_fix_cifs_access() {
local host_dir="$1"
local is_unprivileged="$2"
local FUNC_VERSION="1.0"
pmx_journal_context "lmm_fix_cifs_access" "$FUNC_VERSION"
# CIFS mounted by Proxmox GUI uses uid=0/gid=0 by default (root only).
# The fix: remount with uid/gid that the LXC can access.
# We detect the current mount options and propose a corrected remount.
local mount_src mount_opts
mount_src=$(findmnt -n -o SOURCE --target "$host_dir" 2>/dev/null)
mount_opts=$(findmnt -n -o OPTIONS --target "$host_dir" 2>/dev/null)
if [[ -z "$mount_src" ]]; then
dialog --backtitle "ProxMenux" \
--title "$(translate "CIFS Mount Not Found")" \
--msgbox "$(translate "Could not detect the CIFS mount for this directory. Try accessing it manually.")" 8 70
return 0
fi
# Determine which uid/gid to use
local target_uid target_gid
if [[ "$is_unprivileged" == "1" ]]; then
# Unprivileged LXC: container root (UID 0) maps to host UID 100000.
# Use file_mode/dir_mode 0777 + uid=0/gid=0 — CIFS maps them to everyone.
target_uid=0
target_gid=0
else
target_uid=0
target_gid=0
fi
# Build new options: strip existing uid/gid/file_mode/dir_mode, add ours
local new_opts
new_opts=$(echo "$mount_opts" | sed -E \
's/(^|,)(uid|gid|file_mode|dir_mode)=[^,]*//g' | \
sed 's/^,//')
new_opts="${new_opts},uid=${target_uid},gid=${target_gid},file_mode=0777,dir_mode=0777"
new_opts="${new_opts/#,/}"
if dialog --backtitle "ProxMenux" \
--title "$(translate "Fix CIFS Permissions")" \
--yesno \
"$(translate "This CIFS share is mounted with restrictive permissions.")\n\n\
$(translate "ProxMenux can remount it with open permissions so any LXC can read and write.")\n\n\
$(translate "Current mount options:")\n${mount_opts}\n\n\
$(translate "New mount options to apply:")\n${new_opts}\n\n\
$(translate "Apply fix now? (The share will be briefly remounted)")" \
18 84 3>&1 1>&2 2>&3; then
msg_info "$(translate "Remounting CIFS share with open permissions...")"
pmx_record_execution "remount CIFS share ${mount_src} at ${host_dir}" \
"umount ${host_dir}; mount -t cifs ${mount_src} ${host_dir} -o ${new_opts}"
if umount "$host_dir" 2>/dev/null && \
mount -t cifs "$mount_src" "$host_dir" -o "$new_opts" 2>/dev/null; then
msg_ok "$(translate "CIFS share remounted — LXC containers can now read and write")"
# Update fstab if the mount is there
if grep -qF "$host_dir" /etc/fstab 2>/dev/null; then
pmx_edit_file /etc/fstab \
"s|^\(${mount_src}[[:space:]].*${host_dir}.*cifs[[:space:]]\).*|\1${new_opts} 0 0|" 2>/dev/null || true
msg_ok "$(translate "/etc/fstab updated — permissions will persist after reboot")"
fi
else
msg_warn "$(translate "Could not remount automatically. Try manually or check credentials.")"
fi
fi
}
lmm_fix_nfs_access() {
local host_dir="$1"
local is_unprivileged="$2"
local uid_shift="${3:-100000}"
local FUNC_VERSION="1.0"
pmx_journal_context "lmm_fix_nfs_access" "$FUNC_VERSION"
# NFS: the host cannot override server-side permissions.
# BUT: if the server exports with root_squash (default), we can check
# if no_root_squash or all_squash is possible, and guide the user.
# What we CAN do on the host: apply a sticky+open directory as a cache layer
# if the NFS mount allows it.
local mount_src mount_opts
mount_src=$(findmnt -n -o SOURCE --target "$host_dir" 2>/dev/null)
mount_opts=$(findmnt -n -o OPTIONS --target "$host_dir" 2>/dev/null)
# Try to detect if we can write to the NFS share as root
local can_write=false
local testfile="${host_dir}/.proxmenux_write_test_$$"
if touch "$testfile" 2>/dev/null; then
rm -f "$testfile" 2>/dev/null
can_write=true
fi
local server_hint=""
if [[ -n "$mount_src" ]]; then
server_hint="${mount_src%%:*}"
fi
if [[ "$can_write" == "true" && "$is_unprivileged" == "1" ]]; then
# Root on host CAN write to NFS, but unprivileged LXC UIDs (100000+)
# will be squashed by the NFS server. We can set a world-writable sticky
# dir on the share itself so the container can write to it.
if dialog --backtitle "ProxMenux" \
--title "$(translate "Fix NFS Access for Unprivileged LXC")" \
--yesno \
"$(translate "NFS server export is writable from the host, but unprivileged LXC containers use mapped UIDs (${uid_shift}+) which the NFS server will squash.")\n\n\
$(translate "ProxMenux can apply open permissions on this NFS directory from the host so the container can read and write:")\n\n\
$(translate " chmod 1777 + setfacl o::rwx (applied on the NFS share from this host)")\n\n\
$(translate "Note: this only works if the NFS server does NOT use 'all_squash' for root.")\n\
$(translate "If it still fails, the NFS server export options must be changed on the server.")\n\n\
$(translate "Apply fix now?")" \
18 84 3>&1 1>&2 2>&3; then
pmx_record_execution "apply LXC access permissions to NFS directory ${host_dir}" \
"chmod 1777 and setfacl on ${host_dir}"
if chmod 1777 "$host_dir" 2>/dev/null; then
msg_ok "$(translate "NFS directory permissions set — containers should now be able to write")"
else
msg_warn "$(translate "chmod failed — NFS server may be restricting changes from root")"
fi
if command -v setfacl >/dev/null 2>&1; then
setfacl -m o::rwx "$host_dir" 2>/dev/null || true
setfacl -m d:o::rwx "$host_dir" 2>/dev/null || true
fi
fi
elif [[ "$can_write" == "false" ]]; then
# Even root cannot write — NFS server is fully restrictive
local server_msg=""
[[ -n "$server_hint" ]] && server_msg="\n$(translate "NFS server:"): ${server_hint}"
dialog --backtitle "ProxMenux" \
--title "$(translate "NFS Access Restricted")" \
--msgbox \
"$(translate "This NFS share is fully restricted — even the host root cannot write to it.")\n\
${server_msg}\n\n\
$(translate "ProxMenux cannot override NFS server-side permissions from the host.")\n\n\
$(translate "To allow LXC write access, change the NFS export on the server to include:")\n\n\
$(translate " no_root_squash") $(translate "(if only privileged LXCs need write access)")\n\
$(translate " all_squash,anonuid=65534,anongid=65534") $(translate "(for unprivileged LXCs)")\n\n\
$(translate "You can still mount this share for READ-ONLY access.")" \
20 84 3>&1 1>&2 2>&3
fi
}
# ==========================================================
# HOST PERMISSION CHECK (host-side only, never touches the container)
# ==========================================================
lmm_offer_host_permissions() {
local host_dir="$1"
local is_unprivileged="$2"
local FUNC_VERSION="1.0"
pmx_journal_context "lmm_offer_host_permissions" "$FUNC_VERSION"
# Privileged containers: UID 0 inside = UID 0 on host — always accessible
[[ "$is_unprivileged" != "1" ]] && return 0
# Check if 'others' already have r+x (minimum to traverse and read)
local stat_perms others_bits
stat_perms=$(stat -c "%a" "$host_dir" 2>/dev/null) || return 0
others_bits=$(( 8#${stat_perms} & 7 ))
# Check ACLs first if available (takes precedence over mode bits)
if command -v getfacl >/dev/null 2>&1; then
if getfacl -p "$host_dir" 2>/dev/null | grep -q "^other::.*r.*x"; then
return 0 # ACL already grants others r+x or better
fi
fi
# 5 = r-x (bits: r=4, x=1). If already r+x or rwx we're fine.
(( (others_bits & 5) == 5 )) && return 0
# Permissions are insufficient — offer to fix HOST directory only
local current_perms
current_perms=$(stat -c "%A" "$host_dir" 2>/dev/null)
if dialog --backtitle "ProxMenux" \
--title "$(translate "Unprivileged Container Access")" \
--yesno \
"$(translate "The host directory may not be accessible from an unprivileged container.")\n\n\
$(translate "Unprivileged containers map their UIDs to high host UIDs (e.g. 100000+), which appear as 'others' on the host filesystem.")\n\n\
$(translate "Current permissions:"): ${current_perms}\n\n\
$(translate "Apply read+write access for 'others' on the host directory?")\n\n\
$(translate "(Only the host directory is modified. Nothing inside the container is changed.")" \
16 80 3>&1 1>&2 2>&3; then
pmx_record_execution "grant mapped LXC users access to host directory ${host_dir}" \
"chmod o+rwx and setfacl on ${host_dir}"
chmod o+rwx "$host_dir" 2>/dev/null || true
if command -v setfacl >/dev/null 2>&1; then
setfacl -m o::rwx "$host_dir" 2>/dev/null || true
setfacl -m d:o::rwx "$host_dir" 2>/dev/null || true
fi
msg_ok "$(translate "Host directory permissions updated — unprivileged containers can now access it")"
fi
}
# Probe the freshly-applied bind-mount from inside the container.
# Three states the user actually cares about, distinguished here:
#
# 1. Mount point missing → the bind-mount didn't take effect at all
# (pct set / restart problem). Show an error and stop.
# 2. Directory visible but read-only → the classic unprivileged-LXC
# perms trap (host dir is root:root 755 → others = r-x). The dir
# exists, but `touch` fails with EACCES. Surface the actual
# "permission denied" line so the user can `chmod o+rwx` on the
# host path (or re-run the add flow with the host-perms prompt
# accepted this time).
# 3. touch succeeds → the bind-mount is fully working from the CT.
_lmm_verify_writable() {
local container_id="$1"
local ct_mount_point="$2"
local probe=".proxmenux_write_test_$$"
if ! pct exec "$container_id" -- test -d "$ct_mount_point" 2>/dev/null; then
msg_warn "$(translate "Mount point not visible inside the container yet")"
return 1
fi
if pct exec "$container_id" -- touch "$ct_mount_point/$probe" 2>/dev/null; then
pct exec "$container_id" -- rm -f "$ct_mount_point/$probe" 2>/dev/null
msg_ok "$(translate "Mount point is writable from inside the container")"
return 0
fi
msg_warn "$(translate "Mount point is visible but NOT writable from inside the container")"
echo -e "${TAB}${YW}$(translate "Likely cause: host directory permissions deny the container's mapped UID.")${CL}"
echo -e "${TAB}${YW}$(translate "Fix: on the host, run") chmod o+rwx ${ct_mount_point} && setfacl -m o::rwx ${ct_mount_point}${CL}"
echo -e "${TAB}${YW}$(translate "Or re-run this script and accept the 'apply host permissions' prompt.")${CL}"
return 2
}
# ==========================================================
# MAIN FUNCTION — ADD MOUNT
# ==========================================================
mount_host_directory_minimal() {
local FUNC_VERSION="1.0"
# Step 1: Select container
local container_id
container_id=$(select_lxc_container)
[[ $? -ne 0 || -z "$container_id" ]] && return 1
# Step 2: Select host directory
local host_dir
host_dir=$(select_host_directory_unified)
[[ $? -ne 0 || -z "$host_dir" ]] && return 1
# Step 3: Select container mount point
local ct_mount_point
ct_mount_point=$(select_container_mount_point "$container_id" "$host_dir")
[[ $? -ne 0 || -z "$ct_mount_point" ]] && return 1
# Step 4: Get container type info (for display only)
local uid_shift container_type_display
uid_shift=$(awk '/^lxc.idmap.*u 0/ {print $5}' "/etc/pve/lxc/${container_id}.conf" 2>/dev/null | head -1)
local is_unprivileged
is_unprivileged=$(grep "^unprivileged:" "/etc/pve/lxc/${container_id}.conf" 2>/dev/null | awk '{print $2}')
if [[ "$is_unprivileged" == "1" ]]; then
container_type_display="$(translate "Unprivileged")"
uid_shift="${uid_shift:-100000}"
else
container_type_display="$(translate "Privileged")"
uid_shift="0"
fi
# Step 5: Active fix for network storage (before confirmation, while we know container type)
case "${LMM_HOST_DIR_TYPE:-local}" in
cifs) lmm_fix_cifs_access "$host_dir" "$is_unprivileged" ;;
nfs) lmm_fix_nfs_access "$host_dir" "$is_unprivileged" "$uid_shift" ;;
esac
# Step 6: Confirmation
local confirm_msg
confirm_msg="$(translate "Mount Configuration Summary:")
$(translate "Container ID"): $container_id ($container_type_display)
$(translate "Host Directory"): $host_dir
$(translate "Container Mount Point"): $ct_mount_point
$(translate "IMPORTANT NOTES:")
- $(translate "Nothing inside the container is modified")
- $(if [[ "$is_unprivileged" == "1" ]]; then
translate "Host directory access for unprivileged containers has been prepared above"
else
translate "Privileged container — host root maps directly, no permission changes needed"
fi)
$(translate "Proceed")?"
if ! dialog --clear --title "$(translate "Confirm Mount")" --yesno "$confirm_msg" 22 80; then
return 1
fi
show_proxmenux_logo
msg_title "$(translate "Mount Host Directory to LXC")"
msg_ok "$(translate "Container:") $container_id ($container_type_display)"
msg_ok "$(translate "Host directory:") $host_dir"
msg_ok "$(translate "Container mount point:") $ct_mount_point"
# Step 7: Add bind mount
if ! add_bind_mount "$container_id" "$host_dir" "$ct_mount_point"; then
echo ""
msg_success "$(translate "Press Enter to continue...")"
read -r
return 1
fi
# Step 8: Host permission check for local dirs (only if not already handled above for CIFS/NFS)
if [[ "${LMM_HOST_DIR_TYPE:-local}" == "local" ]]; then
lmm_offer_host_permissions "$host_dir" "$is_unprivileged"
fi
# Step 9: Summary
echo ""
echo -e "${TAB}${BOLD}$(translate "Mount Added Successfully:")${CL}"
echo -e "${TAB}${BGN}$(translate "Container:")${CL} ${BL}$container_id${CL}"
echo -e "${TAB}${BGN}$(translate "Host Directory:")${CL} ${BL}$host_dir${CL}"
echo -e "${TAB}${BGN}$(translate "Mount Point:")${CL} ${BL}$ct_mount_point${CL}"
if [[ "$is_unprivileged" == "1" ]]; then
echo -e "${TAB}${YW}$(translate "Unprivileged container — UID offset:") ${uid_shift}${CL}"
else
echo -e "${TAB}${DGN}$(translate "Privileged container — direct root access")${CL}"
fi
echo ""
# Step 10: Activate the mount and verify it's WRITABLE from inside
# the container.
#
# The kernel only picks up a new `pct set mp*` after the CT is
# started / rebooted, so a running CT needs a reboot and a stopped
# CT needs a start. In both cases the check used to be a read-only
# `test -d $ct_mount_point` — which succeeds whenever the directory
# exists, even if the bind-mount is read-only because the host
# path's "others" bits don't grant rwx (the unprivileged-LXC trap).
# Replace it with a real touch+rm round-trip so the user is told
# straight away when writes will fail — that's the surprise the
# bind-mount is supposed to spare them.
local ct_status
ct_status=$(pct status "$container_id" 2>/dev/null | awk '{print $2}')
pmx_journal_context "mount_host_directory_minimal" "$FUNC_VERSION"
echo ""
if [[ "$ct_status" == "running" ]]; then
if whiptail --yesno "$(translate "Restart container to activate mount?")" 8 60; then
msg_info "$(translate "Restarting container...")"
pmx_record_execution "restart CT ${container_id} to activate bind mount" \
"pct reboot ${container_id}"
if pct reboot "$container_id"; then
sleep 5
msg_ok "$(translate "Container restarted successfully")"
_lmm_verify_writable "$container_id" "$ct_mount_point"
else
msg_warn "$(translate "Failed to restart — restart manually to activate mount")"
fi
fi
else
# A stopped CT can't tell us whether the bind-mount will work
# until it boots, so offer to start it now. If the user
# declines, fall back to the informational line.
if whiptail --yesno "$(translate "Container is stopped. Start it now to verify the mount works?")" 8 70; then
msg_info "$(translate "Starting container...")"
pmx_record_execution "start CT ${container_id} to activate and verify bind mount" \
"pct start ${container_id}"
if pct start "$container_id"; then
sleep 5
msg_ok "$(translate "Container started successfully")"
_lmm_verify_writable "$container_id" "$ct_mount_point"
else
msg_warn "$(translate "Failed to start container — start manually and check the mount")"
fi
else
msg_ok "$(translate "Container will pick up the mount on next start")"
fi
fi
echo ""
msg_success "$(translate "Press Enter to continue...")"
read -r
}
# ==========================================================
# MAIN MENU
# ==========================================================
main_menu() {
while true; do
local choice
choice=$(dialog --title "$(translate "LXC Mount Manager")" \
--menu "\n$(translate "Choose an option:")" 18 80 5 \
"1" "$(translate "Add: Mount Host Directory into LXC")" \
"2" "$(translate "View Mount Points")" \
"3" "$(translate "Remove Mount Point")" \
"4" "$(translate "Exit")" 3>&1 1>&2 2>&3)
case $choice in
1) mount_host_directory_minimal ;;
2) view_mount_points ;;
3) remove_mount_point ;;
4|"") exit 0 ;;
esac
done
}
main_menu