mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-25 08:56:21 +00:00
895 lines
32 KiB
Bash
895 lines
32 KiB
Bash
#!/bin/bash
|
|
# ==========================================================
|
|
# ProxMenux - LXC Mount Manager
|
|
# ==========================================================
|
|
# Author : MacRimi
|
|
# Copyright : (c) 2024 MacRimi
|
|
# License : MIT
|
|
# ==========================================================
|
|
# Description:
|
|
# Adds bind mounts from Proxmox host directories into LXC
|
|
# containers using pct set -mpX (Proxmox native).
|
|
#
|
|
# SAFE DESIGN: This script NEVER modifies permissions, ownership,
|
|
# or ACLs on the host or inside the container. All existing
|
|
# configurations are preserved as-is.
|
|
# ==========================================================
|
|
|
|
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
|
|
}
|
|
|
|
# ==========================================================
|
|
# 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: Offer restart
|
|
echo ""
|
|
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")"
|
|
|
|
# Quick access test (read-only, no files written)
|
|
local ct_status
|
|
ct_status=$(pct status "$container_id" 2>/dev/null | awk '{print $2}')
|
|
if [[ "$ct_status" == "running" ]]; then
|
|
echo ""
|
|
if pct exec "$container_id" -- test -d "$ct_mount_point" 2>/dev/null; then
|
|
msg_ok "$(translate "Mount point is accessible inside container")"
|
|
else
|
|
msg_warn "$(translate "Mount point not yet accessible — may need manual permission adjustment")"
|
|
fi
|
|
fi
|
|
else
|
|
msg_warn "$(translate "Failed to restart — restart manually to activate mount")"
|
|
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
|