mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2025-10-02 08:06:17 +00:00
453 lines
18 KiB
Bash
453 lines
18 KiB
Bash
#!/usr/bin/env bash
|
|
# ==========================================================
|
|
# ProxMenux - Global Share Functions (reusable)
|
|
# File: scripts/global/share_common.func
|
|
# ==========================================================
|
|
# All console-visible strings MUST be wrapped with $(translate "...").
|
|
# Comments and code are in English, as requested.
|
|
# Requires: utils.sh (show_proxmenux_logo, msg_info, msg_ok, msg_warn, msg_error, msg_title, etc.)
|
|
# ==========================================================
|
|
|
|
# Guard against multiple sourcing
|
|
if [[ -n "${__PROXMENUX_SHARE_COMMON__}" ]]; then
|
|
return 0
|
|
fi
|
|
__PROXMENUX_SHARE_COMMON__=1
|
|
|
|
# Default group name used for shared folders (can be overridden by caller)
|
|
: "${PROXMENUX_DEFAULT_SHARE_GROUP:=sharedfiles}"
|
|
|
|
# Where to store simple mappings (dir -> group) to remember choices (optional)
|
|
: "${PROXMENUX_SHARE_MAP_DB:=/usr/local/share/proxmenux/share-map.db}"
|
|
|
|
# Ensure the mapping DB exists (best-effort)
|
|
mkdir -p "$(dirname "$PROXMENUX_SHARE_MAP_DB")" 2>/dev/null || true
|
|
touch "$PROXMENUX_SHARE_MAP_DB" 2>/dev/null || true
|
|
|
|
# Read mapping: returns group name for a given path
|
|
pmx_share_map_get() {
|
|
# comments in English
|
|
# Usage: pmx_share_map_get "/mnt/myshare"
|
|
local key="$1"
|
|
awk -F'=' -v k="$key" '$1==k {print $2}' "$PROXMENUX_SHARE_MAP_DB" 2>/dev/null | tail -n1
|
|
}
|
|
|
|
# Write mapping: persist association between directory and group
|
|
pmx_share_map_set() {
|
|
# comments in English
|
|
# Usage: pmx_share_map_set "/mnt/myshare" "sharedfiles"
|
|
local key="$1" val="$2"
|
|
# Remove any existing line for this key
|
|
sed -i "\|^${key}=|d" "$PROXMENUX_SHARE_MAP_DB" 2>/dev/null || true
|
|
echo "${key}=${val}" >> "$PROXMENUX_SHARE_MAP_DB"
|
|
}
|
|
|
|
# Select container from available LXC containers
|
|
select_container() {
|
|
CONTAINERS=$(pct list | awk 'NR>1 {print $1, $3}' | xargs -n2)
|
|
if [ -z "$CONTAINERS" ]; then
|
|
msg_error "$(translate 'No containers available in Proxmox.')"
|
|
return 1
|
|
fi
|
|
|
|
CONTAINER_ID=$(whiptail --title "$(translate 'Select Container')" \
|
|
--menu "$(translate 'Select the LXC container:')" 20 70 10 $CONTAINERS 3>&1 1>&2 2>&3)
|
|
|
|
if [ -z "$CONTAINER_ID" ]; then
|
|
msg_error "$(translate 'No container selected.')"
|
|
return 1
|
|
fi
|
|
|
|
if ! pct list | awk 'NR>1 {print $1}' | grep -qw "$CONTAINER_ID"; then
|
|
msg_error "$(translate 'Container with ID') $CONTAINER_ID $(translate 'does not exist.')"
|
|
return 1
|
|
fi
|
|
|
|
msg_ok "$(translate 'Container selected:') $CONTAINER_ID"
|
|
echo "$CONTAINER_ID"
|
|
}
|
|
|
|
# Validate container ID and handle running state
|
|
validate_container_id() {
|
|
local ctid="$1"
|
|
if [ -z "$ctid" ]; then
|
|
msg_error "$(translate 'Container ID not defined. Make sure to select a container first.')"
|
|
return 1
|
|
fi
|
|
|
|
if pct status "$ctid" | grep -q "running"; then
|
|
if whiptail --yesno "$(translate 'Container is running. Stop it to apply mount configuration?')" 10 70 --title "$(translate 'Container Running')"; then
|
|
msg_info "$(translate 'Stopping the container before applying configuration...')"
|
|
pct stop "$ctid"
|
|
msg_ok "$(translate 'Container stopped.')"
|
|
else
|
|
msg_warn "$(translate 'Mount will be applied to config but requires container restart.')"
|
|
fi
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# Select existing host directory for mounting
|
|
pmx_select_existing_host_directory() {
|
|
local title="${1:-$(translate "Select Host Directory")}"
|
|
local choice folder_path result
|
|
|
|
while true; do
|
|
choice=$(whiptail --title "$title" --menu "$(translate "Select existing directory location:")" 16 76 4 \
|
|
"mnt" "$(translate "Browse /mnt directories")" \
|
|
"srv" "$(translate "Browse /srv directories")" \
|
|
"media" "$(translate "Browse /media directories")" \
|
|
"custom" "$(translate "Enter custom path")" 3>&1 1>&2 2>&3) || { echo ""; return 1; }
|
|
|
|
case "$choice" in
|
|
mnt)
|
|
folder_path=$(pmx_browse_directory "/mnt")
|
|
[[ -n "$folder_path" ]] && { echo "$folder_path"; return 0; }
|
|
;;
|
|
srv)
|
|
folder_path=$(pmx_browse_directory "/srv")
|
|
[[ -n "$folder_path" ]] && { echo "$folder_path"; return 0; }
|
|
;;
|
|
media)
|
|
folder_path=$(pmx_browse_directory "/media")
|
|
[[ -n "$folder_path" ]] && { echo "$folder_path"; return 0; }
|
|
;;
|
|
custom)
|
|
result=$(whiptail --inputbox "$(translate "Enter full directory path:")" 10 80 "/mnt/shared" --title "$(translate "Custom Path")" 3>&1 1>&2 2>&3) || continue
|
|
if [[ -d "$result" ]]; then
|
|
echo "$result"; return 0
|
|
else
|
|
msg_error "$(translate "Directory does not exist:") $result"
|
|
fi
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# Browse directories in a given path
|
|
pmx_browse_directory() {
|
|
local base_path="$1"
|
|
local dirs=()
|
|
local dir_list=""
|
|
|
|
if [[ ! -d "$base_path" ]]; then
|
|
msg_error "$(translate "Base path does not exist:") $base_path"
|
|
return 1
|
|
fi
|
|
|
|
# Get directories in base path
|
|
while IFS= read -r -d '' dir; do
|
|
dir_name=$(basename "$dir")
|
|
dirs+=("$dir" "$dir_name")
|
|
done < <(find "$base_path" -maxdepth 1 -type d -not -path "$base_path" -print0 2>/dev/null | sort -z)
|
|
|
|
if [[ ${#dirs[@]} -eq 0 ]]; then
|
|
msg_warn "$(translate "No directories found in") $base_path"
|
|
return 1
|
|
fi
|
|
|
|
# Add option to use base path itself
|
|
dirs=("$base_path" "$(translate "Use this directory") ($base_path)" "${dirs[@]}")
|
|
|
|
local selected_dir
|
|
selected_dir=$(whiptail --title "$(translate "Select Directory")" \
|
|
--menu "$(translate "Choose directory from") $base_path:" 20 80 10 "${dirs[@]}" 3>&1 1>&2 2>&3)
|
|
|
|
[[ -n "$selected_dir" ]] && echo "$selected_dir"
|
|
}
|
|
|
|
# Get container user mapping information
|
|
pmx_get_container_user_mapping() {
|
|
local ctid="$1"
|
|
local conf="/etc/pve/lxc/${ctid}.conf"
|
|
|
|
# Check if container is unprivileged
|
|
local unpriv=$(awk '/^unprivileged:/ {print $2}' "$conf" 2>/dev/null)
|
|
|
|
if [[ "$unpriv" == "1" ]]; then
|
|
# Get UID/GID mapping for unprivileged container
|
|
local uid_map=$(awk '/^lxc.idmap:/ && /uid/ {print $2}' "$conf" 2>/dev/null | head -1)
|
|
local gid_map=$(awk '/^lxc.idmap:/ && /gid/ {print $2}' "$conf" 2>/dev/null | head -1)
|
|
|
|
# Default mapping if not found in config
|
|
[[ -z "$uid_map" ]] && uid_map="0 100000 65536"
|
|
[[ -z "$gid_map" ]] && gid_map="0 100000 65536"
|
|
|
|
echo "unprivileged:$uid_map:$gid_map"
|
|
else
|
|
echo "privileged"
|
|
fi
|
|
}
|
|
|
|
# Ask user to reuse an existing group or create a new one
|
|
pmx_choose_or_create_group() {
|
|
# comments in English
|
|
# OUT: echoes chosen group name
|
|
local default_group="${1:-$PROXMENUX_DEFAULT_SHARE_GROUP}"
|
|
local choice group_name
|
|
choice=$(whiptail --title "$(translate "Shared Group")" --menu "$(translate "Choose a group policy for this shared directory:")" 16 78 3 \
|
|
"reuse" "$(translate "Use existing group (recommended)")" \
|
|
"new" "$(translate "Create a new group for isolation")" \
|
|
"custom" "$(translate "Enter a custom existing group name")" 3>&1 1>&2 2>&3) || { echo ""; return 1; }
|
|
|
|
case "$choice" in
|
|
reuse)
|
|
# Ensure default group exists or create it
|
|
pmx_ensure_host_group "$default_group" >/dev/null || return 1
|
|
echo "$default_group"
|
|
;;
|
|
new)
|
|
group_name=$(whiptail --inputbox "$(translate "Enter new group name:")" 10 70 "sharedfiles-project" --title "$(translate "New Group")" 3>&1 1>&2 2>&3) || { echo ""; return 1; }
|
|
[[ -z "$group_name" ]] && { msg_error "$(translate "Group name cannot be empty.")"; echo ""; return 1; }
|
|
pmx_ensure_host_group "$group_name" >/dev/null || return 1
|
|
echo "$group_name"
|
|
;;
|
|
custom)
|
|
group_name=$(whiptail --inputbox "$(translate "Enter existing group name:")" 10 70 "$default_group" --title "$(translate "Group Name")" 3>&1 1>&2 2>&3) || { echo ""; return 1; }
|
|
[[ -z "$group_name" ]] && { msg_error "$(translate "Group name cannot be empty.")"; echo ""; return 1; }
|
|
pmx_ensure_host_group "$group_name" >/dev/null || return 1
|
|
echo "$group_name"
|
|
;;
|
|
*) echo ""; return 1;;
|
|
esac
|
|
}
|
|
|
|
# Ensure group exists on host and return its GID
|
|
pmx_ensure_host_group() {
|
|
# comments in English
|
|
# IN: group name
|
|
# OUT: echoes GID to stdout, return 0 on success
|
|
local group_name="$1"
|
|
if ! getent group "$group_name" >/dev/null 2>&1; then
|
|
if groupadd "$group_name" >/dev/null 2>&1; then
|
|
msg_ok "$(translate "Group created:") $group_name"
|
|
else
|
|
msg_error "$(translate "Failed to create group:") $group_name"
|
|
return 1
|
|
fi
|
|
fi
|
|
local gid
|
|
gid="$(getent group "$group_name" | cut -d: -f3)"
|
|
if [[ -z "$gid" ]]; then
|
|
msg_error "$(translate "Failed to resolve group GID for") $group_name"
|
|
return 1
|
|
fi
|
|
echo "$gid"
|
|
return 0
|
|
}
|
|
|
|
# Prepare directory on host with shared permissions (root:group, 2775, default ACLs)
|
|
pmx_prepare_host_shared_dir() {
|
|
# comments in English
|
|
# IN: dir path, group name
|
|
local dir="$1" group_name="$2"
|
|
[[ -z "$dir" || -z "$group_name" ]] && { msg_error "$(translate "Internal error: missing arguments in pmx_prepare_host_shared_dir")"; return 1; }
|
|
|
|
if [[ ! -d "$dir" ]]; then
|
|
if mkdir -p "$dir" 2>/dev/null; then
|
|
msg_ok "$(translate "Created directory on host:") $dir"
|
|
else
|
|
msg_error "$(translate "Failed to create directory on host:") $dir"
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
chown -R root:"$group_name" "$dir" 2>/dev/null || true
|
|
chmod -R 2775 "$dir" 2>/dev/null || true
|
|
|
|
if command -v setfacl >/dev/null 2>&1; then
|
|
setfacl -R -m d:g:"$group_name":rwx -m d:o::rx -m g:"$group_name":rwx "$dir" 2>/dev/null || true
|
|
msg_ok "$(translate "Default ACLs applied for group inheritance.")"
|
|
else
|
|
msg_warn "$(translate "setfacl not found; default ACLs were not applied.")"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# Common selector for host mountpoint base (used by SMB/NFS/Local)
|
|
pmx_select_host_mount_point() {
|
|
# comments in English
|
|
# OUT: echoes selected mount point path
|
|
local title="${1:-$(translate "Select Mount Point")}"
|
|
local default_path="${2:-/mnt/shared}"
|
|
local choice folder_name result
|
|
|
|
while true; do
|
|
choice=$(whiptail --title "$title" --menu "$(translate "Where do you want the host folder?")" 16 76 4 \
|
|
"mnt" "$(translate "Create folder in /mnt")" \
|
|
"srv" "$(translate "Create folder in /srv")" \
|
|
"media" "$(translate "Create folder in /media")" \
|
|
"custom" "$(translate "Enter custom path")" 3>&1 1>&2 2>&3) || { echo ""; return 1; }
|
|
|
|
case "$choice" in
|
|
mnt)
|
|
folder_name=$(whiptail --inputbox "$(translate "Enter folder name for /mnt:")" 10 70 "shared" --title "$(translate "Folder Name")" 3>&1 1>&2 2>&3) || { echo ""; return 1; }
|
|
[[ -z "$folder_name" ]] && continue
|
|
echo "/mnt/$folder_name"; return 0;;
|
|
srv)
|
|
folder_name=$(whiptail --inputbox "$(translate "Enter folder name for /srv:")" 10 70 "shared" --title "$(translate "Folder Name")" 3>&1 1>&2 2>&3) || { echo ""; return 1; }
|
|
[[ -z "$folder_name" ]] && continue
|
|
echo "/srv/$folder_name"; return 0;;
|
|
media)
|
|
folder_name=$(whiptail --inputbox "$(translate "Enter folder name for /media:")" 10 70 "shared" --title "$(translate "Folder Name")" 3>&1 1>&2 2>&3) || { echo ""; return 1; }
|
|
[[ -z "$folder_name" ]] && continue
|
|
echo "/media/$folder_name"; return 0;;
|
|
custom)
|
|
result=$(whiptail --inputbox "$(translate "Enter full path:")" 10 80 "$default_path" --title "$(translate "Custom Path")" 3>&1 1>&2 2>&3) || { echo ""; return 1; }
|
|
[[ -z "$result" ]] && continue
|
|
echo "$result"; return 0;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# Compute next free mp index for a CT config (mp0..mpN)
|
|
pmx_compute_next_mp_index() {
|
|
# comments in English
|
|
# IN: CTID
|
|
local ctid="$1" conf="/etc/pve/lxc/${ctid}.conf"
|
|
[[ ! -f "$conf" ]] && { echo "0"; return 0; }
|
|
local used idx next=0
|
|
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 host->CT (handles privileged/unprivileged, running state)
|
|
pmx_add_bind_mount_to_ct() {
|
|
# comments in English
|
|
# IN: CTID, host_path, ct_path
|
|
local ctid="$1" host_path="$2" ct_path="$3"
|
|
local conf="/etc/pve/lxc/${ctid}.conf"
|
|
[[ -z "$ctid" || -z "$host_path" || -z "$ct_path" ]] && { msg_error "$(translate "Internal error: missing arguments in pmx_add_bind_mount_to_ct")"; return 1; }
|
|
[[ ! -f "$conf" ]] && { msg_error "$(translate "Container config not found:") $conf"; return 1; }
|
|
|
|
# Avoid duplicates
|
|
if grep -qE "^mp[0-9]+:\s*${host_path}," "$conf"; then
|
|
msg_warn "$(translate "This host path is already present in CT config.")"
|
|
return 0
|
|
fi
|
|
|
|
local unpriv running mpidx mpkey user_mapping
|
|
unpriv=$(awk '/^unprivileged:/ {print $2}' "$conf")
|
|
running=$(pct status "$ctid" 2>/dev/null | grep -qi running && echo "yes" || echo "no")
|
|
mpidx="$(pmx_compute_next_mp_index "$ctid")"
|
|
mpkey="mp${mpidx}"
|
|
user_mapping=$(pmx_get_container_user_mapping "$ctid")
|
|
|
|
# Get host directory group and GID
|
|
local host_group host_gid
|
|
host_group=$(stat -c "%G" "$host_path" 2>/dev/null)
|
|
host_gid=$(stat -c "%g" "$host_path" 2>/dev/null)
|
|
|
|
if [[ "$unpriv" == "1" ]]; then
|
|
# Unprivileged container - need to handle UID/GID mapping
|
|
|
|
|
|
# For unprivileged containers, we need to ensure proper permissions
|
|
# The host directory should be owned by the mapped UID/GID
|
|
local uid_map_start gid_map_start
|
|
uid_map_start=$(echo "$user_mapping" | cut -d: -f2 | awk '{print $2}')
|
|
gid_map_start=$(echo "$user_mapping" | cut -d: -f3 | awk '{print $2}')
|
|
|
|
# Set proper ownership for unprivileged access
|
|
if [[ -n "$host_gid" && -n "$gid_map_start" ]]; then
|
|
local mapped_gid=$((host_gid + gid_map_start))
|
|
|
|
# Ensure the directory is accessible by the mapped GID
|
|
chmod -R g+rwx "$host_path" 2>/dev/null || true
|
|
fi
|
|
|
|
# Append to config; requires restart to take effect
|
|
echo "${mpkey}: ${host_path},mp=${ct_path}" >> "$conf"
|
|
msg_ok "$(translate "Bind mount appended to config (unprivileged).")"
|
|
if [[ "$running" == "yes" ]]; then
|
|
msg_warn "$(translate "Restart the container to apply the mount.")"
|
|
fi
|
|
else
|
|
# Privileged: try hot-apply via pct set; fallback to config
|
|
if [[ "$running" == "yes" ]]; then
|
|
if pct set "$ctid" -mp${mpidx} "${host_path},mp=${ct_path},create=dir" >/dev/null 2>&1; then
|
|
msg_ok "$(translate "Bind mount applied live to running container (privileged).")"
|
|
else
|
|
echo "${mpkey}: ${host_path},mp=${ct_path},create=dir" >> "$conf"
|
|
msg_warn "$(translate "pct set failed; entry appended to config.")"
|
|
fi
|
|
else
|
|
echo "${mpkey}: ${host_path},mp=${ct_path},create=dir" >> "$conf"
|
|
msg_ok "$(translate "Bind mount appended to config (privileged).")"
|
|
fi
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# Ensure same shared group (name + GID) inside CT and add CT users to it
|
|
pmx_sync_group_in_ct() {
|
|
# comments in English
|
|
# IN: CTID, group_name, host_gid, [ct_user1 ct_user2 ...]
|
|
local ctid="$1" group_name="$2" host_gid="$3"; shift 3
|
|
local users=("$@")
|
|
local user_mapping
|
|
|
|
user_mapping=$(pmx_get_container_user_mapping "$ctid")
|
|
|
|
if [[ "$user_mapping" =~ ^unprivileged ]]; then
|
|
# For unprivileged containers, we need to map the GID
|
|
local gid_map_start
|
|
gid_map_start=$(echo "$user_mapping" | cut -d: -f3 | awk '{print $2}')
|
|
local mapped_gid=$((host_gid - gid_map_start))
|
|
|
|
# Ensure the mapped GID is valid (within container's range)
|
|
if [[ $mapped_gid -ge 0 && $mapped_gid -lt 65536 ]]; then
|
|
pct exec "$ctid" -- sh -lc "
|
|
getent group ${group_name} >/dev/null || \
|
|
(addgroup --gid ${mapped_gid} ${group_name} 2>/dev/null || groupadd -g ${mapped_gid} ${group_name} 2>/dev/null)
|
|
" >/dev/null 2>&1
|
|
msg_ok "$(translate "Group synchronized inside unprivileged CT with mapped GID:") $mapped_gid"
|
|
else
|
|
msg_warn "$(translate "GID mapping out of range for unprivileged container")"
|
|
fi
|
|
else
|
|
# Privileged container - use same GID
|
|
pct exec "$ctid" -- sh -lc "
|
|
getent group ${group_name} >/dev/null || \
|
|
(addgroup --gid ${host_gid} ${group_name} 2>/dev/null || groupadd -g ${host_gid} ${group_name} 2>/dev/null)
|
|
" >/dev/null 2>&1
|
|
msg_ok "$(translate "Group synchronized inside privileged CT.")"
|
|
fi
|
|
|
|
# Add users to group
|
|
local u
|
|
for u in "${users[@]}"; do
|
|
pct exec "$ctid" -- sh -lc "id -u \"$u\" >/dev/null 2>&1 && usermod -aG ${group_name} \"$u\" 2>/dev/null || true" >/dev/null 2>&1
|
|
done
|
|
|
|
if [[ ${#users[@]} -gt 0 ]]; then
|
|
msg_ok "$(translate "Users added to group inside CT (if present).")"
|
|
fi
|
|
}
|
|
|
|
# Ensure target path exists inside CT with group+2775
|
|
pmx_prepare_ct_target_path() {
|
|
# comments in English
|
|
# IN: CTID, ct_path, group_name
|
|
local ctid="$1" ct_path="$2" group_name="$3"
|
|
local user_mapping
|
|
|
|
user_mapping=$(pmx_get_container_user_mapping "$ctid")
|
|
|
|
# Create directory and set permissions
|
|
pct exec "$ctid" -- sh -lc "mkdir -p \"$ct_path\"" >/dev/null 2>&1 || true
|
|
|
|
if [[ "$user_mapping" =~ ^unprivileged ]]; then
|
|
# For unprivileged containers, set more permissive permissions
|
|
pct exec "$ctid" -- sh -lc "chgrp \"$group_name\" \"$ct_path\" 2>/dev/null && chmod 2775 \"$ct_path\" 2>/dev/null" >/dev/null 2>&1 || true
|
|
msg_ok "$(translate "Prepared CT target path for unprivileged container.")"
|
|
else
|
|
# Privileged container
|
|
pct exec "$ctid" -- sh -lc "chgrp \"$group_name\" \"$ct_path\" && chmod 2775 \"$ct_path\"" >/dev/null 2>&1 || true
|
|
msg_ok "$(translate "Prepared CT target path with group and 2775.")"
|
|
fi
|
|
}
|