Files
ProxMenux/scripts/share/lxc-mount-manager_minimal.sh
T
MacRimi 0ac84dc3e4 lxc-mount-manager: start stopped CT on request and verify writes
Two related improvements to the post-add verification step that the
user hit while testing the stopped-CT case.

A stopped container couldn't be probed at all — the previous patch
just told the user "mount will activate on next start" and left
them to discover any issues later (the typical issue being
permission denied on the host directory, since the dialog confirms
the bind-mount was added but never proves it works). Offer to start
the container right now so the user gets feedback in the same
session; if they decline, fall back to the informational line.

The post-restart / post-start probe used `test -d $ct_mount_point`
which only checks that the directory is visible inside the
container. That always succeeds whenever the bind-mount took
effect, even if the host directory permissions don't let the
unprivileged-LXC mapped uid write — exactly the case the user just
ran into with /mnt/disk-sda (700 → others gets r-x). Replace with a
touch+rm probe in a new `_lmm_verify_writable` helper used by both
branches so the user is told straight away when writes will fail
and, when they will, is given the exact `chmod o+rwx` / `setfacl`
command and a pointer to the host-perms prompt.

Verified on .55 / LXC 112 (unprivileged) against /mnt/disk-sda:
container stopped → start prompt → start → directory visible →
touch probe → success.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 18:36:17 +02:00

962 lines
36 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"
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 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
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"
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
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() {
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..."
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...")"
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"
# 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...")"
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
sed -i "s|^\(${mount_src}[[:space:]].*${host_dir}.*cifs[[:space:]]\).*|\1${new_opts} 0 0|" /etc/fstab 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}"
# 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
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"
# 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
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() {
# 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}')
echo ""
if [[ "$ct_status" == "running" ]]; then
if whiptail --yesno "$(translate "Restart container to activate mount?")" 8 60; then
msg_info "$(translate "Restarting container...")"
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...")"
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